pax_global_header00006660000000000000000000000064152015527670014524gustar00rootroot0000000000000052 comment=255eefce1463e3535b8b727843ae469208cb1f79 uv-0.9.17+ds1/000077500000000000000000000000001520155276700127375ustar00rootroot00000000000000uv-0.9.17+ds1/.cargo/000077500000000000000000000000001520155276700141105ustar00rootroot00000000000000uv-0.9.17+ds1/.cargo/config.toml000066400000000000000000000005021520155276700162470ustar00rootroot00000000000000[alias] dev = "run --package uv-dev --features dev" # statically link the C runtime so the executable does not depend on # that shared/dynamic library. # # See: https://github.com/astral-sh/ruff/issues/11503 [target.'cfg(all(target_env = "msvc", target_os = "windows"))'] rustflags = ["-C", "target-feature=+crt-static"] uv-0.9.17+ds1/.claude/000077500000000000000000000000001520155276700142525ustar00rootroot00000000000000uv-0.9.17+ds1/.claude/hooks/000077500000000000000000000000001520155276700153755ustar00rootroot00000000000000uv-0.9.17+ds1/.claude/hooks/post-edit-format.py000066400000000000000000000036441520155276700211540ustar00rootroot00000000000000# /// script # requires-python = ">=3.12" # dependencies = [] # /// """Post-edit hook to auto-format files after Claude edits.""" import json import subprocess import sys from pathlib import Path def format_rust(file_path: str, cwd: str) -> None: """Format Rust files with cargo fmt.""" try: subprocess.run( ["cargo", "fmt", "--", file_path], cwd=cwd, capture_output=True, ) except FileNotFoundError: pass def format_python(file_path: str, cwd: str) -> None: """Format Python files with ruff.""" try: subprocess.run( ["uvx", "ruff", "format", file_path], cwd=cwd, capture_output=True, ) except FileNotFoundError: pass def format_prettier(file_path: str, cwd: str, prose_wrap: bool = False) -> None: """Format files with prettier.""" args = ["npx", "prettier", "--write"] if prose_wrap: args.extend(["--prose-wrap", "always"]) args.append(file_path) try: subprocess.run(args, cwd=cwd, capture_output=True) except FileNotFoundError: pass def main() -> None: import os input_data = json.load(sys.stdin) tool_name = input_data.get("tool_name") tool_input = input_data.get("tool_input", {}) file_path = tool_input.get("file_path") # Only process Write, Edit, and MultiEdit tools if tool_name not in ("Write", "Edit", "MultiEdit"): return if not file_path: return cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()) path = Path(file_path) ext = path.suffix if ext == ".rs": format_rust(file_path, cwd) elif ext in (".py", ".pyi"): format_python(file_path, cwd) elif ext in (".json5", ".yaml", ".yml"): format_prettier(file_path, cwd) elif ext == ".md": format_prettier(file_path, cwd, prose_wrap=True) if __name__ == "__main__": main() uv-0.9.17+ds1/.claude/settings.json000066400000000000000000000004011520155276700170000ustar00rootroot00000000000000{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write|MultiEdit", "hooks": [ { "type": "command", "command": "uv run .claude/hooks/post-edit-format.py" } ] } ] } } uv-0.9.17+ds1/.config/000077500000000000000000000000001520155276700142625ustar00rootroot00000000000000uv-0.9.17+ds1/.config/nextest.toml000066400000000000000000000006241520155276700166530ustar00rootroot00000000000000[profile.default] # Mark tests that take longer than 10s as slow. # Terminate after 120s as a stop-gap measure to terminate on deadlock. slow-timeout = { period = "10s", terminate-after = 12 } [test-groups] serial = { max-threads = 1 } [[profile.default.overrides]] filter = 'test(native_auth)' test-group = 'serial' [[profile.default.overrides]] filter = 'package(uv-keyring)' test-group = 'serial' uv-0.9.17+ds1/.editorconfig000066400000000000000000000006371520155276700154220ustar00rootroot00000000000000# Check http://editorconfig.org for more information # This is the main config file for this project: root = true [*] charset = utf-8 trim_trailing_whitespace = true end_of_line = lf indent_style = space insert_final_newline = true indent_size = 2 [*.{rs,py,pyi}] indent_size = 4 [*.snap] trim_trailing_whitespace = false [crates/uv/tests/help.rs] trim_trailing_whitespace = false [*.md] max_line_length = 100 uv-0.9.17+ds1/.gitattributes000066400000000000000000000001511520155276700156270ustar00rootroot00000000000000* text=auto eol=lf *.inc linguist-language=Rust uv.schema.json linguist-generated=true text=auto eol=lf uv-0.9.17+ds1/.github/000077500000000000000000000000001520155276700142775ustar00rootroot00000000000000uv-0.9.17+ds1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001520155276700164625ustar00rootroot00000000000000uv-0.9.17+ds1/.github/ISSUE_TEMPLATE/1_bug_report.yaml000066400000000000000000000026721520155276700217450ustar00rootroot00000000000000name: Bug report description: Report an error or unexpected behavior labels: ["bug"] body: - type: markdown attributes: value: | **Please review [our guide on interacting with the issue tracker](https://github.com/astral-sh/uv/issues/9452) before opening a new issue.** - type: textarea attributes: label: Summary description: | A clear and concise description of the bug, including [a minimal reproducible example](https://docs.astral.sh/uv/reference/troubleshooting/reproducible-examples/). If we cannot reproduce the bug, it is unlikely that we will be able to help you. Please include the full output of uv with the complete error message. validations: required: true - type: input attributes: label: Platform description: What operating system and architecture are you using? (see `uname -orsm`) placeholder: e.g., macOS 14 arm64, Windows 11 x86_64, Ubuntu 20.04 amd64 validations: required: true - type: input attributes: label: Version description: What version of uv are you using? (see `uv self version`) placeholder: e.g., uv 0.5.20 (1c17662b3 2025-01-15) validations: required: true - type: input attributes: label: Python version description: What version of Python are you using? (see `uv run python --version`) placeholder: e.g., Python 3.12.6 validations: required: false uv-0.9.17+ds1/.github/ISSUE_TEMPLATE/2_feature_request.yaml000066400000000000000000000014211520155276700227700ustar00rootroot00000000000000name: Feature request description: Suggest a new feature or improvement labels: ["enhancement"] body: - type: markdown attributes: value: | **Please review [our guide on interacting with the issue tracker](https://github.com/astral-sh/uv/issues/9452) before opening a new issue.** - type: textarea attributes: label: Summary description: | A clear and concise description of what new feature or behavior you would like to see. If applicable, please describe the current behavior as well. validations: required: true - type: textarea attributes: label: Example description: Provide an example of how the user experience would change or how the new feature would be used. validations: required: false uv-0.9.17+ds1/.github/ISSUE_TEMPLATE/3_question.yaml000066400000000000000000000016311520155276700214400ustar00rootroot00000000000000name: Question description: Ask a question about uv labels: ["question"] body: - type: markdown attributes: value: | **Please review [our guide on interacting with the issue tracker](https://github.com/astral-sh/uv/issues/9452) before opening a new issue.** - type: textarea attributes: label: Question description: Describe your question in detail. validations: required: true - type: input attributes: label: Platform description: What operating system and architecture are you using? (see `uname -orsm`) placeholder: e.g., macOS 14 arm64, Windows 11 x86_64, Ubuntu 20.04 amd64 validations: required: false - type: input attributes: label: Version description: What version of uv are you using? (see `uv self version`) placeholder: e.g., uv 0.5.20 (1c17662b3 2025-01-15) validations: required: false uv-0.9.17+ds1/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000004651520155276700204570ustar00rootroot00000000000000blank_issues_enabled: true contact_links: - name: Documentation url: https://docs.astral.sh/uv about: Please consult the documentation before creating an issue. - name: Community url: https://discord.com/invite/astral-sh about: Join our Discord community to ask questions and collaborate. uv-0.9.17+ds1/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000006501520155276700201010ustar00rootroot00000000000000 ## Summary ## Test Plan uv-0.9.17+ds1/.github/renovate.json5000066400000000000000000000117241520155276700171070ustar00rootroot00000000000000{ $schema: "https://docs.renovatebot.com/renovate-schema.json", dependencyDashboard: true, suppressNotifications: ["prEditedNotification"], extends: [ "github>astral-sh/renovate-config", // For tool versions defined in GitHub Actions: "customManagers:githubActionsVersions", ], labels: ["internal"], schedule: ["* 0-3 * * 1"], semanticCommits: "disabled", separateMajorMinor: false, enabledManagers: ["github-actions", "pre-commit", "cargo", "custom.regex"], cargo: { // See https://docs.renovatebot.com/configuration-options/#rangestrategy rangeStrategy: "update-lockfile", managerFilePatterns: ["/^Cargo\\.toml$/", "/^crates/.*Cargo\\.toml$/"], }, "pre-commit": { enabled: true, }, packageRules: [ // Pin GitHub Actions to immutable SHAs. { matchDepTypes: ["action"], pinDigests: true, }, // Annotate GitHub Actions SHAs with a SemVer version. { extends: ["helpers:pinGitHubActionDigests"], extractVersion: "^(?v?\\d+\\.\\d+\\.\\d+)$", versioning: "regex:^v?(?\\d+)(\\.(?\\d+)\\.(?\\d+))?$", }, { // Disable updates of `zip-rs`; intentionally pinned for now due to ownership change // See: https://github.com/astral-sh/uv/issues/3642 matchPackageNames: ["/zip/"], matchManagers: ["cargo"], enabled: false, }, { // Create dedicated branches to update references to dependencies in the documentation. matchFileNames: ["docs/**/*.md"], commitMessageTopic: "documentation references to {{{depName}}}", semanticCommitType: "docs", semanticCommitScope: null, additionalBranchPrefix: "docs-", }, { // Group upload/download artifact updates, the versions are dependent groupName: "Artifact GitHub Actions dependencies", matchManagers: ["github-actions"], matchDatasources: ["gitea-tags", "github-tags"], matchPackageNames: ["/actions/.*-artifact/"], description: "Weekly update of artifact-related GitHub Actions dependencies", }, { // This package rule disables updates for GitHub runners: // we'd only pin them to a specific version // if there was a deliberate reason to do so groupName: "GitHub runners", matchManagers: ["github-actions"], matchDatasources: ["github-runners"], description: "Disable PRs updating GitHub runners (e.g. 'runs-on: macos-14')", enabled: false, }, { groupName: "pre-commit dependencies", matchManagers: ["pre-commit"], description: "Weekly update of pre-commit dependencies", }, { groupName: "Rust dev-dependencies", matchManagers: ["cargo"], matchDepTypes: ["devDependencies"], description: "Weekly update of Rust development dependencies", }, { // We don't really use PyO3 in this project; it's pulled in as an optional feature // of the PEP 440 and PEP 508 crates, which we vendored and forked. groupName: "pyo3", matchManagers: ["cargo"], matchPackageNames: ["/pyo3/"], description: "Weekly update of pyo3 dependencies", enabled: false, }, { groupName: "pubgrub", matchManagers: ["cargo"], matchDepNames: ["pubgrub", "version-ranges"], description: "version-ranges and pubgrub are in the same Git repository", }, { commitMessageTopic: "MSRV", matchManagers: ["custom.regex"], matchDepNames: ["msrv"], // We have a rolling support policy for the MSRV // 2 releases back * 6 weeks per release * 7 days per week + 1 minimumReleaseAge: "85 days", internalChecksFilter: "strict", groupName: "MSRV", }, { matchManagers: ["custom.regex"], matchDepNames: ["rust"], commitMessageTopic: "Rust", }, ], customManagers: [ // Update major GitHub actions references in documentation. { customType: "regex", managerFilePatterns: ["/^docs/.*\\.md$/"], matchStrings: [ "\\suses: (?[\\w-]+/[\\w-]+)(?/.*)?@(?.+?)\\s", ], datasourceTemplate: "github-tags", versioningTemplate: "regex:^v(?\\d+)$", }, // Minimum supported Rust toolchain version { customType: "regex", managerFilePatterns: ["/(^|/)Cargo\\.toml?$/"], matchStrings: [ 'rust-version\\s*=\\s*"(?\\d+\\.\\d+(\\.\\d+)?)"', ], depNameTemplate: "msrv", packageNameTemplate: "rust-lang/rust", datasourceTemplate: "github-releases", }, // Rust toolchain version { customType: "regex", managerFilePatterns: ["/(^|/)rust-toolchain\\.toml?$/"], matchStrings: [ 'channel\\s*=\\s*"(?\\d+\\.\\d+(\\.\\d+)?)"', ], depNameTemplate: "rust", packageNameTemplate: "rust-lang/rust", datasourceTemplate: "github-releases", }, ], vulnerabilityAlerts: { commitMessageSuffix: "", labels: ["internal", "security"], }, } uv-0.9.17+ds1/.github/workflows/000077500000000000000000000000001520155276700163345ustar00rootroot00000000000000uv-0.9.17+ds1/.github/workflows/build-binaries.yml000066400000000000000000001273031520155276700217560ustar00rootroot00000000000000# Build uv on all platforms. # # Generates both wheels (for PyPI) and archived binaries (for GitHub releases). # # Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a local # artifacts job within `cargo-dist`. name: "Build release binaries" on: workflow_call: inputs: plan: required: true type: string pull_request: paths: # We want to ensure that the maturin builds still work when we change # Project metadata - pyproject.toml - Cargo.toml - .cargo/config.toml - crates/uv-build/Cargo.toml - crates/uv-build/pyproject.toml # Toolchain or dependency versions - Cargo.lock - rust-toolchain.toml # And the workflow itself - .github/workflows/build-binaries.yml concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: PACKAGE_NAME: uv MODULE_NAME: uv PYTHON_VERSION: "3.11" CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 permissions: {} jobs: sdist: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} # uv - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build sdist" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 command: sdist args: --out dist - name: "Test sdist" run: | # We can't use `--find-links` here, since we need maturin, which means no `--no-index`, and without that option # we run the risk that pip pull uv from PyPI instead. pip install dist/${PACKAGE_NAME}-*.tar.gz --force-reinstall ${MODULE_NAME} --help python -m ${MODULE_NAME} --help uvx --help - name: "Upload sdist" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv-sdist path: dist # uv-build - name: "Build sdist uv-build" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 command: sdist args: --out crates/uv-build/dist -m crates/uv-build/Cargo.toml - name: "Test sdist uv-build" run: | pip install crates/uv-build/dist/${PACKAGE_NAME}_build-*.tar.gz --force-reinstall ${MODULE_NAME}-build --help python -m ${MODULE_NAME}_build --help - name: "Upload sdist uv-build" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv_build-sdist path: crates/uv-build/dist macos-x86_64: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: depot-macos-14 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi # uv - name: "Build wheels - x86_64" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: x86_64 args: --release --locked --out dist --features self-update - name: "Upload wheels" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv-macos-x86_64 path: dist - name: "Archive binary" run: | TARGET=x86_64-apple-darwin ARCHIVE_NAME=uv-$TARGET ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz mkdir -p $ARCHIVE_NAME cp target/$TARGET/release/uv $ARCHIVE_NAME/uv cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: artifacts-macos-x86_64 path: | *.tar.gz *.sha256 # uv-build - name: "Build wheels uv-build - x86_64" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: x86_64 args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml - name: "Upload wheels uv-build" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv_build-macos-x86_64 path: crates/uv-build/dist macos-aarch64: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: depot-macos-14 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: arm64 - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi # uv - name: "Build wheels - aarch64" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: aarch64 args: --release --locked --out dist --features self-update - name: "Test wheel - aarch64" run: | pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall ${MODULE_NAME} --help python -m ${MODULE_NAME} --help uvx --help - name: "Upload wheels" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv-aarch64-apple-darwin path: dist - name: "Archive binary" run: | TARGET=aarch64-apple-darwin ARCHIVE_NAME=uv-$TARGET ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz mkdir -p $ARCHIVE_NAME cp target/$TARGET/release/uv $ARCHIVE_NAME/uv cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: artifacts-aarch64-apple-darwin path: | *.tar.gz *.sha256 # uv-build - name: "Build wheels uv-build - aarch64" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: aarch64 args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml - name: "Test wheel - aarch64" run: | pip install ${PACKAGE_NAME}_build --no-index --find-links crates/uv-build/dist --force-reinstall ${MODULE_NAME}-build --help python -m ${MODULE_NAME}_build --help - name: "Upload wheels uv-build" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv_build-aarch64-apple-darwin path: crates/uv-build/dist windows: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: github-windows-2022-x86_64-8 strategy: matrix: platform: - target: x86_64-pc-windows-msvc arch: x64 - target: i686-pc-windows-msvc arch: x86 - target: aarch64-pc-windows-msvc arch: x64 # not relevant here steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: ${{ matrix.platform.arch }} - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi # uv - name: "Build wheels" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.platform.target }} args: --release --locked --out dist --features self-update,windows-gui-bin - name: "Test wheel" if: ${{ !startsWith(matrix.platform.target, 'aarch64') }} shell: bash run: | pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall ${MODULE_NAME} --help python -m ${MODULE_NAME} --help uvx --help uvw --help - name: "Upload wheels" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv-${{ matrix.platform.target }} path: dist - name: "Archive binary" shell: bash run: | ARCHIVE_FILE=uv-${PLATFORM_TARGET}.zip 7z a $ARCHIVE_FILE ./target/${PLATFORM_TARGET}/release/uv.exe 7z a $ARCHIVE_FILE ./target/${PLATFORM_TARGET}/release/uvx.exe 7z a $ARCHIVE_FILE ./target/${PLATFORM_TARGET}/release/uvw.exe sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 env: PLATFORM_TARGET: ${{ matrix.platform.target }} - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: artifacts-${{ matrix.platform.target }} path: | *.zip *.sha256 # uv-build - name: "Build wheels uv-build" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.platform.target }} args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml - name: "Test wheel uv-build" if: ${{ !startsWith(matrix.platform.target, 'aarch64') }} shell: bash run: | pip install ${PACKAGE_NAME}_build --no-index --find-links crates/uv-build/dist --force-reinstall ${MODULE_NAME}-build --help python -m ${MODULE_NAME}_build --help - name: "Upload wheels uv-build" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv_build-${{ matrix.platform.target }} path: crates/uv-build/dist linux: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: depot-ubuntu-latest-4 strategy: matrix: include: - { target: "i686-unknown-linux-gnu", cc: "gcc -m32" } - { target: "x86_64-unknown-linux-gnu", cc: "gcc" } steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi # uv - name: "Build wheels" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.target }} # Generally, we try to build in a target docker container. In this case however, a # 32-bit compiler runs out of memory (4GB memory limit for 32-bit), so we cross compile # from 64-bit version of the container, breaking the pattern from other builds. container: quay.io/pypa/manylinux2014 manylinux: auto args: --release --locked --out dist --features self-update # See: https://github.com/sfackler/rust-openssl/issues/2036#issuecomment-1724324145 before-script-linux: | # Install the 32-bit cross target on 64-bit (noop if we're already on 64-bit) rustup target add ${{ matrix.target }} # If we're running on rhel centos, install needed packages. if command -v yum &> /dev/null; then yum update -y && yum install -y perl-core openssl openssl-devel pkgconfig libatomic # If we're running on i686 we need to symlink libatomic # in order to build openssl with -latomic flag. if [[ ! -d "/usr/lib64" ]]; then ln -s /usr/lib/libatomic.so.1 /usr/lib/libatomic.so else # Support cross-compiling from 64-bit to 32-bit yum install -y glibc-devel.i686 libstdc++-devel.i686 fi else # If we're running on debian-based system. apt update -y && apt-get install -y libssl-dev openssl pkg-config fi env: CC: ${{ matrix.cc }} - name: "Test wheel" if: ${{ startsWith(matrix.target, 'x86_64') }} run: | pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall ${MODULE_NAME} --help python -m ${MODULE_NAME} --help uvx --help - name: "Upload wheels" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv-${{ matrix.target }} path: dist - name: "Archive binary" shell: bash run: | ARCHIVE_NAME=uv-$TARGET ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz mkdir -p $ARCHIVE_NAME cp target/$TARGET/release/uv $ARCHIVE_NAME/uv cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 env: TARGET: ${{ matrix.target }} - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: artifacts-${{ matrix.target }} path: | *.tar.gz *.sha256 # uv-build - name: "Build wheels uv-build" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.target }} manylinux: auto args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml - name: "Test wheel uv-build" if: ${{ startsWith(matrix.target, 'x86_64') }} run: | pip install ${PACKAGE_NAME}_build --no-index --find-links crates/uv-build/dist --force-reinstall ${MODULE_NAME}-build --help python -m ${MODULE_NAME}_build --help - name: "Upload wheels uv-build" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv_build-${{ matrix.target }} path: crates/uv-build/dist linux-arm: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: depot-ubuntu-22.04-8 timeout-minutes: 30 strategy: matrix: platform: - target: aarch64-unknown-linux-gnu arch: aarch64 # see https://github.com/astral-sh/ruff/issues/3791 # and https://github.com/gnzlbg/jemallocator/issues/170#issuecomment-1503228963 maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16 - target: armv7-unknown-linux-gnueabihf arch: armv7 - target: arm-unknown-linux-musleabihf arch: arm steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi # uv - name: "Build wheels" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.platform.target }} # On `aarch64`, use `manylinux: 2_28`; otherwise, use `manylinux: auto`. manylinux: ${{ matrix.platform.arch == 'aarch64' && '2_28' || 'auto' }} docker-options: ${{ matrix.platform.maturin_docker_options }} args: --release --locked --out dist --features self-update - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 name: "Test wheel" with: arch: ${{ matrix.platform.arch == 'arm' && 'armv6' || matrix.platform.arch }} distro: ${{ matrix.platform.arch == 'arm' && 'bullseye' || 'ubuntu20.04' }} install: | apt-get update apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 pip3 install -U pip run: | pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall ${MODULE_NAME} --help # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. # python -m ${MODULE_NAME} --help uvx --help env: | PACKAGE_NAME: ${{ env.PACKAGE_NAME }} MODULE_NAME: ${{ env.MODULE_NAME }} - name: "Upload wheels" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv-${{ matrix.platform.target }} path: dist - name: "Archive binary" shell: bash run: | ARCHIVE_NAME=uv-$TARGET ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz mkdir -p $ARCHIVE_NAME cp target/$TARGET/release/uv $ARCHIVE_NAME/uv cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 env: TARGET: ${{ matrix.platform.target }} - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: artifacts-${{ matrix.platform.target }} path: | *.tar.gz *.sha256 # uv-build - name: "Build wheels uv-build" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.platform.target }} # On `aarch64`, use `manylinux: 2_28`; otherwise, use `manylinux: auto`. manylinux: ${{ matrix.platform.arch == 'aarch64' && '2_28' || 'auto' }} docker-options: ${{ matrix.platform.maturin_docker_options }} args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 name: "Test wheel uv-build" with: arch: ${{ matrix.platform.arch == 'arm' && 'armv6' || matrix.platform.arch }} distro: ${{ matrix.platform.arch == 'arm' && 'bullseye' || 'ubuntu20.04' }} install: | apt-get update apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 pip3 install -U pip run: | pip install ${PACKAGE_NAME}_build --no-index --find-links crates/uv-build/dist --force-reinstall ${MODULE_NAME}-build --help # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. # python -m ${MODULE_NAME}_build --help env: | PACKAGE_NAME: ${{ env.PACKAGE_NAME }} MODULE_NAME: ${{ env.MODULE_NAME }} - name: "Upload wheels uv-build" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv_build-${{ matrix.platform.target }} path: crates/uv-build/dist # Like `linux-arm`. linux-s390x: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} timeout-minutes: 30 runs-on: depot-ubuntu-latest-4 strategy: matrix: platform: - target: s390x-unknown-linux-gnu arch: s390x steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi # uv - name: "Build wheels" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.platform.target }} manylinux: auto docker-options: ${{ matrix.platform.maturin_docker_options }} args: --release --locked --out dist --features self-update rust-toolchain: ${{ matrix.platform.toolchain || null }} - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 if: matrix.platform.arch != 'ppc64' name: "Test wheel" with: arch: ${{ matrix.platform.arch }} distro: ubuntu20.04 install: | apt-get update apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 pip3 install -U pip run: | pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall ${MODULE_NAME} --help # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. # python -m ${MODULE_NAME} --help uvx --help env: | PACKAGE_NAME: ${{ env.PACKAGE_NAME }} MODULE_NAME: ${{ env.MODULE_NAME }} - name: "Upload wheels" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv-${{ matrix.platform.target }} path: dist - name: "Archive binary" shell: bash run: | ARCHIVE_NAME=uv-$TARGET ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz mkdir -p $ARCHIVE_NAME cp target/$TARGET/release/uv $ARCHIVE_NAME/uv cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 env: TARGET: ${{ matrix.platform.target }} - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: artifacts-${{ matrix.platform.target }} path: | *.tar.gz *.sha256 # uv-build - name: "Build wheels uv-build" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.platform.target }} manylinux: auto docker-options: ${{ matrix.platform.maturin_docker_options }} args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 if: matrix.platform.arch != 'ppc64' name: "Test wheel uv-build" with: arch: ${{ matrix.platform.arch }} distro: ubuntu20.04 install: | apt-get update apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 pip3 install -U pip run: | pip install ${PACKAGE_NAME}-build --no-index --find-links crates/uv-build/dist --force-reinstall ${MODULE_NAME}-build --help # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. # python -m ${MODULE_NAME}-build --help env: | PACKAGE_NAME: ${{ env.PACKAGE_NAME }} MODULE_NAME: ${{ env.MODULE_NAME }} - name: "Upload wheels uv-build" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv_build-${{ matrix.platform.target }} path: crates/uv-build/dist # Like `linux-arm`, but install the `gcc-powerpc64-linux-gnu` package. linux-powerpc: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: ubuntu-latest strategy: matrix: platform: - target: powerpc64le-unknown-linux-gnu arch: ppc64le # see https://github.com/astral-sh/uv/issues/6528 maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16 - target: powerpc64-unknown-linux-gnu arch: ppc64 # see https://github.com/astral-sh/uv/issues/6528 maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi # uv - name: "Build wheels" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.platform.target }} manylinux: auto docker-options: ${{ matrix.platform.maturin_docker_options }} args: --release --locked --out dist --features self-update before-script-linux: | if command -v yum &> /dev/null; then yum update -y yum -y install epel-release yum repolist yum install -y gcc-powerpc64-linux-gnu fi # TODO(charlie): Re-enable testing for PPC wheels. # - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 # if: matrix.platform.arch != 'ppc64' # name: "Test wheel" # with: # arch: ${{ matrix.platform.arch }} # distro: ubuntu20.04 # install: | # apt-get update # apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 # pip3 install -U pip # run: | # pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall # ${MODULE_NAME} --help # #(konsti) TODO: Enable this test on all platforms,currently `find_uv_bin` is failingto discover uv here. # # python -m ${MODULE_NAME} --helppython -m ${MODULE_NAME} --help # uvx --help - name: "Upload wheels" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv-${{ matrix.platform.target }} path: dist - name: "Archive binary" shell: bash run: | ARCHIVE_NAME=uv-$TARGET ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz mkdir -p $ARCHIVE_NAME cp target/$TARGET/release/uv $ARCHIVE_NAME/uv cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 env: TARGET: ${{ matrix.platform.target }} - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: artifacts-${{ matrix.platform.target }} path: | *.tar.gz *.sha256 # uv-build - name: "Build wheels uv-build" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.platform.target }} manylinux: auto docker-options: ${{ matrix.platform.maturin_docker_options }} args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml before-script-linux: | if command -v yum &> /dev/null; then yum update -y yum -y install epel-release yum repolist yum install -y gcc-powerpc64-linux-gnu fi # TODO(charlie): Re-enable testing for PPC wheels. - name: "Upload wheels uv-build" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv_build-${{ matrix.platform.target }} path: crates/uv-build/dist # Like `linux-arm`. linux-riscv64: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} timeout-minutes: 30 runs-on: depot-ubuntu-latest-4 strategy: matrix: platform: - target: riscv64gc-unknown-linux-gnu arch: riscv64 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi # uv - name: "Build wheels" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.platform.target }} manylinux: auto docker-options: ${{ matrix.platform.maturin_docker_options }} args: --release --locked --out dist --features self-update - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 name: "Test wheel" with: arch: ${{ matrix.platform.arch }} distro: ubuntu20.04 githubToken: ${{ github.token }} install: | apt-get update apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 pip3 install -U pip run: | pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall ${MODULE_NAME} --help # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. # python -m ${MODULE_NAME} --help uvx --help env: | PACKAGE_NAME: ${{ env.PACKAGE_NAME }} MODULE_NAME: ${{ env.MODULE_NAME }} - name: "Upload wheels" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv-${{ matrix.platform.target }} path: dist - name: "Archive binary" shell: bash run: | ARCHIVE_NAME=uv-$TARGET ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz mkdir -p $ARCHIVE_NAME cp target/$TARGET/release/uv $ARCHIVE_NAME/uv cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 env: TARGET: ${{ matrix.platform.target }} - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: artifacts-${{ matrix.platform.target }} path: | *.tar.gz *.sha256 # uv-build - name: "Build wheels uv-build" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.platform.target }} manylinux: auto docker-options: ${{ matrix.platform.maturin_docker_options }} args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 name: "Test wheel uv-build" with: arch: ${{ matrix.platform.arch }} distro: ubuntu20.04 githubToken: ${{ github.token }} install: | apt-get update apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 pip3 install -U pip run: | pip install ${PACKAGE_NAME}-build --no-index --find-links crates/uv-build/dist --force-reinstall ${MODULE_NAME}-build --help # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. # python -m ${MODULE_NAME}-build --help env: | PACKAGE_NAME: ${{ env.PACKAGE_NAME }} MODULE_NAME: ${{ env.MODULE_NAME }} - name: "Upload wheels uv-build" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv_build-${{ matrix.platform.target }} path: crates/uv-build/dist musllinux: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: ubuntu-latest strategy: matrix: target: - x86_64-unknown-linux-musl - i686-unknown-linux-musl steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi # uv - name: "Build wheels" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.target }} manylinux: musllinux_1_1 args: --release --locked --out dist --features self-update - name: "Test wheel" if: matrix.target == 'x86_64-unknown-linux-musl' uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 with: image: alpine:3.12 options: -v ${{ github.workspace }}:/io -w /io --env MODULE_NAME --env PACKAGE_NAME run: | apk add python3 python3 -m venv .venv .venv/bin/pip install --upgrade pip .venv/bin/pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall .venv/bin/${MODULE_NAME} --help # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. # .venv/bin/python -m ${MODULE_NAME} --help .venv/bin/uvx --help - name: "Upload wheels" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv-${{ matrix.target }} path: dist - name: "Archive binary" shell: bash run: | ARCHIVE_NAME=uv-$TARGET ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz mkdir -p $ARCHIVE_NAME cp target/$TARGET/release/uv $ARCHIVE_NAME/uv cp target/$TARGET/release/uvx $ARCHIVE_NAME/uvx tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 env: TARGET: ${{ matrix.target }} - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: artifacts-${{ matrix.target }} path: | *.tar.gz *.sha256 # uv-build - name: "Build wheels uv-build" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.target }} manylinux: musllinux_1_1 args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml - name: "Test wheel uv-build" if: matrix.target == 'x86_64-unknown-linux-musl' uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 with: image: alpine:3.12 options: -v ${{ github.workspace }}:/io -w /io --env MODULE_NAME --env PACKAGE_NAME run: | apk add python3 python3 -m venv .venv .venv/bin/pip install --upgrade pip .venv/bin/pip install ${PACKAGE_NAME}-build --no-index --find-links crates/uv-build/dist --force-reinstall .venv/bin/${MODULE_NAME}-build --help .venv/bin/python -m ${MODULE_NAME}_build --help - name: "Upload wheels uv-build" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv_build-${{ matrix.target }} path: crates/uv-build/dist musllinux-cross: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: depot-ubuntu-22.04-8 strategy: matrix: platform: - target: aarch64-unknown-linux-musl arch: aarch64 maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16 - target: armv7-unknown-linux-musleabihf arch: armv7 fail-fast: false steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi # uv - name: "Build wheels" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.platform.target }} manylinux: musllinux_1_1 args: --release --locked --out dist --features self-update ${{ matrix.platform.arch == 'aarch64' && '--compatibility 2_17' || ''}} docker-options: ${{ matrix.platform.maturin_docker_options }} rust-toolchain: ${{ matrix.platform.toolchain || null }} - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 name: "Test wheel" with: arch: ${{ matrix.platform.arch }} distro: alpine_latest install: | apk add python3 run: | python -m venv .venv .venv/bin/pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall .venv/bin/${MODULE_NAME} --help # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. # .venv/bin/python -m ${MODULE_NAME} --help .venv/bin/uvx --help env: | PACKAGE_NAME: ${{ env.PACKAGE_NAME }} MODULE_NAME: ${{ env.MODULE_NAME }} - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 name: "Test wheel (manylinux)" if: matrix.platform.arch == 'aarch64' with: arch: aarch64 distro: ubuntu20.04 install: | apt-get update apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 pip3 install -U pip run: | pip install ${PACKAGE_NAME} --no-index --find-links dist/ --force-reinstall ${MODULE_NAME} --help # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. # python -m ${MODULE_NAME} --help uvx --help env: | PACKAGE_NAME: ${{ env.PACKAGE_NAME }} MODULE_NAME: ${{ env.MODULE_NAME }} - name: "Upload wheels" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv-${{ matrix.platform.target }} path: dist - name: "Archive binary" shell: bash run: | ARCHIVE_NAME=uv-$TARGET ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz mkdir -p $ARCHIVE_NAME cp target/$TARGET/$PROFILE/uv $ARCHIVE_NAME/uv cp target/$TARGET/$PROFILE/uvx $ARCHIVE_NAME/uvx tar czvf $ARCHIVE_FILE $ARCHIVE_NAME shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 env: TARGET: ${{ matrix.platform.target }} PROFILE: ${{ matrix.platform.arch == 'ppc64le' && 'release-no-lto' || 'release' }} - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: artifacts-${{ matrix.platform.target }} path: | *.tar.gz *.sha256 # uv-build - name: "Build wheels" uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: maturin-version: v1.9.6 target: ${{ matrix.platform.target }} manylinux: musllinux_1_1 args: --profile minimal-size --locked ${{ matrix.platform.arch == 'aarch64' && '--compatibility 2_17' || ''}} --out crates/uv-build/dist -m crates/uv-build/Cargo.toml docker-options: ${{ matrix.platform.maturin_docker_options }} rust-toolchain: ${{ matrix.platform.toolchain || null }} - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 name: "Test wheel" with: arch: ${{ matrix.platform.arch }} distro: alpine_latest install: | apk add python3 run: | python -m venv .venv .venv/bin/pip install ${PACKAGE_NAME}-build --no-index --find-links crates/uv-build/dist --force-reinstall .venv/bin/${MODULE_NAME}-build --help # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. # .venv/bin/python -m ${MODULE_NAME}_build --help env: | PACKAGE_NAME: ${{ env.PACKAGE_NAME }} MODULE_NAME: ${{ env.MODULE_NAME }} - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1 name: "Test wheel (manylinux)" if: matrix.platform.arch == 'aarch64' with: arch: aarch64 distro: ubuntu20.04 install: | apt-get update apt-get install -y --no-install-recommends python3 python3-pip python-is-python3 pip3 install -U pip run: | pip install ${PACKAGE_NAME}-build --no-index --find-links crates/uv-build/dist --force-reinstall ${MODULE_NAME}-build --help # TODO(konsti): Enable this test on all platforms, currently `find_uv_bin` is failing to discover uv here. # python -m ${MODULE_NAME}_build --help env: | PACKAGE_NAME: ${{ env.PACKAGE_NAME }} MODULE_NAME: ${{ env.MODULE_NAME }} - name: "Upload wheels" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wheels_uv_build-${{ matrix.platform.target }} path: crates/uv-build/dist uv-0.9.17+ds1/.github/workflows/build-docker.yml000066400000000000000000000444351520155276700214350ustar00rootroot00000000000000# Build and publish Docker images. # # Uses Depot for multi-platform builds. Includes both a `uv` base image, which # is just the binary in a scratch image, and a set of extra, common images with # the uv binary installed. # # Images are built on all runs. # # On release, assumed to run as a subworkflow of .github/workflows/release.yml; # specifically, as a local artifacts job within `cargo-dist`. In this case, # images are published based on the `plan`. # # TODO(charlie): Ideally, the publish step would happen as a publish job within # `cargo-dist`, but sharing the built image as an artifact between jobs is # challenging. name: "Docker images" on: workflow_call: inputs: plan: required: true type: string pull_request: paths: # We want to ensure that the maturin builds still work when we change # Project metadata - pyproject.toml - Cargo.toml - .cargo/config.toml # Toolchain or dependency versions - Cargo.lock - rust-toolchain.toml # The Dockerfile itself - Dockerfile # And the workflow itself - .github/workflows/build-docker.yml env: UV_GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/uv UV_DOCKERHUB_IMAGE: docker.io/astral/uv permissions: {} jobs: docker-plan: name: plan runs-on: ubuntu-latest outputs: login: ${{ steps.plan.outputs.login }} push: ${{ steps.plan.outputs.push }} tag: ${{ steps.plan.outputs.tag }} action: ${{ steps.plan.outputs.action }} steps: - name: Set push variable env: DRY_RUN: ${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }} TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag }} IS_LOCAL_PR: ${{ github.event.pull_request.head.repo.full_name == 'astral-sh/uv' }} id: plan run: | if [ "${DRY_RUN}" == "false" ]; then echo "login=true" >> "$GITHUB_OUTPUT" echo "push=true" >> "$GITHUB_OUTPUT" echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "action=build and publish" >> "$GITHUB_OUTPUT" else echo "login=${IS_LOCAL_PR}" >> "$GITHUB_OUTPUT" echo "push=false" >> "$GITHUB_OUTPUT" echo "tag=dry-run" >> "$GITHUB_OUTPUT" echo "action=build" >> "$GITHUB_OUTPUT" fi docker-publish-base: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} name: ${{ needs.docker-plan.outputs.action }} uv needs: - docker-plan runs-on: ubuntu-latest permissions: contents: read id-token: write # for Depot OIDC and GHCR signing packages: write # for GHCR image pushes attestations: write # for GHCR attestations environment: name: ${{ needs.docker-plan.outputs.push == 'true' && 'release' || '' }} outputs: image-tags: ${{ steps.meta.outputs.tags }} image-annotations: ${{ steps.meta.outputs.annotations }} image-digest: ${{ steps.build.outputs.digest }} image-version: ${{ steps.meta.outputs.version }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: recursive persist-credentials: false # Login to DockerHub (when not pushing, it's to avoid rate-limiting) - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 if: ${{ needs.docker-plan.outputs.login == 'true' }} with: username: ${{ needs.docker-plan.outputs.push == 'true' && 'astral' || 'astralshbot' }} password: ${{ needs.docker-plan.outputs.push == 'true' && secrets.DOCKERHUB_TOKEN_RW || secrets.DOCKERHUB_TOKEN_RO }} - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 - name: Check tag consistency if: ${{ needs.docker-plan.outputs.push == 'true' }} run: | version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g') if [ "${TAG}" != "${version}" ]; then echo "The input tag does not match the version from pyproject.toml:" >&2 echo "${TAG}" >&2 echo "${version}" >&2 exit 1 else echo "Releasing ${version}" fi env: TAG: ${{ needs.docker-plan.outputs.tag }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: index with: images: | ${{ env.UV_GHCR_IMAGE }} ${{ env.UV_DOCKERHUB_IMAGE }} # Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name tags: | type=raw,value=dry-run,enable=${{ needs.docker-plan.outputs.push == 'false' }} type=pep440,pattern={{ version }},value=${{ needs.docker-plan.outputs.tag }},enable=${{ needs.docker-plan.outputs.push }} type=pep440,pattern={{ major }}.{{ minor }},value=${{ needs.docker-plan.outputs.tag }},enable=${{ needs.docker-plan.outputs.push }} - name: Build and push by digest id: build uses: depot/build-push-action@9785b135c3c76c33db102e45be96a25ab55cd507 # v1.16.2 with: project: 7hd4vdzmw5 # astral-sh/uv context: . platforms: linux/amd64,linux/arm64 push: ${{ needs.docker-plan.outputs.push }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} # TODO(zanieb): Annotations are not supported by Depot yet and are ignored annotations: ${{ steps.meta.outputs.annotations }} - name: Generate artifact attestation for base image if: ${{ needs.docker-plan.outputs.push == 'true' }} uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: subject-name: ${{ env.UV_GHCR_IMAGE }} subject-digest: ${{ steps.build.outputs.digest }} docker-publish-extra: name: ${{ needs.docker-plan.outputs.action }} ${{ matrix.image-mapping }} runs-on: ubuntu-latest environment: name: ${{ needs.docker-plan.outputs.push == 'true' && 'release' || '' }} needs: - docker-plan - docker-publish-base permissions: id-token: write # for Depot OIDC and GHCR signing packages: write # for GHCR image pushes attestations: write # for GHCR attestations strategy: fail-fast: false matrix: # Mapping of base image followed by a comma followed by one or more base tags (comma separated) # Note, org.opencontainers.image.version label will use the first base tag (use the most specific tag first) image-mapping: - alpine:3.22,alpine3.22,alpine - alpine:3.21,alpine3.21 - debian:trixie-slim,trixie-slim,debian-slim - buildpack-deps:trixie,trixie,debian - debian:bookworm-slim,bookworm-slim - buildpack-deps:bookworm,bookworm - python:3.14-alpine,python3.14-alpine - python:3.13-alpine,python3.13-alpine - python:3.12-alpine,python3.12-alpine - python:3.11-alpine,python3.11-alpine - python:3.10-alpine,python3.10-alpine - python:3.9-alpine,python3.9-alpine - python:3.8-alpine,python3.8-alpine - python:3.14-trixie,python3.14-trixie - python:3.13-trixie,python3.13-trixie - python:3.12-trixie,python3.12-trixie - python:3.11-trixie,python3.11-trixie - python:3.10-trixie,python3.10-trixie - python:3.9-trixie,python3.9-trixie - python:3.14-slim-trixie,python3.14-trixie-slim - python:3.13-slim-trixie,python3.13-trixie-slim - python:3.12-slim-trixie,python3.12-trixie-slim - python:3.11-slim-trixie,python3.11-trixie-slim - python:3.10-slim-trixie,python3.10-trixie-slim - python:3.9-slim-trixie,python3.9-trixie-slim - python:3.14-bookworm,python3.14-bookworm - python:3.13-bookworm,python3.13-bookworm - python:3.12-bookworm,python3.12-bookworm - python:3.11-bookworm,python3.11-bookworm - python:3.10-bookworm,python3.10-bookworm - python:3.9-bookworm,python3.9-bookworm - python:3.8-bookworm,python3.8-bookworm - python:3.14-slim-bookworm,python3.14-bookworm-slim - python:3.13-slim-bookworm,python3.13-bookworm-slim - python:3.12-slim-bookworm,python3.12-bookworm-slim - python:3.11-slim-bookworm,python3.11-bookworm-slim - python:3.10-slim-bookworm,python3.10-bookworm-slim - python:3.9-slim-bookworm,python3.9-bookworm-slim - python:3.8-slim-bookworm,python3.8-bookworm-slim steps: # Login to DockerHub (when not pushing, it's to avoid rate-limiting) - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 if: ${{ needs.docker-plan.outputs.login == 'true' }} with: username: ${{ needs.docker-plan.outputs.push == 'true' && 'astral' || 'astralshbot' }} password: ${{ needs.docker-plan.outputs.push == 'true' && secrets.DOCKERHUB_TOKEN_RW || secrets.DOCKERHUB_TOKEN_RO }} - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 - name: Generate Dynamic Dockerfile Tags shell: bash run: | set -euo pipefail # Extract the image and tags from the matrix variable IFS=',' read -r BASE_IMAGE BASE_TAGS <<< "${{ matrix.image-mapping }}" # Generate Dockerfile content cat < Dockerfile FROM ${BASE_IMAGE} COPY --from=${UV_GHCR_IMAGE}:latest /uv /uvx /usr/local/bin/ ENV UV_TOOL_BIN_DIR="/usr/local/bin" ENTRYPOINT [] CMD ["/usr/local/bin/uv"] EOF # Initialize a variable to store all tag docker metadata patterns TAG_PATTERNS="" # Loop through all base tags and append its docker metadata pattern to the list # Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version IFS=','; for TAG in ${BASE_TAGS}; do TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${VERSION}\n" TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${VERSION}\n" TAG_PATTERNS="${TAG_PATTERNS}type=raw,value=${TAG}\n" done # Remove the trailing newline from the pattern list TAG_PATTERNS="${TAG_PATTERNS%\\n}" # Export tag patterns using the multiline env var syntax { echo "TAG_PATTERNS<> $GITHUB_ENV env: VERSION: ${{ needs.docker-plan.outputs.tag }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 # ghcr.io prefers index level annotations env: DOCKER_METADATA_ANNOTATIONS_LEVELS: index with: images: | ${{ env.UV_GHCR_IMAGE }} ${{ env.UV_DOCKERHUB_IMAGE }} flavor: | latest=false tags: | ${{ env.TAG_PATTERNS }} - name: Build and push id: build-and-push uses: depot/build-push-action@9785b135c3c76c33db102e45be96a25ab55cd507 # v1.16.2 with: context: . project: 7hd4vdzmw5 # astral-sh/uv platforms: linux/amd64,linux/arm64 push: ${{ needs.docker-plan.outputs.push }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} # TODO(zanieb): Annotations are not supported by Depot yet and are ignored annotations: ${{ steps.meta.outputs.annotations }} - name: Generate artifact attestation if: ${{ needs.docker-plan.outputs.push == 'true' }} uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: subject-name: ${{ env.UV_GHCR_IMAGE }} subject-digest: ${{ steps.build-and-push.outputs.digest }} # Push annotations manually. # See `docker-annotate-base` for details. - name: Add annotations to images if: ${{ needs.docker-plan.outputs.push == 'true' }} env: IMAGES: "${{ env.UV_GHCR_IMAGE }} ${{ env.UV_DOCKERHUB_IMAGE }}" DIGEST: ${{ steps.build-and-push.outputs.digest }} TAGS: ${{ steps.meta.outputs.tags }} ANNOTATIONS: ${{ steps.meta.outputs.annotations }} run: | set -x readarray -t lines <<< "$ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done for image in $IMAGES; do readarray -t lines < <(grep "^${image}:" <<< "$TAGS"); tags=(); for line in "${lines[@]}"; do tags+=(-t "$line"); done docker buildx imagetools create \ "${annotations[@]}" \ "${tags[@]}" \ "${image}@${DIGEST}" done # See `docker-annotate-base` for details. - name: Export manifest digest id: manifest-digest if: ${{ needs.docker-plan.outputs.push == 'true' }} env: IMAGE: ${{ env.UV_GHCR_IMAGE }} VERSION: ${{ steps.meta.outputs.version }} run: | digest="$( docker buildx imagetools inspect \ "${IMAGE}:${VERSION}" \ --format '{{json .Manifest}}' \ | jq -r '.digest' )" echo "digest=${digest}" >> "$GITHUB_OUTPUT" # See `docker-annotate-base` for details. - name: Generate artifact attestation if: ${{ needs.docker-plan.outputs.push == 'true' }} uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: subject-name: ${{ env.UV_GHCR_IMAGE }} subject-digest: ${{ steps.manifest-digest.outputs.digest }} # Annotate the base image docker-annotate-base: name: annotate uv runs-on: ubuntu-latest permissions: contents: read id-token: write # for GHCR signing packages: write # for GHCR image pushes attestations: write # for GHCR attestations environment: name: ${{ needs.docker-plan.outputs.push == 'true' && 'release' || '' }} needs: - docker-plan - docker-publish-base - docker-publish-extra if: ${{ needs.docker-plan.outputs.push == 'true' }} steps: - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: username: astral password: ${{ secrets.DOCKERHUB_TOKEN_RW }} - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} # Depot doesn't support annotating images, so we need to do so manually # afterwards. Mutating the manifest is desirable regardless, because we # want to bump the base image to appear at the top of the list on GHCR. # However, once annotation support is added to Depot, this step can be # minimized to just touch the GHCR manifest. - name: Add annotations to images env: IMAGES: "${{ env.UV_GHCR_IMAGE }} ${{ env.UV_DOCKERHUB_IMAGE }}" DIGEST: ${{ needs.docker-publish-base.outputs.image-digest }} TAGS: ${{ needs.docker-publish-base.outputs.image-tags }} ANNOTATIONS: ${{ needs.docker-publish-base.outputs.image-annotations }} # The readarray part is used to make sure the quoting and special characters are preserved on expansion (e.g. spaces) # The final command becomes `docker buildx imagetools create --annotation 'index:foo=1' --annotation 'index:bar=2' ... -t tag1 -t tag2 ... @sha256:` run: | set -x readarray -t lines <<< "$ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done for image in $IMAGES; do readarray -t lines < <(grep "^${image}:" <<< "$TAGS"); tags=(); for line in "${lines[@]}"; do tags+=(-t "$line"); done docker buildx imagetools create \ "${annotations[@]}" \ "${tags[@]}" \ "${image}@${DIGEST}" done # Now that we've modified the manifest, we need to attest it again. # Note we only generate an attestation for GHCR. - name: Export manifest digest id: manifest-digest env: IMAGE: ${{ env.UV_GHCR_IMAGE }} VERSION: ${{ needs.docker-publish-base.outputs.image-version }} # To sign the manifest, we need it's digest. Unfortunately "docker # buildx imagetools create" does not (yet) have a clean way of sharing # the digest of the manifest it creates (see docker/buildx#2407), so # we use a separate command to retrieve it. # imagetools inspect [TAG] --format '{{json .Manifest}}' gives us # the machine readable JSON description of the manifest, and the # jq command extracts the digest from this. The digest is then # sent to the Github step output file for sharing with other steps. run: | digest="$( docker buildx imagetools inspect \ "${IMAGE}:${VERSION}" \ --format '{{json .Manifest}}' \ | jq -r '.digest' )" echo "digest=${digest}" >> "$GITHUB_OUTPUT" - name: Generate artifact attestation uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: subject-name: ${{ env.UV_GHCR_IMAGE }} subject-digest: ${{ steps.manifest-digest.outputs.digest }} uv-0.9.17+ds1/.github/workflows/ci.yml000066400000000000000000003212341520155276700174570ustar00rootroot00000000000000name: CI on: push: branches: [main] pull_request: workflow_dispatch: permissions: {} concurrency: group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always PYTHON_VERSION: "3.12" RUSTUP_MAX_RETRIES: 10 RUST_BACKTRACE: 1 jobs: determine_changes: name: "Determine changes" runs-on: ubuntu-latest outputs: # Flag that is raised when any code is changed code: ${{ steps.changed.outputs.code_any_changed }} # Flag that is raised when uv.schema.json is changed (e.g., in a release PR) schema: ${{ steps.changed.outputs.schema_changed }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 persist-credentials: false - name: "Determine changed files" id: changed shell: bash run: | CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha || 'origin/main' }}...HEAD) CODE_CHANGED=false SCHEMA_CHANGED=false while IFS= read -r file; do # Check if the schema file changed (e.g., in a release PR) if [[ "${file}" == "uv.schema.json" ]]; then echo "Detected schema change: ${file}" SCHEMA_CHANGED=true fi if [[ "${file}" =~ ^docs/ ]]; then echo "Skipping ${file} (matches docs/ pattern)" continue fi if [[ "${file}" =~ ^mkdocs.*\.yml$ ]]; then echo "Skipping ${file} (matches mkdocs*.yml pattern)" continue fi if [[ "${file}" =~ \.md$ ]]; then echo "Skipping ${file} (matches *.md pattern)" continue fi if [[ "${file}" =~ ^bin/ ]]; then echo "Skipping ${file} (matches bin/ pattern)" continue fi if [[ "${file}" =~ ^assets/ ]]; then echo "Skipping ${file} (matches assets/ pattern)" continue fi echo "Detected code change in: ${file}" CODE_CHANGED=true break done <<< "${CHANGED_FILES}" echo "code_any_changed=${CODE_CHANGED}" >> "${GITHUB_OUTPUT}" echo "schema_changed=${SCHEMA_CHANGED}" >> "${GITHUB_OUTPUT}" lint: timeout-minutes: 10 name: "lint" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: 3.12 - name: "Install Rustfmt" run: rustup component add rustfmt - name: "Install uv" uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 with: version: "0.9.13" - name: "rustfmt" run: cargo fmt --all --check - name: "Prettier" run: | npx prettier --check "**/*.{json5,yaml,yml}" npx prettier --prose-wrap always --check "**/*.md" - name: "README check" run: python scripts/transform_readme.py --target pypi - name: "Python format" run: uvx ruff format --diff . - name: "Python lint" run: uvx ruff check . - name: "Python type check" run: uvx mypy - name: "Validate project metadata" run: uvx --from 'validate-pyproject[all,store]' validate-pyproject pyproject.toml - name: "Lint shell scripts" uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0 env: # renovate: datasource=github-tags depName=koalaman/shellcheck SHELLCHECK_VERSION: "v0.11.0" SHELLCHECK_OPTS: --shell bash with: version: ${{ env.SHELLCHECK_VERSION }} severity: style check_together: "yes" cargo-clippy: timeout-minutes: 10 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: ubuntu-latest name: "cargo clippy | ubuntu" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Check uv_build dependencies" uses: EmbarkStudios/cargo-deny-action@f2ba7abc2abebaf185c833c3961145a3c275caad # v2.0.13 with: command: check bans manifest-path: crates/uv-build/Cargo.toml - name: "Install Rust toolchain" run: rustup component add clippy - name: "Clippy" run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings cargo-clippy-windows: timeout-minutes: 15 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: windows-latest name: "cargo clippy | windows" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Dev Drive run: ${{ github.workspace }}/.github/workflows/setup-dev-drive.ps1 # actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone... - name: Copy Git Repo to Dev Drive run: | Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: workspaces: ${{ env.UV_WORKSPACE }} - name: "Install Rust toolchain" run: rustup component add clippy - name: "Clippy" working-directory: ${{ env.UV_WORKSPACE }} run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings cargo-publish-dry-run: timeout-minutes: 20 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: depot-ubuntu-22.04-8 name: "cargo publish dry-run" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "cargo publish dry-run" run: cargo publish --workspace --dry-run cargo-dev-generate-all: timeout-minutes: 10 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: ubuntu-latest name: "cargo dev generate-all" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Generate all" run: cargo dev generate-all --mode dry-run - name: "Check sysconfig mappings" run: cargo dev generate-sysconfig-metadata --mode check - name: "Check JSON schema" if: ${{ needs.determine_changes.outputs.schema == 'true' }} run: cargo dev generate-json-schema --mode check cargo-shear: timeout-minutes: 10 name: "cargo shear" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Install cargo shear" uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1 with: tool: cargo-shear - run: cargo shear # We use the large GitHub actions runners # For Ubuntu and Windows, this requires Organization-level configuration # See: https://docs.github.com/en/actions/using-github-hosted-runners/about-larger-runners/about-larger-runners#about-ubuntu-and-windows-larger-runners cargo-test-linux: timeout-minutes: 10 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: depot-ubuntu-22.04-16 name: "cargo test | ubuntu" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - name: "Install Rust toolchain" run: rustup show - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 with: version: "0.9.13" - name: "Install required Python versions" run: uv python install - name: "Install secret service" run: | sudo apt update -y sudo apt install -y gnome-keyring - name: "Start gnome-keyring" # run gnome-keyring with 'foobar' as password for the login keyring # this will create a new login keyring and unlock it # the login password doesn't matter, but the keyring must be unlocked for the tests to work run: gnome-keyring-daemon --components=secrets --daemonize --unlock <<< 'foobar' - name: "Install cargo nextest" uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1 with: tool: cargo-nextest - name: "Cargo test" env: # Retry more than default to reduce flakes in CI UV_HTTP_RETRIES: 5 run: | cargo nextest run \ --features python-patch,native-auth,secret-service \ --workspace \ --status-level skip --failure-output immediate-final --no-fail-fast -j 20 --final-status-level slow cargo-test-macos: timeout-minutes: 20 needs: determine_changes # Only run macOS tests on main without opt-in if: ${{ contains(github.event.pull_request.labels.*.name, 'test:macos') || github.ref == 'refs/heads/main' }} runs-on: depot-macos-14 name: "cargo test | macos" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - name: "Install Rust toolchain" run: rustup show - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 with: version: "0.9.13" - name: "Install required Python versions" run: uv python install - name: "Install cargo nextest" uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1 with: tool: cargo-nextest - name: "Cargo test" env: # Retry more than default to reduce flakes in CI UV_HTTP_RETRIES: 5 run: | cargo nextest run \ --no-default-features \ --features python,python-managed,pypi,git,git-lfs,performance,crates-io,native-auth,apple-native \ --workspace \ --status-level skip --failure-output immediate-final --no-fail-fast -j 12 --final-status-level slow cargo-test-windows: timeout-minutes: 15 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: depot-windows-2022-16 name: "cargo test | windows" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Dev Drive run: ${{ github.workspace }}/.github/workflows/setup-dev-drive.ps1 # actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone... - name: Copy Git Repo to Dev Drive run: | Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 with: version: "0.9.13" - name: "Install required Python versions" run: uv python install - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: workspaces: ${{ env.UV_WORKSPACE }} - name: "Install Rust toolchain" working-directory: ${{ env.UV_WORKSPACE }} run: rustup show - name: "Install cargo nextest" uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1 with: tool: cargo-nextest - name: "Cargo test" working-directory: ${{ env.UV_WORKSPACE }} env: # Retry more than default to reduce flakes in CI UV_HTTP_RETRIES: 5 # Avoid permission errors during concurrent tests # See https://github.com/astral-sh/uv/issues/6940 UV_LINK_MODE: copy shell: bash run: | cargo nextest run \ --no-default-features \ --features python,pypi,python-managed,native-auth,windows-native \ --workspace \ --status-level skip --failure-output immediate-final --no-fail-fast -j 20 --final-status-level slow # Separate jobs for the nightly crate windows-trampoline-check: timeout-minutes: 15 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: windows-latest name: "check windows trampoline | ${{ matrix.target-arch }}" strategy: fail-fast: false matrix: target-arch: ["x86_64", "i686", "aarch64"] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Dev Drive run: ${{ github.workspace }}/.github/workflows/setup-dev-drive.ps1 # actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone... - name: Copy Git Repo to Dev Drive run: | Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: workspaces: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline - name: "Install Rust toolchain" working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline run: | rustup target add ${{ matrix.target-arch }}-pc-windows-msvc rustup component add rust-src --target ${{ matrix.target-arch }}-pc-windows-msvc - name: "Install cargo-bloat" uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1 with: tool: cargo-bloat - name: "rustfmt" working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline run: cargo fmt --all --check - name: "Clippy" working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline run: cargo clippy --all-features --locked --target x86_64-pc-windows-msvc --tests -- -D warnings - name: "Bloat Check" working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline run: | $output = cargo bloat --release --target x86_64-pc-windows-msvc $filteredOutput = $output | Select-String -Pattern 'core::fmt::write|core::fmt::getcount' -NotMatch $containsPatterns = $filteredOutput | Select-String -Pattern 'core::fmt|std::panicking|std::backtrace_rs' if ($containsPatterns) { Exit 1 } else { Exit 0 } # Separate jobs for the nightly crate windows-trampoline-test: timeout-minutes: 10 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: ${{ matrix.runner }} name: "test windows trampoline | ${{ matrix.target-arch }}" strategy: fail-fast: false matrix: include: - { runner: windows-latest, target-arch: "x86_64" } - { runner: windows-latest, target-arch: "i686" } - { runner: windows-11-arm, target-arch: "aarch64" } steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Dev Drive run: ${{ github.workspace }}/.github/workflows/setup-dev-drive.ps1 # actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone... - name: Copy Git Repo to Dev Drive run: | Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: workspaces: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline - name: "Install Rust toolchain" working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline run: | rustup target add ${{ matrix.target-arch }}-pc-windows-msvc rustup component add rust-src --target ${{ matrix.target-arch }}-pc-windows-msvc - name: "Test committed binaries" working-directory: ${{ env.UV_WORKSPACE }} run: | rustup target add ${{ matrix.target-arch }}-pc-windows-msvc cargo test -p uv-trampoline-builder --target ${{ matrix.target-arch }}-pc-windows-msvc # Build and copy the new binaries - name: "Build" working-directory: ${{ env.UV_WORKSPACE }}/crates/uv-trampoline run: | cargo build --target ${{ matrix.target-arch }}-pc-windows-msvc cp target/${{ matrix.target-arch }}-pc-windows-msvc/debug/uv-trampoline-console.exe ../uv-trampoline-builder/trampolines/uv-trampoline-${{ matrix.target-arch }}-console.exe cp target/${{ matrix.target-arch }}-pc-windows-msvc/debug/uv-trampoline-gui.exe ../uv-trampoline-builder/trampolines/uv-trampoline-${{ matrix.target-arch }}-gui.exe - name: "Test new binaries" working-directory: ${{ env.UV_WORKSPACE }} run: | # We turn off the default "production" test feature since these are debug binaries cargo test -p uv-trampoline-builder --target ${{ matrix.target-arch }}-pc-windows-msvc --no-default-features typos: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: crate-ci/typos@64e4db431eb262bb5c6baa19dce280d78532830c # v1.37.3 docs: timeout-minutes: 10 name: "mkdocs" runs-on: ubuntu-latest env: MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 persist-credentials: false - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 with: version: "0.9.13" - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Generate reference documentation" run: | cargo dev generate-options-reference cargo dev generate-cli-reference cargo dev generate-env-vars-reference - name: "Add SSH key" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 with: ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} - name: "Build docs (public)" run: uvx --with-requirements docs/requirements.txt mkdocs build --strict -f mkdocs.public.yml - name: "Build docs (insiders)" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} run: uvx --with-requirements docs/requirements-insiders.txt mkdocs build --strict -f mkdocs.insiders.yml build-binary-linux-libc: timeout-minutes: 10 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: github-ubuntu-24.04-x86_64-8 name: "build binary | linux libc" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - name: "Build" run: cargo build - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: uv-linux-libc-${{ github.sha }} path: | ./target/debug/uv ./target/debug/uvx retention-days: 1 build-binary-linux-aarch64: timeout-minutes: 10 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: github-ubuntu-24.04-aarch64-4 name: "build binary | linux aarch64" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - name: "Build" run: cargo build - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: uv-linux-aarch64-${{ github.sha }} path: | ./target/debug/uv ./target/debug/uvx retention-days: 1 build-binary-linux-musl: timeout-minutes: 10 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: github-ubuntu-24.04-x86_64-8 name: "build binary | linux musl" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: "Setup musl" run: | sudo apt-get install musl-tools rustup target add x86_64-unknown-linux-musl - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - name: "Build" run: cargo build --target x86_64-unknown-linux-musl --bin uv --bin uvx - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: uv-linux-musl-${{ github.sha }} path: | ./target/x86_64-unknown-linux-musl/debug/uv ./target/x86_64-unknown-linux-musl/debug/uvx retention-days: 1 build-binary-macos-aarch64: timeout-minutes: 10 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: macos-14 # github-macos-14-aarch64-3 name: "build binary | macos aarch64" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - name: "Build" run: cargo build --bin uv --bin uvx - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: uv-macos-aarch64-${{ github.sha }} path: | ./target/debug/uv ./target/debug/uvx retention-days: 1 build-binary-macos-x86_64: timeout-minutes: 10 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: macos-latest-large # github-macos-14-x86_64-12 name: "build binary | macos x86_64" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - name: "Build" run: cargo build --bin uv --bin uvx - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: uv-macos-x86_64-${{ github.sha }} path: | ./target/debug/uv ./target/debug/uvx retention-days: 1 build-binary-windows-x86_64: needs: determine_changes timeout-minutes: 10 if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: windows-latest name: "build binary | windows x86_64" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Setup Dev Drive run: ${{ github.workspace }}/.github/workflows/setup-dev-drive.ps1 # actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone... - name: Copy Git Repo to Dev Drive run: | Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: workspaces: ${{ env.UV_WORKSPACE }} - name: "Build" working-directory: ${{ env.UV_WORKSPACE }} run: cargo build --bin uv --bin uvx - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: uv-windows-x86_64-${{ github.sha }} path: | ${{ env.UV_WORKSPACE }}/target/debug/uv.exe ${{ env.UV_WORKSPACE }}/target/debug/uvx.exe retention-days: 1 build-binary-windows-aarch64: needs: determine_changes timeout-minutes: 25 if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: labels: windows-latest name: "build binary | windows aarch64" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Create Dev Drive using ReFS run: ${{ github.workspace }}/.github/workflows/setup-dev-drive.ps1 # actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone... - name: Copy Git Repo to Dev Drive run: | Copy-Item -Path "${{ github.workspace }}" -Destination "$Env:UV_WORKSPACE" -Recurse - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: workspaces: ${{ env.UV_WORKSPACE }} - name: "Install cross target" run: rustup target add aarch64-pc-windows-msvc - name: "Build" working-directory: ${{ env.UV_WORKSPACE }} run: cargo build --target aarch64-pc-windows-msvc - name: "Upload binary" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: uv-windows-aarch64-${{ github.sha }} path: | ${{ env.UV_WORKSPACE }}/target/aarch64-pc-windows-msvc/debug/uv.exe ${{ env.UV_WORKSPACE }}/target/aarch64-pc-windows-msvc/debug/uvx.exe retention-days: 1 build-binary-msrv: name: "build binary | msrv" needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: github-ubuntu-24.04-x86_64-8 timeout-minutes: 10 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: SebRollen/toml-action@b1b3628f55fc3a28208d4203ada8b737e9687876 # v1.2.0 id: msrv with: file: "Cargo.toml" field: "workspace.package.rust-version" - name: "Install Rust toolchain" run: rustup default ${MSRV} env: MSRV: ${{ steps.msrv.outputs.value }} - name: "Install mold" uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - run: cargo +${MSRV} build env: MSRV: ${{ steps.msrv.outputs.value }} - run: ./target/debug/uv --version build-binary-freebsd: needs: determine_changes timeout-minutes: 10 if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} runs-on: ubuntu-latest name: "build binary | freebsd" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - name: "Cross build" run: | # Install cross from `freebsd-firecracker` wget -q -O cross https://github.com/acj/freebsd-firecracker/releases/download/v0.0.10/cross chmod +x cross mv cross /usr/local/bin/cross cross build --target x86_64-unknown-freebsd - name: Test in Firecracker VM uses: acj/freebsd-firecracker-action@a5a3fc1709c5b5368141a5699f10259aca3cd965 # v0.6.0 with: verbose: false checkout: false pre-run: | # The exclude `*` prevents examination of directories so we need to # include each parent directory of the binary include_path="$(mktemp)" cat < $include_path target target/x86_64-unknown-freebsd target/x86_64-unknown-freebsd/debug target/x86_64-unknown-freebsd/debug/uv EOF rsync -r -e "ssh" \ --relative \ --copy-links \ --include-from "$include_path" \ --exclude "*" \ . firecracker: run-in-vm: | mv target/x86_64-unknown-freebsd/debug/uv uv chmod +x uv ./uv --version ecosystem-test: timeout-minutes: 10 needs: build-binary-linux-libc name: "ecosystem test | ${{ matrix.repo }}" runs-on: ubuntu-latest strategy: matrix: include: - repo: "prefecthq/prefect" ref: "7f25bbdf45fc81cca6dc23fb6a7377d436b70c83" commands: - "uv venv" - "uv pip install -e '.[dev]'" python: "3.9" - repo: "pallets/flask" ref: "b78b5a210bde49e7e04b62a2a4f453ca10e0048c" commands: - "uv venv" - "uv pip install -r requirements/dev.txt" python: "3.12" - repo: "pydantic/pydantic-core" ref: "d03bf4a01ca3b378cc8590bd481f307e82115bc6" commands: - "uv sync --group all" - "uv lock --upgrade" python: "3.12" fail-fast: false steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: repository: ${{ matrix.repo }} ref: ${{ matrix.ref }} persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python }} - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Test" run: | echo '${{ toJSON(matrix.commands) }}' | jq -r '.[]' | while read cmd; do echo "+ $cmd" >&2 if [[ $cmd == uv* ]]; then ./$cmd else $cmd fi done smoke-test-linux: timeout-minutes: 10 needs: build-binary-linux-libc name: "smoke test | linux" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: | chmod +x ./uv chmod +x ./uvx - name: "Smoke test" run: | ./uv run scripts/smoke-test - name: "Test shell completions" run: | eval "$(./uv generate-shell-completion bash)" eval "$(./uvx --generate-shell-completion bash)" smoke-test-linux-aarch64: timeout-minutes: 10 needs: build-binary-linux-aarch64 name: "smoke test | linux aarch64" runs-on: github-ubuntu-24.04-aarch64-2 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-aarch64-${{ github.sha }} - name: "Prepare binary" run: | chmod +x ./uv chmod +x ./uvx - name: "Smoke test" run: | ./uv run scripts/smoke-test - name: "Test shell completions" run: | eval "$(./uv generate-shell-completion bash)" eval "$(./uvx --generate-shell-completion bash)" smoke-test-linux-musl: timeout-minutes: 10 needs: build-binary-linux-musl name: "check system | alpine" runs-on: ubuntu-latest container: alpine:latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-musl-${{ github.sha }} - name: "Prepare binary" run: | chmod +x ./uv chmod +x ./uvx - name: "Smoke test" run: | ./uv run scripts/smoke-test smoke-test-macos: timeout-minutes: 10 needs: build-binary-macos-x86_64 name: "smoke test | macos" runs-on: macos-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-macos-x86_64-${{ github.sha }} - name: "Prepare binary" run: | chmod +x ./uv chmod +x ./uvx - name: "Smoke test" run: | ./uv run scripts/smoke-test - name: "Test shell completions" run: | eval "$(./uv generate-shell-completion bash)" eval "$(./uvx --generate-shell-completion bash)" smoke-test-windows-x86_64: timeout-minutes: 10 needs: build-binary-windows-x86_64 name: "smoke test | windows x86_64" runs-on: windows-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-x86_64-${{ github.sha }} - name: "Smoke test" working-directory: ${{ env.UV_WORKSPACE }} run: | ./uv run scripts/smoke-test - name: "Test uv shell completions" working-directory: ${{ env.UV_WORKSPACE }} run: | (& ./uv generate-shell-completion powershell) | Out-String | Invoke-Expression - name: "Test uvx shell completions" working-directory: ${{ env.UV_WORKSPACE }} run: | (& ./uvx --generate-shell-completion powershell) | Out-String | Invoke-Expression smoke-test-windows-aarch64: timeout-minutes: 10 needs: build-binary-windows-aarch64 name: "smoke test | windows aarch64" runs-on: windows-11-arm steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-aarch64-${{ github.sha }} - name: "Smoke test" working-directory: ${{ env.UV_WORKSPACE }} run: | ./uv run scripts/smoke-test - name: "Test uv shell completions" working-directory: ${{ env.UV_WORKSPACE }} run: | (& ./uv generate-shell-completion powershell) | Out-String | Invoke-Expression - name: "Test uvx shell completions" working-directory: ${{ env.UV_WORKSPACE }} run: | (& ./uvx --generate-shell-completion powershell) | Out-String | Invoke-Expression integration-test-nushell: timeout-minutes: 10 needs: build-binary-linux-libc name: "integration test | activate nushell venv" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Install nushell env: # This token only needs read access to the GitHub repository nushell/nushell. # This token is used (via gh-cli) to avoid hitting GitHub REST API rate limits. GITHUB_TOKEN: ${{ github.token }} run: |- # get latest nushell tag name nu_latest=$(gh release list --repo nushell/nushell --limit 1 --exclude-pre-releases --exclude-drafts --json "tagName" --jq '.[0].tagName') # trim any trailing whitespace from output nu_tag=${nu_latest%%[[:space:]]*} # download binary for x86_64-unknown-linux-gnu target gh release download ${nu_tag} --repo nushell/nushell --pattern "nu-${nu_tag}-x86_64-unknown-linux-gnu.tar.gz" # extract nu binary from tar.gz tar -xf "nu-${nu_tag}-x86_64-unknown-linux-gnu.tar.gz" # make the binary executable chmod +x "./nu-${nu_tag}-x86_64-unknown-linux-gnu/nu" # add it to PATH echo "${{ github.workspace }}/nu-${nu_tag}-x86_64-unknown-linux-gnu" >> "${GITHUB_PATH}" - name: Download binary uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: Prepare binary run: chmod +x ./uv - name: Create venv # The python version is arbitrary for this test. # We only want to ensure the activation script behaves properly run: ./uv venv - name: Activate venv shell: nu {0} run: overlay use ${{ github.workspace }}/.venv/bin/activate.nu integration-test-conda: timeout-minutes: 10 needs: build-binary-linux-libc name: "integration test | conda on ubuntu" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: conda-incubator/setup-miniconda@835234971496cad1653abb28a638a281cf32541f # v3.2.0 with: miniconda-version: latest activate-environment: uv python-version: "3.12" - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: Conda info shell: bash -el {0} run: conda info - name: "Install a package" shell: bash -el {0} run: | echo "$CONDA_PREFIX" ./uv pip install anyio integration-test-deadsnakes-39-linux: timeout-minutes: 15 needs: build-binary-linux-libc name: "integration test | deadsnakes python3.9 on ubuntu" runs-on: ubuntu-latest steps: - name: "Install python3.9" run: | for i in {1..5}; do sudo add-apt-repository ppa:deadsnakes && break || { echo "Attempt $i failed, retrying in 10 seconds..."; sleep 10; } if [ $i -eq 5 ]; then echo "Failed to add repository after 5 attempts" exit 1 fi done sudo apt-get update sudo apt-get install python3.9 - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Check missing distutils" run: | ./uv venv -p 3.9 --python-preference only-system -v 2>&1 | tee log.txt || true # We should report that distutils is missing grep 'Python installation is missing `distutils`' log.txt - name: "Install distutils" run: | sudo apt-get install python3.9-distutils - name: "Create a virtualenv" run: | ./uv venv -p 3.9 --python-preference only-system -v - name: "Check version" run: | .venv/bin/python --version - name: "Check install" run: | ./uv pip install -v anyio integration-test-free-threaded-windows-x86_64: timeout-minutes: 10 needs: build-binary-windows-x86_64 name: "integration test | free-threaded on windows" runs-on: windows-latest steps: - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-x86_64-${{ github.sha }} - name: "Install free-threaded Python via uv" run: | ./uv python install -v 3.13t - name: "Create a virtual environment (stdlib)" run: | & (./uv python find 3.13t) -m venv .venv - name: "Check version (stdlib)" run: | .venv/Scripts/python --version - name: "Create a virtual environment (uv)" run: | ./uv venv -c -p 3.13t --managed-python - name: "Check version (uv)" run: | .venv/Scripts/python --version - name: "Check is free-threaded" run: | .venv/Scripts/python -c "import sys; exit(1) if sys._is_gil_enabled() else exit(0)" - name: "Check install" run: | ./uv pip install -v anyio - name: "Check uv run" run: | ./uv run python -c "" ./uv run -p 3.13t python -c "" integration-test-windows-aarch64-implicit: timeout-minutes: 10 needs: build-binary-windows-aarch64 name: "integration test | aarch64 windows implicit" runs-on: windows-11-arm steps: - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-aarch64-${{ github.sha }} - name: "Install Python via uv (implicitly select x64)" run: | ./uv python install -v 3.13 - name: "Create a virtual environment (stdlib)" run: | & (./uv python find 3.13) -m venv .venv - name: "Check version (stdlib)" run: | .venv/Scripts/python --version - name: "Create a virtual environment (uv)" run: | ./uv venv -c -p 3.13 --managed-python - name: "Check version (uv)" run: | .venv/Scripts/python --version - name: "Check is x64" run: | .venv/Scripts/python -c "import sys; exit(1) if 'AMD64' not in sys.version else exit(0)" - name: "Check install" run: | ./uv pip install -v anyio - name: "Check uv run" run: | ./uv run python -c "" ./uv run -p 3.13 python -c "" integration-test-windows-aarch64-explicit: timeout-minutes: 10 needs: build-binary-windows-aarch64 name: "integration test | aarch64 windows explicit" runs-on: windows-11-arm steps: - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-aarch64-${{ github.sha }} - name: "Install Python via uv (explicitly select aarch64)" run: | ./uv python install -v cpython-3.13-windows-aarch64-none - name: "Create a virtual environment (stdlib)" run: | & (./uv python find 3.13) -m venv .venv - name: "Check version (stdlib)" run: | .venv/Scripts/python --version - name: "Create a virtual environment (uv)" run: | ./uv venv -c -p 3.13 --managed-python - name: "Check version (uv)" run: | .venv/Scripts/python --version - name: "Check is NOT x64" run: | .venv/Scripts/python -c "import sys; exit(1) if 'AMD64' in sys.version else exit(0)" - name: "Check install" run: | ./uv pip install -v anyio - name: "Check uv run" run: | ./uv run python -c "" ./uv run -p 3.13 python -c "" integration-test-windows-python-install-manager: timeout-minutes: 10 needs: build-binary-windows-x86_64 name: "integration test | windows python install manager" runs-on: windows-latest steps: - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-x86_64-${{ github.sha }} - name: "Install Python via Python Install manager" run: | # https://www.python.org/downloads/release/pymanager-250/ winget install --accept-package-agreements --accept-source-agreements 9NQ7512CXL7T # Call Python Install Manager's py.exe by full path to avoid legacy py.exe & "$env:LOCALAPPDATA\Microsoft\WindowsApps\py.exe" install 3.14 # https://github.com/astral-sh/uv/issues/16204 - name: "Check temporary environment creation" run: | ./uv run -p $env:LOCALAPPDATA\Python\pythoncore-3.14-64\python.exe --with numpy python -c "import sys; print(sys.executable)" integration-test-pypy-linux: timeout-minutes: 10 needs: build-binary-linux-libc name: "integration test | pypy on ubuntu" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Install PyPy" run: ./uv python install -v pypy3.9 - name: "Create a virtual environment" run: | ./uv venv -p pypy3.9 --managed-python - name: "Check for executables" run: | check_in_bin() { local executable_name=$1 local bin_path=".venv/bin" if [[ -x "$bin_path/$executable_name" ]]; then return 0 else echo "Executable '$executable_name' not found in folder '$bin_path'." return 1 fi } executables=("pypy" "pypy3" "python") all_found=true for executable_name in "${executables[@]}"; do check_in_bin "$executable_name" "$folder_path" result=$? if [[ $result -ne 0 ]]; then all_found=false fi done if ! $all_found; then echo "One or more expected executables were not found." exit 1 fi - name: "Check version" run: | .venv/bin/pypy --version .venv/bin/pypy3 --version .venv/bin/python --version - name: "Check install" run: | ./uv pip install anyio integration-test-pypy-windows-x86_64: timeout-minutes: 10 needs: build-binary-windows-x86_64 name: "integration test | pypy on windows" runs-on: windows-latest steps: - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-x86_64-${{ github.sha }} - name: "Install PyPy" run: .\uv.exe python install pypy3.9 - name: "Create a virtual environment" run: | .\uv.exe venv -p pypy3.9 --managed-python - name: "Check for executables" shell: python run: | import sys from pathlib import Path def binary_exist(binary): binaries_path = Path(".venv\\Scripts") if (binaries_path / binary).exists(): return True print(f"Executable '{binary}' not found in folder '{binaries_path}'.") all_found = True expected_binaries = [ "pypy3.9.exe", "pypy3.9w.exe", "pypy3.exe", "pypyw.exe", "python.exe", "python3.9.exe", "python3.exe", "pythonw.exe", ] for binary in expected_binaries: if not binary_exist(binary): all_found = False if not all_found: print("One or more expected executables were not found.") sys.exit(1) - name: "Check version" run: | & .venv\Scripts\pypy3.9.exe --version & .venv\Scripts\pypy3.exe --version & .venv\Scripts\python.exe --version - name: "Check install" run: | .\uv.exe pip install anyio integration-test-graalpy-linux: timeout-minutes: 10 needs: build-binary-linux-libc name: "integration test | graalpy on ubuntu" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Install GraalPy" run: ./uv python install -v graalpy - name: "Create a virtual environment" run: | ./uv venv -p graalpy --managed-python - name: "Check for executables" run: | check_in_bin() { local executable_name=$1 local bin_path=".venv/bin" if [[ -x "$bin_path/$executable_name" ]]; then return 0 else echo "Executable '$executable_name' not found in folder '$bin_path'." return 1 fi } executables=("graalpy" "python3" "python") all_found=true for executable_name in "${executables[@]}"; do check_in_bin "$executable_name" "$folder_path" result=$? if [[ $result -ne 0 ]]; then all_found=false fi done if ! $all_found; then echo "One or more expected executables were not found." exit 1 fi - name: "Check version" run: | .venv/bin/graalpy --version .venv/bin/python3 --version .venv/bin/python --version - name: "Check install" run: | ./uv pip install anyio - name: "Check a GraalPy dev version (different version parsing)" run: | curl -sLf https://github.com/graalvm/graal-languages-ea-builds/releases/download/graalpy-25.0.0-ea.31/graalpy-25.0.0-ea.31-linux-amd64.tar.gz | tar xz ./uv run -p ./graalpy-25.0.0-dev-linux-amd64/bin/python python --version integration-test-graalpy-windows-x86_64: timeout-minutes: 10 needs: build-binary-windows-x86_64 name: "integration test | graalpy on windows" runs-on: windows-latest steps: - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-x86_64-${{ github.sha }} - name: "Install GraalPy" run: .\uv.exe python install graalpy - name: "Create a virtual environment" run: | .\uv.exe venv -p graalpy --managed-python - name: "Check for executables" shell: python run: | import sys from pathlib import Path def binary_exist(binary): binaries_path = Path(".venv\\Scripts") if (binaries_path / binary).exists(): return True print(f"Executable '{binary}' not found in folder '{binaries_path}'.") all_found = True expected_binaries = [ "graalpy.exe", "python.exe", "python3.exe", ] for binary in expected_binaries: if not binary_exist(binary): all_found = False if not all_found: print("One or more expected executables were not found.") sys.exit(1) - name: "Check version" run: | & .venv\Scripts\graalpy.exe --version & .venv\Scripts\python3.exe --version & .venv\Scripts\python.exe --version - name: "Check install" run: | .\uv.exe pip install anyio integration-test-pyodide-linux: timeout-minutes: 10 needs: build-binary-linux-libc name: "integration test | pyodide on ubuntu" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Create a native virtual environment" run: | ./uv venv venv-native -p 3.12 # We use features added in 0.30.3 but there is no known breakage in # newer versions. ./uv pip install -p venv-native/bin/python pyodide-build==0.30.7 pip - name: "Install Pyodide interpreter" run: | source ./venv-native/bin/activate pyodide xbuildenv install 0.27.5 PYODIDE_PYTHON=$(pyodide config get interpreter) PYODIDE_INDEX=$(pyodide config get package_index) echo "PYODIDE_PYTHON=$PYODIDE_PYTHON" >> $GITHUB_ENV echo "PYODIDE_INDEX=$PYODIDE_INDEX" >> $GITHUB_ENV - name: "Create Pyodide virtual environment" run: | ./uv venv -p $PYODIDE_PYTHON venv-pyodide source ./venv-pyodide/bin/activate ./uv pip install --extra-index-url=$PYODIDE_INDEX --no-build numpy python -c 'import numpy' - name: "Install Pyodide with uv python" run: | ./uv python install cpython-3.13.2-emscripten-wasm32-musl - name: "Create a Pyodide virtual environment using uv installed Python" run: | ./uv venv -p cpython-3.13.2-emscripten-wasm32-musl venv-pyodide2 # TODO: be able to install Emscripten wheels here... source ./venv-pyodide2/bin/activate ./uv pip install packaging python -c 'import packaging' integration-test-github-actions: timeout-minutes: 10 needs: build-binary-linux-libc name: "integration test | github actions" runs-on: ubuntu-latest steps: - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12.7" - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Install a package without system opt-in" run: | ./uv pip install anyio && exit 1 || echo "Failed as expected" - name: "Install a package with system opt-in" run: | ./uv pip install anyio --system - name: Configure uv to use the system Python by default run: echo "UV_SYSTEM_PYTHON=1" >> $GITHUB_ENV - name: "Install a package with system opt-in via the environment" run: | ./uv pip install anyio --reinstall - name: "Create a project" run: | # Use Python 3.11 as the minimum required version ./uv init --python 3.11 ./uv add anyio - name: "Sync to the system Python" run: ./uv sync -v --python 3.12 env: UV_PROJECT_ENVIRONMENT: "/opt/hostedtoolcache/Python/3.12.7/x64" - name: "Attempt to sync to the system Python with an incompatible version" run: | ./uv sync -v --python 3.11 && { echo "ci: Error; should not succeed"; exit 1; } || { echo "ci: Ok; expected failure"; exit 0; } env: UV_PROJECT_ENVIRONMENT: "/opt/hostedtoolcache/Python/3.12.7/x64" - name: "Attempt to sync to a non-Python environment directory" run: | mkdir -p /home/runner/example touch /home/runner/example/some-file ./uv sync -v && { echo "ci: Error; should not succeed"; exit 1; } || { echo "ci: Ok; expected failure"; exit 0; } env: UV_PROJECT_ENVIRONMENT: "/home/runner/example" integration-test-github-actions-freethreaded: timeout-minutes: 10 needs: build-binary-linux-libc name: "integration test | free-threaded python on github actions" runs-on: ubuntu-latest steps: - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13t" - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Install a package without system opt-in" run: | ./uv pip install anyio && exit 1 || echo "Failed as expected" - name: "Install a package with system opt-in but without free-threaded opt-in" run: | ./uv pip install anyio --system --python 3.13 || echo "Failed as expected" # (we need to request 3.13 or we'll discover 3.12 on the system) - name: "Install a package with system and free-threaded opt-in" run: | ./uv pip install anyio --system --python 3.13t - name: "Create a virtual environment" run: | ./uv venv -p 3.13t --python-preference only-system - name: "Check is free-threaded" run: | .venv/bin/python -c "import sys; exit(1) if sys._is_gil_enabled() else exit(0)" integration-test-wsl: timeout-minutes: 15 needs: build-binary-linux-musl name: "integration test | pyenv on wsl x86-64" runs-on: windows-latest if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event.pull_request.head.repo.fork != true }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-musl-${{ github.sha }} - name: "Setup WSL" uses: Vampire/setup-wsl@6a8db447be7ed35f2f499c02c6e60ff77ef11278 # v6.0.0 with: distribution: Ubuntu-22.04 - name: "Install pyenv-win" shell: pwsh run: | Write-Host "Installing pyenv-win..." Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1" -OutFile "./install-pyenv-win.ps1" .\install-pyenv-win.ps1 # Add pyenv-win to PATH for this session $env:PYENV = "$env:USERPROFILE\.pyenv\pyenv-win" $env:PATH = "$env:PYENV\bin;$env:PYENV\shims;$env:PATH" # Add to GITHUB_PATH so WSL can find the shims echo "$env:PYENV\bin" | Out-File -FilePath $env:GITHUB_PATH -Append echo "$env:PYENV\shims" | Out-File -FilePath $env:GITHUB_PATH -Append Write-Host "Installing Python 3.11.9 via pyenv-win..." & "$env:PYENV\bin\pyenv.bat" install 3.11.9 & "$env:PYENV\bin\pyenv.bat" global 3.11.9 Write-Host "Verifying pyenv-win installation..." & "$env:PYENV\bin\pyenv.bat" versions - name: "Test uv" shell: wsl-bash {0} run: | set -x chmod +x ./uv # Check that we don't fail on `pyenv-win` shims ./uv python list -v integration-test-publish-changed: timeout-minutes: 10 needs: build-binary-linux-libc name: "integration test | determine publish changes" runs-on: ubuntu-latest outputs: # Flag that is raised when any code is changed code: ${{ steps.changed.outputs.code_any_changed }} # Only the main repository is a trusted publisher if: github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 persist-credentials: false # Only publish a new release if the publishing code changed - name: "Determine changed files" id: changed shell: bash run: | CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha || 'origin/main' }}...HEAD) CODE_CHANGED=false while IFS= read -r file; do if [[ "${file}" =~ ^crates/uv-publish/ || "${file}" =~ ^scripts/publish/ || "${file}" == ".github/workflows/ci.yml" ]]; then echo "Detected code change in: ${file}" CODE_CHANGED=true break fi echo "Skipping ${file} (not in watched paths)" continue done <<< "${CHANGED_FILES}" echo "code_any_changed=${CODE_CHANGED}" >> "${GITHUB_OUTPUT}" integration-test-registries: timeout-minutes: 10 needs: build-binary-linux-libc name: "integration test | registries" runs-on: ubuntu-latest if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event.pull_request.head.repo.fork != true }} environment: uv-test-registries env: PYTHON_VERSION: 3.12 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "${{ env.PYTHON_VERSION }}" - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Configure AWS credentials" uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-east-1 - name: "Get AWS CodeArtifact token" run: | UV_TEST_AWS_TOKEN=$(aws codeartifact get-authorization-token \ --domain tests \ --domain-owner ${{ secrets.AWS_ACCOUNT_ID }} \ --region us-east-1 \ --query authorizationToken \ --output text) echo "::add-mask::$UV_TEST_AWS_TOKEN" echo "UV_TEST_AWS_TOKEN=$UV_TEST_AWS_TOKEN" >> $GITHUB_ENV - name: "Authenticate with GCP" id: "auth" uses: "google-github-actions/auth@fc2174804b84f912b1f6d334e9463f484f1c552d" with: credentials_json: "${{ secrets.GCP_SERVICE_ACCOUNT_KEY }}" - name: "Set up GCP SDK" uses: "google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db" - name: "Get GCP Artifact Registry token" id: get_token run: | UV_TEST_GCP_TOKEN=$(gcloud auth print-access-token) echo "::add-mask::$UV_TEST_GCP_TOKEN" echo "UV_TEST_GCP_TOKEN=$UV_TEST_GCP_TOKEN" >> $GITHUB_ENV - name: "Run registry tests with environment variable backend" run: ./uv run -p "${PYTHON_VERSION}" scripts/registries-test.py --uv ./uv --color always --all --auth-method env env: RUST_LOG: uv=debug UV_TEST_ARTIFACTORY_TOKEN: ${{ secrets.UV_TEST_ARTIFACTORY_TOKEN }} UV_TEST_ARTIFACTORY_URL: ${{ secrets.UV_TEST_ARTIFACTORY_URL }} UV_TEST_ARTIFACTORY_USERNAME: ${{ secrets.UV_TEST_ARTIFACTORY_USERNAME }} UV_TEST_AWS_URL: ${{ secrets.UV_TEST_AWS_URL }} UV_TEST_AWS_USERNAME: aws UV_TEST_AZURE_TOKEN: ${{ secrets.UV_TEST_AZURE_TOKEN }} UV_TEST_AZURE_URL: ${{ secrets.UV_TEST_AZURE_URL }} UV_TEST_AZURE_USERNAME: dummy UV_TEST_CLOUDSMITH_TOKEN: ${{ secrets.UV_TEST_CLOUDSMITH_TOKEN }} UV_TEST_CLOUDSMITH_URL: ${{ secrets.UV_TEST_CLOUDSMITH_URL }} UV_TEST_CLOUDSMITH_USERNAME: ${{ secrets.UV_TEST_CLOUDSMITH_USERNAME }} UV_TEST_GCP_URL: ${{ secrets.UV_TEST_GCP_URL }} UV_TEST_GCP_USERNAME: oauth2accesstoken UV_TEST_GEMFURY_TOKEN: ${{ secrets.UV_TEST_GEMFURY_TOKEN }} UV_TEST_GEMFURY_URL: ${{ secrets.UV_TEST_GEMFURY_URL }} UV_TEST_GEMFURY_USERNAME: ${{ secrets.UV_TEST_GEMFURY_USERNAME }} UV_TEST_GITLAB_TOKEN: ${{ secrets.UV_TEST_GITLAB_TOKEN }} UV_TEST_GITLAB_URL: ${{ secrets.UV_TEST_GITLAB_URL }} UV_TEST_GITLAB_USERNAME: token - name: "Run registry tests with text store backend" run: ./uv run -p "${PYTHON_VERSION}" scripts/registries-test.py --uv ./uv --color always --all --auth-method text-store env: RUST_LOG: uv=debug UV_TEST_ARTIFACTORY_TOKEN: ${{ secrets.UV_TEST_ARTIFACTORY_TOKEN }} UV_TEST_ARTIFACTORY_URL: ${{ secrets.UV_TEST_ARTIFACTORY_URL }} UV_TEST_ARTIFACTORY_USERNAME: ${{ secrets.UV_TEST_ARTIFACTORY_USERNAME }} UV_TEST_AWS_URL: ${{ secrets.UV_TEST_AWS_URL }} UV_TEST_AWS_USERNAME: aws UV_TEST_AZURE_TOKEN: ${{ secrets.UV_TEST_AZURE_TOKEN }} UV_TEST_AZURE_URL: ${{ secrets.UV_TEST_AZURE_URL }} UV_TEST_AZURE_USERNAME: dummy UV_TEST_CLOUDSMITH_TOKEN: ${{ secrets.UV_TEST_CLOUDSMITH_TOKEN }} UV_TEST_CLOUDSMITH_URL: ${{ secrets.UV_TEST_CLOUDSMITH_URL }} UV_TEST_CLOUDSMITH_USERNAME: ${{ secrets.UV_TEST_CLOUDSMITH_USERNAME }} UV_TEST_GCP_URL: ${{ secrets.UV_TEST_GCP_URL }} UV_TEST_GCP_USERNAME: oauth2accesstoken UV_TEST_GEMFURY_TOKEN: ${{ secrets.UV_TEST_GEMFURY_TOKEN }} UV_TEST_GEMFURY_URL: ${{ secrets.UV_TEST_GEMFURY_URL }} UV_TEST_GEMFURY_USERNAME: ${{ secrets.UV_TEST_GEMFURY_USERNAME }} UV_TEST_GITLAB_TOKEN: ${{ secrets.UV_TEST_GITLAB_TOKEN }} UV_TEST_GITLAB_URL: ${{ secrets.UV_TEST_GITLAB_URL }} UV_TEST_GITLAB_USERNAME: token integration-test-publish: timeout-minutes: 20 needs: integration-test-publish-changed name: "integration test | uv publish" runs-on: ubuntu-latest if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event.pull_request.head.repo.fork != true && (needs.integration-test-publish-changed.outputs.code == 'true' || github.ref == 'refs/heads/main') }} environment: uv-test-publish env: # No dbus in GitHub Actions PYTHON_KEYRING_BACKEND: keyrings.alt.file.PlaintextKeyring PYTHON_VERSION: 3.12 permissions: # For trusted publishing id-token: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "${{ env.PYTHON_VERSION }}" - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Build astral-test-pypa-gh-action" run: | # Build a yet unused version of `astral-test-pypa-gh-action` mkdir astral-test-pypa-gh-action cd astral-test-pypa-gh-action ../uv init --package # Get the latest patch version patch_version=$(curl https://test.pypi.org/simple/astral-test-pypa-gh-action/?format=application/vnd.pypi.simple.v1+json | jq --raw-output '.files[-1].filename' | sed 's/astral_test_pypa_gh_action-0\.1\.\([0-9]\+\)\.tar\.gz/\1/') # Set the current version to one higher (which should be unused) sed -i "s/0.1.0/0.1.$((patch_version + 1))/g" pyproject.toml ../uv build - name: "Publish astral-test-pypa-gh-action" uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: # With this GitHub action, we can't do as rigid checks as with our custom Python script, so we publish more # leniently skip-existing: "true" verbose: "true" repository-url: "https://test.pypi.org/legacy/" packages-dir: "astral-test-pypa-gh-action/dist" - name: "Add password to keyring" run: | # `keyrings.alt` contains the plaintext keyring ./uv tool install --with keyrings.alt keyring echo $UV_TEST_PUBLISH_KEYRING | keyring set https://test.pypi.org/legacy/?astral-test-keyring __token__ env: UV_TEST_PUBLISH_KEYRING: ${{ secrets.UV_TEST_PUBLISH_KEYRING }} - name: "Add password to uv text store" run: | ./uv auth login https://test.pypi.org/legacy/?astral-test-text-store --token ${UV_TEST_PUBLISH_TEXT_STORE} env: UV_TEST_PUBLISH_TEXT_STORE: ${{ secrets.UV_TEST_PUBLISH_TEXT_STORE }} - name: "Publish test packages" # `-p 3.12` prefers the python we just installed over the one locked in `.python_version`. run: ./uv run -p "${PYTHON_VERSION}" scripts/publish/test_publish.py --uv ./uv all env: RUST_LOG: uv=debug,uv_publish=trace UV_TEST_PUBLISH_TOKEN: ${{ secrets.UV_TEST_PUBLISH_TOKEN }} UV_TEST_PUBLISH_PASSWORD: ${{ secrets.UV_TEST_PUBLISH_PASSWORD }} UV_TEST_PUBLISH_GITLAB_PAT: ${{ secrets.UV_TEST_PUBLISH_GITLAB_PAT }} UV_TEST_PUBLISH_CODEBERG_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CODEBERG_TOKEN }} UV_TEST_PUBLISH_CLOUDSMITH_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CLOUDSMITH_TOKEN }} UV_TEST_PUBLISH_PYX_TOKEN: ${{ secrets.UV_TEST_PUBLISH_PYX_TOKEN }} UV_TEST_PUBLISH_PYTHON_VERSION: ${{ env.PYTHON_VERSION }} integration-uv-build-backend: timeout-minutes: 10 needs: build-binary-linux-libc name: "integration test | uv_build" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "${{ env.PYTHON_VERSION }}" - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: | chmod +x ./uv chmod +x ./uvx - name: "Test uv_build package" run: | # Build the Python package, which is not covered by uv's integration tests since they can't depend on having # a Python package (only the binary itself is built before running Rust's tests) ./uv build -v crates/uv-build # Test the main path (`build_wheel`) through pip ./uv venv -v --seed ./uv run --no-project python -m pip install -v test/packages/built-by-uv --find-links crates/uv-build/dist --no-index --no-deps ./uv run --no-project python -c "from built_by_uv import greet; print(greet())" # Test both `build_wheel` and `build_sdist` through uv ./uv venv -c -v ./uv build -v --force-pep517 test/packages/built-by-uv --find-links crates/uv-build/dist --offline ./uv pip install -v test/packages/built-by-uv/dist/*.tar.gz --find-links crates/uv-build/dist --offline --no-deps ./uv run --no-project python -c "from built_by_uv import greet; print(greet())" # Test both `build_wheel` and `build_sdist` through the official `build` rm -rf test/packages/built-by-uv/dist/ ./uv venv -c -v ./uv pip install build # Add the uv binary to PATH for `build` to find PATH="$(pwd):$PATH" UV_OFFLINE=1 UV_FIND_LINKS=crates/uv-build/dist ./uv run --no-project python -m build -v --installer uv test/packages/built-by-uv ./uv pip install -v test/packages/built-by-uv/dist/*.tar.gz --find-links crates/uv-build/dist --offline --no-deps ./uv run --no-project python -c "from built_by_uv import greet; print(greet())" cache-test-ubuntu: timeout-minutes: 10 needs: build-binary-linux-libc name: "check cache | ubuntu" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Download binary for last version" run: curl -LsSf "https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-unknown-linux-gnu.tar.gz" | tar -xvz - name: "Check cache compatibility" run: python scripts/check_cache_compat.py --uv-current ./uv --uv-previous ./uv-x86_64-unknown-linux-gnu/uv cache-test-macos-aarch64: timeout-minutes: 10 needs: build-binary-macos-aarch64 name: "check cache | macos aarch64" runs-on: macos-14 # github-macos-14-aarch64-3 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-macos-aarch64-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Download binary for last version" run: curl -LsSf "https://github.com/astral-sh/uv/releases/latest/download/uv-aarch64-apple-darwin.tar.gz" | tar -xvz - name: "Check cache compatibility" run: python scripts/check_cache_compat.py --uv-current ./uv --uv-previous ./uv-aarch64-apple-darwin/uv system-test-debian: timeout-minutes: 10 needs: build-binary-linux-musl name: "check system | python on debian" runs-on: ubuntu-latest container: debian:bookworm steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Install Python" run: apt-get update && apt-get install -y python3.11 python3-pip python3.11-venv python3-debian - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-musl-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which python3.11) - name: "Validate global Python install" run: python3.11 scripts/check_system_python.py --uv ./uv --externally-managed - name: "Test `uv run` with system Python" run: | ./uv run -p python3.11 -v python -c "import debian" ./uv run -p python3.11 -v --with anyio python -c "import debian" system-test-fedora: timeout-minutes: 10 needs: build-binary-linux-libc name: "check system | python on fedora" runs-on: ubuntu-latest container: fedora:43 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Install Python" run: dnf install python3 which -y && python3 -m ensurepip - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which python3) - name: "Validate global Python install" run: python3 scripts/check_system_python.py --uv ./uv system-test-ubuntu: timeout-minutes: 10 needs: build-binary-linux-libc name: "check system | python on ubuntu" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which python) - name: "Validate global Python install" run: python scripts/check_system_python.py --uv ./uv # Currently failing, see https://github.com/astral-sh/uv/issues/13811 # system-test-opensuse: # timeout-minutes: 5 # needs: build-binary-linux-libc # name: "check system | python on opensuse" # runs-on: ubuntu-latest # container: opensuse/tumbleweed # steps: # - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # - name: "Install Python" # run: > # until # zypper install -y python310 which && python3.10 -m ensurepip && mv /usr/bin/python3.10 /usr/bin/python3; # do sleep 10; # done # # We retry because `zypper` can fail during remote repository updates # # The above will not sleep forever due to the job level timeout # - name: "Download binary" # uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 # with: # name: uv-linux-libc-${{ github.sha }} # - name: "Prepare binary" # run: chmod +x ./uv # - name: "Print Python path" # run: echo $(which python3) # - name: "Validate global Python install" # run: python3 scripts/check_system_python.py --uv ./uv # Note: rockylinux is a 1-1 code compatible distro to rhel # rockylinux mimics centos but with added maintenance stability # and avoids issues with centos stream uptime concerns system-test-rocky-linux: timeout-minutes: 10 needs: build-binary-linux-musl name: "check system | python on rocky linux ${{ matrix.rocky-version }}" runs-on: ubuntu-latest container: rockylinux/rockylinux:${{ matrix.rocky-version }} strategy: fail-fast: false matrix: rocky-version: ["8", "9", "10"] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Install Python" if: matrix.rocky-version == '8' run: | for i in {1..5}; do dnf install python39 python39-pip which -y && break || { echo "Attempt $i failed, retrying in 10 seconds..."; sleep 10; } if [ $i -eq 5 ]; then echo "Failed to install Python after 5 attempts" exit 1 fi done - name: "Install Python" if: matrix.rocky-version == '9' run: | for i in {1..5}; do dnf install python3.9 python3.9-pip which -y && break || { echo "Attempt $i failed, retrying in 10 seconds..."; sleep 10; } if [ $i -eq 5 ]; then echo "Failed to install Python after 5 attempts" exit 1 fi done - name: "Install Python" if: matrix.rocky-version == '10' run: | dnf install python3 python3-pip which -y - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-musl-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which python3) # Needed for building Pydantic - name: "Install build tools" run: dnf install -y gcc - name: "Validate global Python install" run: python3 scripts/check_system_python.py --uv ./uv system-test-graalpy: timeout-minutes: 10 needs: build-binary-linux-libc name: "check system | graalpy on ubuntu" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "graalpy24.1" - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which graalpy) - name: "Validate global Python install" run: graalpy scripts/check_system_python.py --uv ./uv system-test-pypy: timeout-minutes: 10 needs: build-binary-linux-libc name: "check system | pypy on ubuntu" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "pypy3.9" - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which pypy) - name: "Validate global Python install" run: pypy scripts/check_system_python.py --uv ./uv system-test-pyston: timeout-minutes: 10 needs: build-binary-linux-musl name: "check system | pyston" runs-on: ubuntu-latest container: pyston/pyston:2.3.5 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-musl-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which pyston) - name: "Validate global Python install" run: pyston scripts/check_system_python.py --uv ./uv system-test-alpine: timeout-minutes: 10 needs: build-binary-linux-musl name: "check system | alpine" runs-on: ubuntu-latest container: alpine:latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Install Python" run: apk add --update --no-cache python3 py3-pip - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-musl-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which python3) - name: "Validate global Python install" run: python3 scripts/check_system_python.py --uv ./uv --externally-managed system-test-macos-aarch64: timeout-minutes: 10 needs: build-binary-macos-aarch64 name: "check system | python on macos aarch64" runs-on: macos-14 # github-macos-14-aarch64-3 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-macos-aarch64-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which python3) - name: "Validate global Python install" run: python3 scripts/check_system_python.py --uv ./uv --externally-managed system-test-macos-aarch64-homebrew: timeout-minutes: 10 needs: build-binary-macos-aarch64 name: "check system | homebrew python on macos aarch64" runs-on: macos-14 # github-macos-14-aarch64-3 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Install Python" run: brew install python3 - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-macos-aarch64-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which python3) - name: "Validate global Python install" run: python3 scripts/check_system_python.py --uv ./uv --externally-managed system-test-macos-aarch64-emulated: timeout-minutes: 10 needs: build-binary-macos-aarch64 name: "check system | x86-64 python on macos aarch64" runs-on: macos-14 # github-macos-14-aarch64-3 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: 3.13 architecture: x64 - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-macos-aarch64-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which python3) - name: "Validate global Python install" run: python3 scripts/check_system_python.py --uv ./uv --externally-managed system-test-macos-x86_64: timeout-minutes: 10 needs: build-binary-macos-x86_64 name: "check system | python on macos x86-64" runs-on: macos-15-intel # github-macos-15-x86_64-4 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false # We test with GitHub's Python as a regression test for # https://github.com/astral-sh/uv/issues/2450 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-macos-x86_64-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which python3) - name: "Validate global Python install" run: python3 scripts/check_system_python.py --uv ./uv --externally-managed system-test-windows-python-310: timeout-minutes: 10 needs: build-binary-windows-x86_64 name: "check system | python3.10 on windows x86-64" runs-on: windows-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-x86_64-${{ github.sha }} - name: "Print Python path" run: echo $(which python) - name: "Validate global Python install" run: py -3.10 ./scripts/check_system_python.py --uv ./uv.exe system-test-windows-x86_64-python-310: timeout-minutes: 10 needs: build-binary-windows-x86_64 name: "check system | python3.10 on windows x86" runs-on: windows-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" architecture: "x86" - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-x86_64-${{ github.sha }} - name: "Print Python path" run: echo $(which python) - name: "Validate global Python install" run: python ./scripts/check_system_python.py --uv ./uv.exe system-test-windows-x86_64-python-313: timeout-minutes: 10 needs: build-binary-windows-x86_64 name: "check system | python3.13 on windows x86-64" runs-on: windows-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" allow-prereleases: true - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-x86_64-${{ github.sha }} - name: "Print Python path" run: echo $(which python) - name: "Validate global Python install" run: py -3.13 ./scripts/check_system_python.py --uv ./uv.exe system-test-windows-aarch64-x86-python-313: timeout-minutes: 10 needs: build-binary-windows-aarch64 name: "check system | x86-64 python3.13 on windows aarch64" runs-on: windows-11-arm steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" architecture: "x64" allow-prereleases: true - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-aarch64-${{ github.sha }} - name: "Validate global Python install" run: py -3.13 ./scripts/check_system_python.py --uv ./uv.exe system-test-windows-aarch64-aarch64-python-313: timeout-minutes: 10 needs: build-binary-windows-aarch64 name: "check system | aarch64 python3.13 on windows aarch64" runs-on: windows-11-arm steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" architecture: "arm64" allow-prereleases: true - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-aarch64-${{ github.sha }} - name: "Validate global Python install" run: py -3.13-arm64 ./scripts/check_system_python.py --uv ./uv.exe # Test our PEP 514 integration that installs Python into the Windows registry. system-test-windows-registry: timeout-minutes: 10 needs: build-binary-windows-x86_64 name: "check system | windows registry" runs-on: windows-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-x86_64-${{ github.sha }} # NB: Run this last, we are modifying the registry - name: "Test PEP 514 registration" run: python ./scripts/check_registry.py --uv ./uv.exe system-test-choco: timeout-minutes: 10 needs: build-binary-windows-x86_64 name: "check system | python3.12 via chocolatey" runs-on: windows-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Install Python" run: choco install python3 --verbose --version=3.9.13 - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-x86_64-${{ github.sha }} - name: "Print Python path" run: echo $(which python3) - name: "Validate global Python install" run: py -3.9 ./scripts/check_system_python.py --uv ./uv.exe system-test-pyenv: timeout-minutes: 10 needs: build-binary-linux-libc name: "check system | python3.9 via pyenv" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Install pyenv" run: | # Install pyenv curl https://pyenv.run | bash # Set up environment variables for current step export PYENV_ROOT="$HOME/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" eval "$(pyenv init -)" # Install Python 3.9 pyenv install 3.9 pyenv global 3.9 # Make environment variables persist across steps echo "PYENV_ROOT=$HOME/.pyenv" >> $GITHUB_ENV echo "$HOME/.pyenv/bin" >> $GITHUB_PATH echo "$HOME/.pyenv/shims" >> $GITHUB_PATH - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which python3.9) - name: "Validate global Python install" run: python3.9 scripts/check_system_python.py --uv ./uv system-test-linux-313: timeout-minutes: 10 needs: build-binary-linux-libc name: "check system | python3.13" runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: 3.13 allow-prereleases: true - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-libc-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which python3.13) - name: "Validate global Python install" run: python3.13 scripts/check_system_python.py --uv ./uv system-test-conda: timeout-minutes: 10 needs: [ build-binary-windows-x86_64, build-binary-macos-aarch64, build-binary-linux-libc, ] name: check system | conda${{ matrix.python-version }} on ${{ matrix.os }} ${{ matrix.arch }} runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: os: ["linux", "windows", "macos"] python-version: ["3.8", "3.11"] include: - { os: "linux", target: "linux-libc", runner: "ubuntu-latest", arch: "x86-64", } - { os: "windows", target: "windows-x86_64", runner: "windows-latest", arch: "x86-64", } - { os: "macos", target: "macos-aarch64", runner: "macos-14", arch: "aarch64", } steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: conda-incubator/setup-miniconda@835234971496cad1653abb28a638a281cf32541f # v3.2.0 with: miniconda-version: "latest" activate-environment: uv python-version: ${{ matrix.python-version }} - name: Conda info shell: bash -el {0} run: conda info - name: Conda list shell: pwsh run: conda list - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-${{ matrix.target }}-${{ github.sha }} - name: "Prepare binary" if: ${{ matrix.os != 'windows' }} run: chmod +x ./uv - name: "Print Python path" shell: bash -el {0} run: echo $(which python) - name: "Validate global Python install" shell: bash -el {0} run: python ./scripts/check_system_python.py --uv ./uv system-test-amazonlinux: timeout-minutes: 10 needs: build-binary-linux-musl name: "check system | amazonlinux" runs-on: ubuntu-latest container: amazonlinux:2023 steps: - name: "Install base requirements" run: | # Needed for `actions/checkout` yum install tar gzip which -y - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Install Python" run: yum install python3 python3-pip -y - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-linux-musl-${{ github.sha }} - name: "Prepare binary" run: chmod +x ./uv - name: "Print Python path" run: echo $(which python3) - name: Install build tools run: yum install -y gcc - name: "Validate global Python install" run: python3 scripts/check_system_python.py --uv ./uv system-test-windows-embedded-python-310: timeout-minutes: 10 needs: build-binary-windows-x86_64 name: "check system | embedded python3.10 on windows x86-64" runs-on: windows-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Download binary" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: uv-windows-x86_64-${{ github.sha }} # Download embedded Python. - name: "Download embedded Python" run: curl -LsSf https://www.python.org/ftp/python/3.11.8/python-3.11.8-embed-amd64.zip -o python-3.11.8-embed-amd64.zip - name: "Unzip embedded Python" run: 7z x python-3.11.8-embed-amd64.zip -oembedded-python - name: "Show embedded Python contents" run: ls embedded-python - name: "Set PATH" run: echo "${{ github.workspace }}\embedded-python" >> $env:GITHUB_PATH - name: "Print Python path" run: echo $(which python) - name: "Validate embedded Python install" run: python ./scripts/check_embedded_python.py --uv ./uv.exe benchmarks-walltime: name: "benchmarks | walltime aarch64 linux" runs-on: codspeed-macro needs: determine_changes if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 25 steps: - name: "Checkout Branch" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - name: "Install Rust toolchain" run: rustup show - name: "Install codspeed" uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1 with: tool: cargo-codspeed - name: "Install requirements and prime cache" run: | sudo apt-get update sudo apt-get install -y libsasl2-dev libldap2-dev libkrb5-dev cargo run --bin uv -- venv --cache-dir .cache cargo run --bin uv -- pip compile test/requirements/jupyter.in --universal --exclude-newer 2024-08-08 --cache-dir .cache cargo run --bin uv -- pip compile test/requirements/airflow.in --universal --exclude-newer 2024-08-08 --cache-dir .cache - name: "Build benchmarks" run: cargo codspeed build --profile profiling -p uv-bench - name: "Run benchmarks" uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1 with: run: cargo codspeed run mode: walltime token: ${{ secrets.CODSPEED_TOKEN }} benchmarks-instrumented: name: "benchmarks | instrumented" runs-on: ubuntu-latest needs: determine_changes if: ${{ github.repository == 'astral-sh/uv' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 20 steps: - name: "Checkout Branch" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - name: "Install Rust toolchain" run: rustup show - name: "Install codspeed" uses: taiki-e/install-action@a416ddeedbd372e614cc1386e8b642692f66865e # v2.57.1 with: tool: cargo-codspeed - name: "Install requirements and prime cache" run: | sudo apt-get update sudo apt-get install -y libsasl2-dev libldap2-dev libkrb5-dev cargo run --bin uv -- venv --cache-dir .cache cargo run --bin uv -- pip compile test/requirements/jupyter.in --universal --exclude-newer 2024-08-08 --cache-dir .cache cargo run --bin uv -- pip compile test/requirements/airflow.in --universal --exclude-newer 2024-08-08 --cache-dir .cache - name: "Build benchmarks" run: cargo codspeed build --profile profiling -p uv-bench - name: "Run benchmarks" uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1 with: run: cargo codspeed run mode: instrumentation token: ${{ secrets.CODSPEED_TOKEN }} uv-0.9.17+ds1/.github/workflows/publish-crates.yml000066400000000000000000000017071520155276700220110ustar00rootroot00000000000000# Publish a release to crates.io. # # Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a publish job # within `cargo-dist`. name: "Publish to crates.io" on: workflow_call: inputs: plan: required: true type: string jobs: crates-publish-uv: name: Upload uv to crates.io runs-on: ubuntu-latest environment: name: release permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false # TODO(zanieb): Switch to trusted publishing once published # - uses: rust-lang/crates-io-auth-action@v1 # id: auth - name: Publish workspace crates # Note `--no-verify` is safe because we do a publish dry-run elsewhere in CI run: cargo publish --workspace --no-verify env: CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_TOKEN }} uv-0.9.17+ds1/.github/workflows/publish-docs.yml000066400000000000000000000125541520155276700214620ustar00rootroot00000000000000# Publish the uv documentation. # # Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a post-announce # job within `cargo-dist`. name: mkdocs on: workflow_dispatch: inputs: ref: description: "The commit SHA, tag, or branch to publish. Uses the default branch if not specified." default: "" type: string workflow_call: inputs: plan: required: true type: string permissions: {} jobs: mkdocs: runs-on: ubuntu-latest env: VERSION: ${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || inputs.ref }} MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ inputs.ref }} fetch-depth: 0 persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: 3.12 - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: "Generate reference documentation" run: | cargo dev generate-options-reference cargo dev generate-cli-reference cargo dev generate-env-vars-reference - name: "Set docs display name" run: | version="${VERSION}" # if version is missing, use 'latest' if [ -z "$version" ]; then echo "Using 'latest' as version" version="latest" fi # Use version as display name for now display_name="$version" echo "DISPLAY_NAME=$display_name" >> $GITHUB_ENV - name: "Set branch name" run: | version="${VERSION}" display_name="${DISPLAY_NAME}" timestamp="$(date +%s)" # create branch_display_name from display_name by replacing all # characters disallowed in git branch names with hyphens branch_display_name="$(echo "$display_name" | tr -c '[:alnum:]._' '-' | tr -s '-')" echo "BRANCH_NAME=update-docs-$branch_display_name-$timestamp" >> $GITHUB_ENV echo "TIMESTAMP=$timestamp" >> $GITHUB_ENV - name: "Add SSH key" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 with: ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} - name: "Install Insiders dependencies" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} run: pip install -r docs/requirements-insiders.txt - name: "Install dependencies" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} run: pip install -r docs/requirements.txt - name: "Build Insiders docs" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} run: mkdocs build --strict -f mkdocs.insiders.yml - name: "Build docs" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} run: mkdocs build --strict -f mkdocs.public.yml - name: "Clone docs repo" run: | version="${VERSION}" git clone https://${ASTRAL_DOCS_PAT}@github.com/astral-sh/docs.git astral-docs env: ASTRAL_DOCS_PAT: ${{ secrets.ASTRAL_DOCS_PAT }} - name: "Copy docs" run: rm -rf astral-docs/site/uv && mkdir -p astral-docs/site && cp -r site/uv astral-docs/site/ - name: "Commit docs" working-directory: astral-docs run: | branch_name="${BRANCH_NAME}" git config user.name "astral-docs-bot" git config user.email "176161322+astral-docs-bot@users.noreply.github.com" git checkout -b $branch_name git add site/uv git commit -m "Update uv documentation for $version" - name: "Create Pull Request" working-directory: astral-docs env: GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }} run: | version="${VERSION}" display_name="${DISPLAY_NAME}" branch_name="${BRANCH_NAME}" # set the PR title pull_request_title="Update uv documentation for $display_name" # Delete any existing pull requests that are open for this version # by checking against pull_request_title because the new PR will # supersede the old one. gh pr list --state open --json title --jq '.[] | select(.title == "$pull_request_title") | .number' | \ xargs -I {} gh pr close {} # push the branch to GitHub git push origin $branch_name # create the PR gh pr create --base main --head $branch_name \ --title "$pull_request_title" \ --body "Automated documentation update for $display_name" \ --label "documentation" - name: "Merge Pull Request" if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} working-directory: astral-docs env: GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }} run: | branch_name="${BRANCH_NAME}" # auto-merge the PR if the build was triggered by a release. Manual builds should be reviewed by a human. # give the PR a few seconds to be created before trying to auto-merge it sleep 10 gh pr merge --squash $branch_name uv-0.9.17+ds1/.github/workflows/publish-pypi.yml000066400000000000000000000026311520155276700215060ustar00rootroot00000000000000# Publish a release to PyPI. # # Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a publish job # within `cargo-dist`. name: "Publish to PyPI" on: workflow_call: inputs: plan: required: true type: string jobs: pypi-publish-uv: name: Upload uv to PyPI runs-on: ubuntu-latest environment: name: release permissions: id-token: write # For PyPI's trusted publishing steps: - name: "Install uv" uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: pattern: wheels_uv-* path: wheels_uv merge-multiple: true - name: Publish to PyPI run: uv publish -v wheels_uv/* pypi-publish-uv-build: name: Upload uv-build to PyPI runs-on: ubuntu-latest environment: name: release permissions: id-token: write # For PyPI's trusted publishing steps: - name: "Install uv" uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: pattern: wheels_uv_build-* path: wheels_uv_build merge-multiple: true - name: Publish to PyPI run: uv publish -v wheels_uv_build/* uv-0.9.17+ds1/.github/workflows/release.yml000066400000000000000000000301631520155276700205020ustar00rootroot00000000000000# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist # # Copyright 2022-2024, axodotdev # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: # # * checks for a Git Tag that looks like a release # * builds artifacts with dist (archives, installers, hashes) # * uploads those artifacts to temporary workflow zip # * on success, uploads the artifacts to a GitHub Release # # Note that the GitHub Release will be created with a generated # title/body based on your changelogs. name: Release permissions: "contents": "write" # This task will run whenever you workflow_dispatch with a tag that looks like a version # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION # must be a Cargo-style SemVer Version (must have at least major.minor.patch). # # If PACKAGE_NAME is specified, then the announcement will be for that # package (erroring out if it doesn't have the given version or isn't dist-able). # # If PACKAGE_NAME isn't specified, then the announcement will be for all # (dist-able) packages in the workspace with that version (this mode is # intended for workspaces with only one dist-able package, or with all dist-able # packages versioned/released in lockstep). # # If you push multiple tags at once, separate instances of this workflow will # spin up, creating an independent announcement for each one. However, GitHub # will hard limit this to 3 tags per commit, as it will assume more tags is a # mistake. # # If there's a prerelease-style suffix to the version, then the release(s) # will be marked as a prerelease. on: pull_request: workflow_dispatch: inputs: tag: description: Release Tag required: true default: dry-run type: string jobs: # Run 'dist plan' (or host) to determine what tasks we need to do plan: runs-on: "depot-ubuntu-latest-4" outputs: val: ${{ steps.plan.outputs.manifest }} tag: ${{ (inputs.tag != 'dry-run' && inputs.tag) || '' }} tag-flag: ${{ inputs.tag && inputs.tag != 'dry-run' && format('--tag={0}', inputs.tag) || '' }} publishing: ${{ inputs.tag && inputs.tag != 'dry-run' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: persist-credentials: false submodules: recursive - name: Install dist # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.2/cargo-dist-installer.sh | sh" - name: Cache dist uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: name: cargo-dist-cache path: ~/.cargo/bin/dist # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* # but also really annoying to build CI around when it needs secrets to work right.) - id: plan run: | dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --output-format=json > plan-dist-manifest.json echo "dist ran successfully" cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: name: artifacts-plan-dist-manifest path: plan-dist-manifest.json custom-build-binaries: needs: - plan if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }} uses: ./.github/workflows/build-binaries.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit custom-build-docker: needs: - plan if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }} uses: ./.github/workflows/build-docker.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit permissions: "attestations": "write" "contents": "read" "id-token": "write" "packages": "write" # Build and package all the platform-agnostic(ish) things build-global-artifacts: needs: - plan - custom-build-binaries - custom-build-docker runs-on: "depot-ubuntu-latest-4" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: persist-credentials: false submodules: recursive - name: Install cached dist uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true - id: cargo-dist shell: bash run: | dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json echo "dist ran successfully" # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: name: artifacts-build-global path: | ${{ steps.cargo-dist.outputs.paths }} ${{ env.BUILD_MANIFEST_NAME }} # Determines if we should publish/announce host: needs: - plan - custom-build-binaries - custom-build-docker - build-global-artifacts # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-binaries.result == 'skipped' || needs.custom-build-binaries.result == 'success') && (needs.custom-build-docker.result == 'skipped' || needs.custom-build-docker.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} runs-on: "depot-ubuntu-latest-4" outputs: val: ${{ steps.host.outputs.manifest }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: persist-credentials: false submodules: recursive - name: Install cached dist uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true # This is a harmless no-op for GitHub Releases, hosting for that happens in "announce" - id: host shell: bash run: | dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json echo "artifacts uploaded and released successfully" cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47 with: # Overwrite the previous copy name: artifacts-dist-manifest path: dist-manifest.json custom-publish-pypi: needs: - plan - host if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} uses: ./.github/workflows/publish-pypi.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit # publish jobs get escalated permissions permissions: "id-token": "write" "packages": "write" custom-publish-crates: needs: - plan - host - custom-publish-pypi # DIRTY: see #16989 if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} uses: ./.github/workflows/publish-crates.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit # publish jobs get escalated permissions permissions: "contents": "read" # Create a GitHub Release while uploading all files to it announce: needs: - plan - host - custom-publish-pypi - custom-publish-crates # use "always() && ..." to allow us to wait for all publish jobs while # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') && (needs.custom-publish-crates.result == 'skipped' || needs.custom-publish-crates.result == 'success') }} runs-on: "depot-ubuntu-latest-4" permissions: "attestations": "write" "contents": "write" "id-token": "write" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: persist-credentials: false submodules: recursive # Create a GitHub Release while uploading all files to it - name: "Download GitHub Artifacts" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: artifacts-* path: artifacts merge-multiple: true - name: Cleanup run: | # Remove the granular manifests rm -f artifacts/*-dist-manifest.json - name: Attest uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 with: subject-path: | artifacts/*.json artifacts/*.sh artifacts/*.ps1 artifacts/*.zip artifacts/*.tar.gz - name: Create GitHub Release env: PRERELEASE_FLAG: "${{ fromJson(needs.host.outputs.val).announcement_is_prerelease && '--prerelease' || '' }}" ANNOUNCEMENT_TITLE: "${{ fromJson(needs.host.outputs.val).announcement_title }}" ANNOUNCEMENT_BODY: "${{ fromJson(needs.host.outputs.val).announcement_github_body }}" RELEASE_COMMIT: "${{ github.sha }}" run: | # Write and read notes from a file to avoid quoting breaking things echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* custom-publish-docs: needs: - plan - announce uses: ./.github/workflows/publish-docs.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit uv-0.9.17+ds1/.github/workflows/setup-dev-drive.ps1000066400000000000000000000063461520155276700220150ustar00rootroot00000000000000# Configures a drive for testing in CI. # # When using standard GitHub Actions runners, a `D:` drive is present and has # similar or better performance characteristics than a ReFS dev drive. Sometimes # using a larger runner is still more performant (e.g., when running the test # suite) and we need to create a dev drive. This script automatically configures # the appropriate drive. # # When using GitHub Actions' "larger runners", the `D:` drive is not present and # we create a DevDrive mount on `C:`. This is purported to be more performant # than an ReFS drive, though we did not see a change when we switched over. # # When using Depot runners, the underling infrastructure is EC2, which does not # support Hyper-V. The `New-VHD` commandlet only works with Hyper-V, but we can # create a ReFS drive using `diskpart` and `format` directory. We cannot use a # DevDrive, as that also requires Hyper-V. The Depot runners use `D:` already, # so we must check if it's a Depot runner first, and we use `V:` as the target # instead. if ($env:DEPOT_RUNNER -eq "1") { Write-Output "DEPOT_RUNNER detected, setting up custom dev drive..." # Create VHD and configure drive using diskpart $vhdPath = "C:\uv_dev_drive.vhdx" @" create vdisk file="$vhdPath" maximum=25600 type=expandable attach vdisk create partition primary active assign letter=V "@ | diskpart # Format the drive as ReFS format V: /fs:ReFS /q /y $Drive = "V:" Write-Output "Custom dev drive created at $Drive" } elseif (Test-Path "D:\") { # Note `Get-PSDrive` is not sufficient because the drive letter is assigned. Write-Output "Using existing drive at D:" $Drive = "D:" } else { # The size (25 GB) is chosen empirically to be large enough for our # workflows; larger drives can take longer to set up. $Volume = New-VHD -Path C:/uv_dev_drive.vhdx -SizeBytes 25GB | Mount-VHD -Passthru | Initialize-Disk -Passthru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -DevDrive -Confirm:$false -Force $Drive = "$($Volume.DriveLetter):" # Set the drive as trusted # See https://learn.microsoft.com/en-us/windows/dev-drive/#how-do-i-designate-a-dev-drive-as-trusted fsutil devdrv trust $Drive # Disable antivirus filtering on dev drives # See https://learn.microsoft.com/en-us/windows/dev-drive/#how-do-i-configure-additional-filters-on-dev-drive fsutil devdrv enable /disallowAv # Remount so the changes take effect Dismount-VHD -Path C:/uv_dev_drive.vhdx Mount-VHD -Path C:/uv_dev_drive.vhdx # Show some debug information Write-Output $Volume fsutil devdrv query $Drive Write-Output "Using Dev Drive at $Volume" } $Tmp = "$($Drive)\uv-tmp" # Create the directory ahead of time in an attempt to avoid race-conditions New-Item $Tmp -ItemType Directory # Move Cargo to the dev drive New-Item -Path "$($Drive)/.cargo/bin" -ItemType Directory -Force if (Test-Path "C:/Users/runneradmin/.cargo") { Copy-Item -Path "C:/Users/runneradmin/.cargo/*" -Destination "$($Drive)/.cargo/" -Recurse -Force } Write-Output ` "DEV_DRIVE=$($Drive)" ` "TMP=$($Tmp)" ` "TEMP=$($Tmp)" ` "RUSTUP_HOME=$($Drive)/.rustup" ` "CARGO_HOME=$($Drive)/.cargo" ` "UV_WORKSPACE=$($Drive)/uv" ` "PATH=$($Drive)/.cargo/bin;$env:PATH" ` >> $env:GITHUB_ENV uv-0.9.17+ds1/.github/workflows/sync-python-releases.yml000066400000000000000000000033431520155276700231560ustar00rootroot00000000000000# Sync Python releases and create a pull request. # # Based on: https://github.com/astral-sh/rye/blob/57b7c089e494138aae29a130afb2e17f447970bf/.github/workflows/sync-python-releases.yml name: "Sync Python downloads" on: workflow_dispatch: schedule: - cron: "0 0 * * *" permissions: {} jobs: sync: if: github.repository == 'astral-sh/uv' runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 with: version: "latest" enable-cache: true - name: Sync Python Releases run: | uv run -- fetch-download-metadata.py working-directory: ./crates/uv-python env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Sync Sysconfig Targets run: ${GITHUB_WORKSPACE}/crates/uv-dev/sync_sysconfig_targets.sh working-directory: ./crates/uv-dev env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: "Create Pull Request" uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: commit-message: "Sync latest Python releases" add-paths: | crates/uv-python/download-metadata.json crates/uv-dev/src/generate_sysconfig_mappings.rs crates/uv-python/src/sysconfig/generated_mappings.rs branch: "sync-python-releases" title: "Sync latest Python releases" body: "Automated update for Python releases." base: "main" draft: true uv-0.9.17+ds1/.github/workflows/zizmor.yml000066400000000000000000000007721520155276700204170ustar00rootroot00000000000000name: zizmor on: push: branches: ["main"] pull_request: branches: ["**"] permissions: {} jobs: zizmor: name: Run zizmor runs-on: ubuntu-latest permissions: security-events: write steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Run zizmor uses: zizmorcore/zizmor-action@5ca5fc7a4779c5263a3ffa0e1f693009994446d1 # v0.1.2 uv-0.9.17+ds1/.gitignore000066400000000000000000000013521520155276700147300ustar00rootroot00000000000000# Insta snapshots. *.pending-snap # Generated by Cargo # will have compiled files and executables /vendor/ debug/ target-alpine/ target/ # Bootstrapped Python versions /bin/ # These are backup files generated by rustfmt **/*.rs.bk # MSVC Windows builds of rustc generate these, which store debugging information *.pdb # Python tmp files __pycache__ # Maturin builds, and other native editable builds *.so *.pyd *.dll /dist /crates/uv-build/dist # Profiling flamegraph.svg perf.data perf.data.old profile.json profile.json.gz # MkDocs /site # Generated reference docs (use `cargo dev generate-all` to regenerate) /docs/reference/cli.md /docs/reference/environment.md /docs/reference/settings.md # macOS **/.DS_Store # IDE .idea .vscode uv-0.9.17+ds1/.ignore000066400000000000000000000000131520155276700142150ustar00rootroot00000000000000!/.github/ uv-0.9.17+ds1/.pre-commit-config.yaml000066400000000000000000000020441520155276700172200ustar00rootroot00000000000000fail_fast: true exclude: | (?x)^( .*/(snapshots)/.*| )$ repos: - repo: https://github.com/abravalheri/validate-pyproject rev: v0.24.1 hooks: - id: validate-pyproject - repo: https://github.com/crate-ci/typos rev: v1.37.2 hooks: - id: typos - repo: local hooks: - id: cargo-fmt name: cargo fmt entry: cargo fmt -- language: system types: [rust] pass_filenames: false # This makes it a lot faster - repo: local hooks: - id: cargo-dev-generate-all name: cargo dev generate-all entry: cargo dev generate-all language: system types: [rust] pass_filenames: false files: ^crates/(uv-cli|uv-settings)/ - repo: https://github.com/pre-commit/mirrors-prettier rev: v3.1.0 hooks: - id: prettier types_or: [yaml, json5] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.13.3 hooks: - id: ruff-format - id: ruff args: [--fix, --exit-non-zero-on-fix] uv-0.9.17+ds1/.prettierignore000066400000000000000000000003071520155276700160020ustar00rootroot00000000000000.venv CHANGELOG.md PREVIEW-CHANGELOG.md docs/reference/cli.md docs/reference/settings.md docs/reference/environment.md test/ecosystem/home-assistant-core/LICENSE.md docs/guides/integration/gitlab.md uv-0.9.17+ds1/.python-versions000066400000000000000000000016431520155276700161330ustar00rootroot00000000000000# These are versions of Python required for running uv's own test suite. You can add or remove # versions here as needed for tests; this doesn't impact uv's own functionality. They can be # installed through any means you like, e.g. `uv python install` if you already have a build of uv, # `cargo run python install`, or through some other installer. # # In uv's CI in GitHub Actions, they are bootstrapped by an existing released version of uv, # installed by the astral-sh/setup-uv action If you need a newer or different version, you will # first need to complete a uv release capable of installing that version, get it picked up by # astral-sh/setup-uv, and update its hash in .github/workflows. 3.14.0 3.13.2 3.12.9 3.11.11 3.10.16 3.9.21 3.8.20 # The following are required for packse scenarios 3.9.20 3.9.12 # The following is needed for `==3.13` request tests 3.13.0 # A pre-release version required for testing 3.14.0rc2 uv-0.9.17+ds1/BENCHMARKS.md000066400000000000000000000107741520155276700147070ustar00rootroot00000000000000# Benchmarks All benchmarks were computed on macOS using Python 3.12.4 (for non-uv tools), and come with a few important caveats: - Benchmark performance may vary dramatically across different operating systems and filesystems. In particular, uv uses different installation strategies based on the underlying filesystem's capabilities. (For example, uv uses reflinking on macOS, and hardlinking on Linux.) - Benchmark performance may vary dramatically depending on the set of packages being installed. For example, a resolution that requires building a single intensive source distribution may appear very similar across tools, since the bottleneck is tool-agnostic. This document benchmarks against Trio's `docs-requirements.in`, as a representative example of a real-world project. In each case, a smaller bar (i.e., lower) is better. ## Warm Installation Benchmarking package installation (e.g., `uv sync`) with a warm cache. This is equivalent to removing and recreating a virtual environment, and then populating it with dependencies that you've installed previously on the same machine. ![install-warm](https://github.com/user-attachments/assets/84118aaa-d030-4e29-8f1e-9483091ceca3) ## Cold Installation Benchmarking package installation (e.g., `uv sync`) with a cold cache. This is equivalent to running `uv sync` on a new machine or in CI (assuming that the package manager cache is not shared across runs). ![install-cold](https://github.com/user-attachments/assets/e7f5b203-7e84-452b-8c56-1ff6531c9898) ## Warm Resolution Benchmarking dependency resolution (e.g., `uv lock`) with a warm cache, but no existing lockfile. This is equivalent to blowing away an existing `requirements.txt` file to regenerate it from a `requirements.in` file. ![resolve-warm](https://github.com/user-attachments/assets/e1637a08-8b27-4077-8138-b3849e53eb04) ## Cold Resolution Benchmarking dependency resolution (e.g., `uv lock`) with a cold cache. This is equivalent to running `uv lock` on a new machine or in CI (assuming that the package manager cache is not shared across runs). ![resolve-cold](https://github.com/user-attachments/assets/b578c264-c209-45ab-b4c3-54073d871e86) ## Reproduction All benchmarks were generated using the `scripts/benchmark` package, which wraps [`hyperfine`](https://github.com/sharkdp/hyperfine) to facilitate benchmarking uv against a variety of other tools. The benchmark script itself has a several requirements: - A local uv release build (`cargo build --release`). - An installation of the production `uv` binary in your path. - The [`hyperfine`](https://github.com/sharkdp/hyperfine) command-line tool installed on your system. To benchmark resolution against pip-compile, Poetry, and PDM: ```shell uv run resolver \ --uv-project \ --poetry \ --pdm \ --pip-compile \ --benchmark resolve-warm --benchmark resolve-cold \ --json \ ../requirements/trio.in ``` To benchmark installation against pip-sync, Poetry, and PDM: ```shell uv run resolver \ --uv-project \ --poetry \ --pdm \ --pip-sync \ --benchmark install-warm --benchmark install-cold \ --json \ ../requirements/compiled/trio.txt ``` Both commands should be run from the `scripts/benchmark` directory. After running the benchmark script, you can generate the corresponding graph via: ```shell cargo run -p uv-dev --all-features render-benchmarks resolve-warm.json --title "Warm Resolution" cargo run -p uv-dev --all-features render-benchmarks resolve-cold.json --title "Cold Resolution" cargo run -p uv-dev --all-features render-benchmarks install-warm.json --title "Warm Installation" cargo run -p uv-dev --all-features render-benchmarks install-cold.json --title "Cold Installation" ``` You need to install the [Roboto Font](https://fonts.google.com/specimen/Roboto) if the labels are missing in the generated graph. ## Acknowledgements The inclusion of this `BENCHMARKS.md` file was inspired by the excellent benchmarking documentation in [Orogene](https://github.com/orogene/orogene/blob/472e481b4fc6e97c2b57e69240bf8fe995dfab83/BENCHMARKS.md). ## Troubleshooting ### Flaky benchmarks If you're seeing high variance when running the cold benchmarks, then it's likely that you're running into throttling or DDoS prevention from your ISP. In that case, ISPs forcefully terminate TCP connections with a TCP reset. We believe this is due to the benchmarks making the exact same requests in a very short time (especially true for `uv`). A possible workaround is to connect to VPN to bypass your ISPs filtering mechanism. uv-0.9.17+ds1/CHANGELOG.md000066400000000000000000000664231520155276700145630ustar00rootroot00000000000000# Changelog ## 0.9.17 Released on 2025-12-09. ### Enhancements - Add `torch-tensorrt` and `torchao` to the PyTorch list ([#17053](https://github.com/astral-sh/uv/pull/17053)) - Add hint for misplaced `--verbose` in `uv tool run` ([#17020](https://github.com/astral-sh/uv/pull/17020)) - Add support for relative durations in `exclude-newer` (a.k.a., dependency cooldowns) ([#16814](https://github.com/astral-sh/uv/pull/16814)) - Add support for relocatable nushell activation script ([#17036](https://github.com/astral-sh/uv/pull/17036)) ### Bug fixes - Respect dropped (but explicit) indexes in dependency groups ([#17012](https://github.com/astral-sh/uv/pull/17012)) ### Documentation - Improve `source-exclude` reference docs ([#16832](https://github.com/astral-sh/uv/pull/16832)) - Recommend `UV_NO_DEV` in Docker installs ([#17030](https://github.com/astral-sh/uv/pull/17030)) - Update `UV_VERSION` in docs for GitLab CI/CD ([#17040](https://github.com/astral-sh/uv/pull/17040)) ## 0.9.16 Released on 2025-12-06. ### Python - Add CPython 3.14.2 - Add CPython 3.13.11 ### Enhancements - Add a 5m default timeout to acquiring file locks to fail faster on deadlock ([#16342](https://github.com/astral-sh/uv/pull/16342)) - Add a stub `debug` subcommand to `uv pip` announcing its intentional absence ([#16966](https://github.com/astral-sh/uv/pull/16966)) - Add bounds in `uv add --script` ([#16954](https://github.com/astral-sh/uv/pull/16954)) - Add brew specific message for `uv self update` ([#16838](https://github.com/astral-sh/uv/pull/16838)) - Error when built wheel is for the wrong platform ([#16074](https://github.com/astral-sh/uv/pull/16074)) - Filter wheels from PEP 751 files based on `--no-binary` et al in `uv pip compile` ([#16956](https://github.com/astral-sh/uv/pull/16956)) - Support `--target` and `--prefix` in `uv pip list`, `uv pip freeze`, and `uv pip show` ([#16955](https://github.com/astral-sh/uv/pull/16955)) - Tweak language for build backend validation errors ([#16720](https://github.com/astral-sh/uv/pull/16720)) - Use explicit credentials cache instead of global static ([#16768](https://github.com/astral-sh/uv/pull/16768)) - Enable SIMD in HTML parsing ([#17010](https://github.com/astral-sh/uv/pull/17010)) ### Preview features - Fix missing preview warning in `uv workspace metadata` ([#16988](https://github.com/astral-sh/uv/pull/16988)) - Add a `uv auth helper --protocol bazel` command ([#16886](https://github.com/astral-sh/uv/pull/16886)) ### Bug fixes - Fix Pyston wheel compatibility tags ([#16972](https://github.com/astral-sh/uv/pull/16972)) - Allow redundant entries in `tool.uv.build-backend.module-name` but emit warnings ([#16928](https://github.com/astral-sh/uv/pull/16928)) - Fix infinite loop in non-attribute re-treats during HTML parsing ([#17010](https://github.com/astral-sh/uv/pull/17010)) ### Documentation - Clarify `--project` flag help text to indicate project discovery ([#16965](https://github.com/astral-sh/uv/pull/16965)) - Regenerate the crates.io READMEs on release ([#16992](https://github.com/astral-sh/uv/pull/16992)) - Update Docker integration guide to prefer `COPY` over `ADD` for simple cases ([#16883](https://github.com/astral-sh/uv/pull/16883)) - Update PyTorch documentation to include information about supporting CUDA 13.0.x ([#16957](https://github.com/astral-sh/uv/pull/16957)) - Update the versioning policy ([#16710](https://github.com/astral-sh/uv/pull/16710)) - Upgrade PyTorch documentation to latest versions ([#16970](https://github.com/astral-sh/uv/pull/16970)) ## 0.9.15 Released on 2025-12-02. ### Python - Add CPython 3.14.1 - Add CPython 3.13.10 ### Enhancements - Add ROCm 6.4 to `--torch-backend=auto` ([#16919](https://github.com/astral-sh/uv/pull/16919)) - Add a Windows manifest to uv binaries ([#16894](https://github.com/astral-sh/uv/pull/16894)) - Add LFS toggle to Git sources ([#16143](https://github.com/astral-sh/uv/pull/16143)) - Cache source reads during resolution ([#16888](https://github.com/astral-sh/uv/pull/16888)) - Allow reading requirements from scripts without an extension ([#16923](https://github.com/astral-sh/uv/pull/16923)) - Allow reading requirements from scripts with HTTP(S) paths ([#16891](https://github.com/astral-sh/uv/pull/16891)) ### Configuration - Add `UV_HIDE_BUILD_OUTPUT` to omit build logs ([#16885](https://github.com/astral-sh/uv/pull/16885)) ### Bug fixes - Fix `uv-trampoline-builder` builds from crates.io by moving bundled executables ([#16922](https://github.com/astral-sh/uv/pull/16922)) - Respect `NO_COLOR` and always show the command as a header when paging `uv help` output ([#16908](https://github.com/astral-sh/uv/pull/16908)) - Use `0o666` permissions for flock files instead of `0o777` ([#16845](https://github.com/astral-sh/uv/pull/16845)) - Revert "Bump `astral-tl` to v0.7.10 (#16887)" to narrow down a regression causing hangs in metadata retrieval ([#16938](https://github.com/astral-sh/uv/pull/16938)) ### Documentation - Link to the uv version in crates.io member READMEs ([#16939](https://github.com/astral-sh/uv/pull/16939)) ## 0.9.14 Released on 2025-12-01. ### Performance - Bump `astral-tl` to v0.7.10 to enable SIMD for HTML parsing ([#16887](https://github.com/astral-sh/uv/pull/16887)) ### Bug fixes - Allow earlier post releases with exclusive ordering ([#16881](https://github.com/astral-sh/uv/pull/16881)) - Prefer updating existing `.zshenv` over creating a new one in `tool update-shell` ([#16866](https://github.com/astral-sh/uv/pull/16866)) - Respect `-e` flags in `uv add` ([#16882](https://github.com/astral-sh/uv/pull/16882)) ### Enhancements - Attach subcommand to User-Agent string ([#16837](https://github.com/astral-sh/uv/pull/16837)) - Prefer `UV_WORKING_DIR` over `UV_WORKING_DIRECTORY` for consistency ([#16884](https://github.com/astral-sh/uv/pull/16884)) ## 0.9.13 Released on 2025-11-26. ### Bug fixes - Revert "Allow `--with-requirements` to load extensionless inline-metadata scripts" to fix reading of requirements files from streams ([#16861](https://github.com/astral-sh/uv/pull/16861)) - Validate URL wheel tags against `Requires-Python` and required environments ([#16824](https://github.com/astral-sh/uv/pull/16824)) ### Documentation - Drop unpublished crates from the uv crates.io README ([#16847](https://github.com/astral-sh/uv/pull/16847)) - Fix the links to uv in crates.io member READMEs ([#16848](https://github.com/astral-sh/uv/pull/16848)) ## 0.9.12 Released on 2025-11-24. ### Enhancements - Allow `--with-requirements` to load extensionless inline-metadata scripts ([#16744](https://github.com/astral-sh/uv/pull/16744)) - Collect and upload PEP 740 attestations during `uv publish` ([#16731](https://github.com/astral-sh/uv/pull/16731)) - Prevent `uv export` from overwriting `pyproject.toml` ([#16745](https://github.com/astral-sh/uv/pull/16745)) ### Documentation - Add a crates.io README for uv ([#16809](https://github.com/astral-sh/uv/pull/16809)) - Add documentation for intermediate Docker layers in a workspace ([#16787](https://github.com/astral-sh/uv/pull/16787)) - Enumerate workspace members in the uv crate README ([#16811](https://github.com/astral-sh/uv/pull/16811)) - Fix documentation links for crates ([#16801](https://github.com/astral-sh/uv/pull/16801)) - Generate a crates.io README for uv workspace members ([#16812](https://github.com/astral-sh/uv/pull/16812)) - Move the "Export" guide to the projects concept section ([#16835](https://github.com/astral-sh/uv/pull/16835)) - Update the cargo install recommendation to use crates ([#16800](https://github.com/astral-sh/uv/pull/16800)) - Use the word "internal" in crate descriptions ([#16810](https://github.com/astral-sh/uv/pull/16810)) ## 0.9.11 Released on 2025-11-20. ### Python - Add CPython 3.15.0a2 See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20251120) for details. ### Enhancements - Add SBOM support to `uv export` ([#16523](https://github.com/astral-sh/uv/pull/16523)) - Publish to `crates.io` ([#16770](https://github.com/astral-sh/uv/pull/16770)) ### Preview features - Add `uv workspace list --paths` ([#16776](https://github.com/astral-sh/uv/pull/16776)) - Fix the preview warning on `uv workspace dir` ([#16775](https://github.com/astral-sh/uv/pull/16775)) ### Bug fixes - Fix `uv init` author serialization via `toml_edit` inline tables ([#16778](https://github.com/astral-sh/uv/pull/16778)) - Fix status messages without TTY ([#16785](https://github.com/astral-sh/uv/pull/16785)) - Preserve end-of-line comment whitespace when editing `pyproject.toml` ([#16734](https://github.com/astral-sh/uv/pull/16734)) - Disable `always-authenticate` when running under Dependabot ([#16773](https://github.com/astral-sh/uv/pull/16773)) ### Documentation - Document the new behavior for free-threaded python versions ([#16781](https://github.com/astral-sh/uv/pull/16781)) - Improve note about build system in publish guide ([#16788](https://github.com/astral-sh/uv/pull/16788)) - Move do not upload publish note out of the guide into concepts ([#16789](https://github.com/astral-sh/uv/pull/16789)) ## 0.9.10 Released on 2025-11-17. ### Enhancements - Add support for `SSL_CERT_DIR` ([#16473](https://github.com/astral-sh/uv/pull/16473)) - Enforce UTF‑8-encoded license files during `uv build` ([#16699](https://github.com/astral-sh/uv/pull/16699)) - Error when a `project.license-files` glob matches nothing ([#16697](https://github.com/astral-sh/uv/pull/16697)) - `pip install --target` (and `sync`) install Python if necessary ([#16694](https://github.com/astral-sh/uv/pull/16694)) - Account for `python_downloads_json_url` in pre-release Python version warnings ([#16737](https://github.com/astral-sh/uv/pull/16737)) - Support HTTP/HTTPS URLs in `uv python --python-downloads-json-url` ([#16542](https://github.com/astral-sh/uv/pull/16542)) ### Preview features - Add support for `--upgrade` in `uv python install` ([#16676](https://github.com/astral-sh/uv/pull/16676)) - Fix handling of `python install --default` for pre-release Python versions ([#16706](https://github.com/astral-sh/uv/pull/16706)) - Add `uv workspace list` to list workspace members ([#16691](https://github.com/astral-sh/uv/pull/16691)) ### Bug fixes - Don't check file URLs for ambiguously parsed credentials ([#16759](https://github.com/astral-sh/uv/pull/16759)) ### Documentation - Add a "storage" reference document ([#15954](https://github.com/astral-sh/uv/pull/15954)) ## 0.9.9 Released on 2025-11-12. ### Deprecations - Deprecate use of `--project` in `uv init` ([#16674](https://github.com/astral-sh/uv/pull/16674)) ### Enhancements - Add iOS support to Python interpreter discovery ([#16686](https://github.com/astral-sh/uv/pull/16686)) - Reject ambiguously parsed URLs ([#16622](https://github.com/astral-sh/uv/pull/16622)) - Allow explicit values in `uv version --bump` ([#16555](https://github.com/astral-sh/uv/pull/16555)) - Warn on use of managed pre-release Python versions when a stable version is available ([#16619](https://github.com/astral-sh/uv/pull/16619)) - Allow signing trampolines on Windows by using `.rcdata` to store metadata ([#15068](https://github.com/astral-sh/uv/pull/15068)) - Add `--only-emit-workspace` and similar variants to `uv export` ([#16681](https://github.com/astral-sh/uv/pull/16681)) ### Preview features - Add `uv workspace dir` command ([#16678](https://github.com/astral-sh/uv/pull/16678)) - Add `uv workspace metadata` command ([#16516](https://github.com/astral-sh/uv/pull/16516)) ### Configuration - Add `UV_NO_DEFAULT_GROUPS` environment variable ([#16645](https://github.com/astral-sh/uv/pull/16645)) ### Bug fixes - Remove `torch-model-archiver` and `torch-tb-profiler` from PyTorch backend ([#16655](https://github.com/astral-sh/uv/pull/16655)) - Fix Pixi environment detection ([#16585](https://github.com/astral-sh/uv/pull/16585)) ### Documentation - Fix `CMD` path in FastAPI Dockerfile ([#16701](https://github.com/astral-sh/uv/pull/16701)) ## 0.9.8 Released on 2025-11-07. ### Enhancements - Accept multiple packages in `uv export` ([#16603](https://github.com/astral-sh/uv/pull/16603)) - Accept multiple packages in `uv sync` ([#16543](https://github.com/astral-sh/uv/pull/16543)) - Add a `uv cache size` command ([#16032](https://github.com/astral-sh/uv/pull/16032)) - Add prerelease guidance for build-system resolution failures ([#16550](https://github.com/astral-sh/uv/pull/16550)) - Allow Python requests to include `+gil` to require a GIL-enabled interpreter ([#16537](https://github.com/astral-sh/uv/pull/16537)) - Avoid pluralizing 'retry' for single value ([#16535](https://github.com/astral-sh/uv/pull/16535)) - Enable first-class dependency exclusions ([#16528](https://github.com/astral-sh/uv/pull/16528)) - Fix inclusive constraints on available package versions in resolver errors ([#16629](https://github.com/astral-sh/uv/pull/16629)) - Improve `uv init` error for invalid directory names ([#16554](https://github.com/astral-sh/uv/pull/16554)) - Show help on `uv build -h` ([#16632](https://github.com/astral-sh/uv/pull/16632)) - Include the Python variant suffix in "Using Python ..." messages ([#16536](https://github.com/astral-sh/uv/pull/16536)) - Log most recently modified file for cache-keys ([#16338](https://github.com/astral-sh/uv/pull/16338)) - Update Docker builds to use nightly Rust toolchain with musl v1.2.5 ([#16584](https://github.com/astral-sh/uv/pull/16584)) - Add GitHub attestations for uv release artifacts ([#11357](https://github.com/astral-sh/uv/pull/11357)) ### Configuration - Expose `UV_NO_GROUP` as an environment variable ([#16529](https://github.com/astral-sh/uv/pull/16529)) - Add `UV_NO_SOURCES` as an environment variable ([#15883](https://github.com/astral-sh/uv/pull/15883)) ### Bug fixes - Allow `--check` and `--locked` to be used together in `uv lock` ([#16538](https://github.com/astral-sh/uv/pull/16538)) - Allow for unnormalized names in the METADATA file (#16547) ([#16548](https://github.com/astral-sh/uv/pull/16548)) - Fix missing value_type for `default-groups` in schema ([#16575](https://github.com/astral-sh/uv/pull/16575)) - Respect multi-GPU outputs in `nvidia-smi` ([#15460](https://github.com/astral-sh/uv/pull/15460)) - Fix DNS lookup errors in Docker containers ([#8450](https://github.com/astral-sh/uv/issues/8450)) ### Documentation - Fix typo in uv tool list doc ([#16625](https://github.com/astral-sh/uv/pull/16625)) - Note `uv pip list` name normalization in docs ([#13210](https://github.com/astral-sh/uv/pull/13210)) ### Other changes - Update Rust toolchain to 1.91 and MSRV to 1.89 ([#16531](https://github.com/astral-sh/uv/pull/16531)) ## 0.9.7 Released on 2025-10-30. ### Enhancements - Add Windows x86-32 emulation support to interpreter architecture checks ([#13475](https://github.com/astral-sh/uv/pull/13475)) - Improve readability of progress bars ([#16509](https://github.com/astral-sh/uv/pull/16509)) ### Bug fixes - Drop terminal coloring from `uv auth token` output ([#16504](https://github.com/astral-sh/uv/pull/16504)) - Don't use UV_LOCKED to enable `--check` flag ([#16521](https://github.com/astral-sh/uv/pull/16521)) ## 0.9.6 Released on 2025-10-29. This release contains an upgrade to Astral's fork of `async_zip`, which addresses potential sources of ZIP parsing differentials between uv and other Python packaging tooling. See [GHSA-pqhf-p39g-3x64](https://github.com/astral-sh/uv/security/advisories/GHSA-pqhf-p39g-3x64) for additional details. ### Security * Address ZIP parsing differentials ([GHSA-pqhf-p39g-3x64](https://github.com/astral-sh/uv/security/advisories/GHSA-pqhf-p39g-3x64)) ### Python - Upgrade GraalPy to 25.0.1 ([#16401](https://github.com/astral-sh/uv/pull/16401)) ### Enhancements - Add `--clear` to `uv build` to remove old build artifacts ([#16371](https://github.com/astral-sh/uv/pull/16371)) - Add `--no-create-gitignore` to `uv build` ([#16369](https://github.com/astral-sh/uv/pull/16369)) - Do not error when a virtual environment directory cannot be removed due to a busy error ([#16394](https://github.com/astral-sh/uv/pull/16394)) - Improve hint on `pip install --system` when externally managed ([#16392](https://github.com/astral-sh/uv/pull/16392)) - Running `uv lock --check` with outdated lockfile will print that `--check` was passed, instead of `--locked` ([#16322](https://github.com/astral-sh/uv/pull/16322)) - Update `uv init` template for Maturin ([#16449](https://github.com/astral-sh/uv/pull/16449)) - Improve ordering of Python sources in logs ([#16463](https://github.com/astral-sh/uv/pull/16463)) - Restore DockerHub release images and annotations ([#16441](https://github.com/astral-sh/uv/pull/16441)) ### Bug fixes - Check for matching Python implementation during `uv python upgrade` ([#16420](https://github.com/astral-sh/uv/pull/16420)) - Deterministically order `--find-links` distributions ([#16446](https://github.com/astral-sh/uv/pull/16446)) - Don't panic in `uv export --frozen` when the lockfile is outdated ([#16407](https://github.com/astral-sh/uv/pull/16407)) - Fix root of `uv tree` when `--package` is used with circular dependencies ([#15908](https://github.com/astral-sh/uv/pull/15908)) - Show package list with `pip freeze --quiet` ([#16491](https://github.com/astral-sh/uv/pull/16491)) - Limit `uv auth login pyx.dev` retries to 60s ([#16498](https://github.com/astral-sh/uv/pull/16498)) - Add an empty group with `uv add --group ... -r ...` ([#16490](https://github.com/astral-sh/uv/pull/16490)) ### Documentation - Update docs for maturin build backend init template ([#16469](https://github.com/astral-sh/uv/pull/16469)) - Update docs to reflect previous changes to signal forwarding semantics ([#16430](https://github.com/astral-sh/uv/pull/16430)) - Add instructions for installing via MacPorts ([#16039](https://github.com/astral-sh/uv/pull/16039)) ## 0.9.5 Released on 2025-10-21. This release contains an upgrade to `astral-tokio-tar`, which addresses a vulnerability in tar extraction on malformed archives with mismatching size information between the ustar header and PAX extensions. While the `astral-tokio-tar` advisory has been graded as "high" due its potential broader impact, the *specific* impact to uv is **low** due to a lack of novel attacker capability. Specifically, uv only processes tar archives from source distributions, which already possess the capability for full arbitrary code execution by design, meaning that an attacker gains no additional capabilities through `astral-tokio-tar`. Regardless, we take the hypothetical risk of parser differentials very seriously. Out of an abundance of caution, we have assigned this upgrade an advisory: https://github.com/astral-sh/uv/security/advisories/GHSA-w476-p2h3-79g9 ### Security * Upgrade `astral-tokio-tar` to 0.5.6 to address a parsing differential ([#16387](https://github.com/astral-sh/uv/pull/16387)) ### Enhancements - Add required environment marker example to hint ([#16244](https://github.com/astral-sh/uv/pull/16244)) - Fix typo in MissingTopLevel warning ([#16351](https://github.com/astral-sh/uv/pull/16351)) - Improve 403 Forbidden error message to indicate package may not exist ([#16353](https://github.com/astral-sh/uv/pull/16353)) - Add a hint on `uv pip install` failure if the `--system` flag is used to select an externally managed interpreter ([#16318](https://github.com/astral-sh/uv/pull/16318)) ### Bug fixes - Fix backtick escaping for PowerShell ([#16307](https://github.com/astral-sh/uv/pull/16307)) ### Documentation - Document metadata consistency expectation ([#15683](https://github.com/astral-sh/uv/pull/15683)) - Remove outdated aarch64 musl note ([#16385](https://github.com/astral-sh/uv/pull/16385)) ## 0.9.4 Released on 2025-10-17. ### Enhancements - Add CUDA 13.0 support ([#16321](https://github.com/astral-sh/uv/pull/16321)) - Add auto-detection for Intel GPU on Windows ([#16280](https://github.com/astral-sh/uv/pull/16280)) - Implement display of RFC 9457 HTTP error contexts ([#16199](https://github.com/astral-sh/uv/pull/16199)) ### Bug fixes - Avoid obfuscating pyx tokens in `uv auth token` output ([#16345](https://github.com/astral-sh/uv/pull/16345)) ## 0.9.3 Released on 2025-10-14. ### Python - Add CPython 3.15.0a1 - Add CPython 3.13.9 ### Enhancements - Obfuscate secret token values in logs ([#16164](https://github.com/astral-sh/uv/pull/16164)) ### Bug fixes - Fix workspace with relative pathing ([#16296](https://github.com/astral-sh/uv/pull/16296)) ## 0.9.2 Released on 2025-10-10. ### Python - Add CPython 3.9.24. - Add CPython 3.10.19. - Add CPython 3.11.14. - Add CPython 3.12.12. ### Enhancements - Avoid inferring check URLs for pyx in `uv publish` ([#16234](https://github.com/astral-sh/uv/pull/16234)) - Add `uv tool list --show-python` ([#15814](https://github.com/astral-sh/uv/pull/15814)) ### Documentation - Add missing "added in" to new environment variables in reference ([#16217](https://github.com/astral-sh/uv/pull/16217)) ## 0.9.1 Released on 2025-10-09. ### Enhancements - Log Python choice in `uv init` ([#16182](https://github.com/astral-sh/uv/pull/16182)) - Fix `pylock.toml` config conflict error messages ([#16211](https://github.com/astral-sh/uv/pull/16211)) ### Configuration - Add `UV_UPLOAD_HTTP_TIMEOUT` and respect `UV_HTTP_TIMEOUT` in uploads ([#16040](https://github.com/astral-sh/uv/pull/16040)) - Support `UV_WORKING_DIRECTORY` for setting `--directory` ([#16125](https://github.com/astral-sh/uv/pull/16125)) ### Bug fixes - Allow missing `Scripts` directory ([#16206](https://github.com/astral-sh/uv/pull/16206)) - Fix handling of Python requests with pre-releases in ranges ([#16208](https://github.com/astral-sh/uv/pull/16208)) - Preserve comments on version bump ([#16141](https://github.com/astral-sh/uv/pull/16141)) - Retry all HTTP/2 errors ([#16038](https://github.com/astral-sh/uv/pull/16038)) - Treat deleted Windows registry keys as equivalent to missing ones ([#16194](https://github.com/astral-sh/uv/pull/16194)) - Ignore pre-release Python versions when a patch version is requested ([#16210](https://github.com/astral-sh/uv/pull/16210)) ### Documentation - Document why uv discards upper bounds on `requires-python` ([#15927](https://github.com/astral-sh/uv/pull/15927)) - Document uv version environment variables were added in ([#15196](https://github.com/astral-sh/uv/pull/15196)) ## 0.9.0 Released on 2025-10-07. This breaking release is primarily motivated by the release of Python 3.14, which contains some breaking changes (we recommend reading the ["What's new in Python 3.14"](https://docs.python.org/3/whatsnew/3.14.html) page). uv may use Python 3.14 in cases where it previously used 3.13, e.g., if you have not pinned your Python version and do not have any Python versions installed on your machine. While we think this is uncommon, we prefer to be cautious. We've included some additional small changes that could break workflows. See our [Python 3.14](https://astral.sh/blog/python-3.14) blog post for some discussion of features we're excited about! There are no breaking changes to [`uv_build`](https://docs.astral.sh/uv/concepts/build-backend/). If you have an upper bound in your `[build-system]` table, you should update it. ### Breaking changes - **Python 3.14 is now the default stable version** The default Python version has changed from 3.13 to 3.14. This applies to Python version installation when no Python version is requested, e.g., `uv python install`. By default, uv will use the system Python version if present, so this may not cause changes to general use of uv. For example, if Python 3.13 is installed already, then `uv venv` will use that version. If no Python versions are installed on a machine and automatic downloads are enabled, uv will now use 3.14 instead of 3.13, e.g., for `uv venv` or `uvx python`. This change will not affect users who are using a `.python-version` file to pin to a specific Python version. - **Allow use of free-threaded variants in Python 3.14+ without explicit opt-in** ([#16142](https://github.com/astral-sh/uv/pull/16142)) Previously, free-threaded variants of Python were considered experimental and required explicit opt-in (i.e., with `3.14t`) for usage. Now uv will allow use of free-threaded Python 3.14+ interpreters without explicit selection. The GIL-enabled build of Python will still be preferred, e.g., when performing an installation with `uv python install 3.14`. However, e.g., if a free-threaded interpreter comes before a GIL-enabled build on the `PATH`, it will be used. This change does not apply to free-threaded Python 3.13 interpreters, which will continue to require opt-in. - **Use Python 3.14 stable Docker images** ([#16150](https://github.com/astral-sh/uv/pull/16150)) Previously, the Python 3.14 images had an `-rc` suffix, e.g., `python:3.14-rc-alpine` or `python:3.14-rc-trixie`. Now, the `-rc` suffix has been removed to match the stable [upstream images](https://hub.docker.com/_/python). The `-rc` images tags will no longer be updated. This change should not break existing workflows. - **Upgrade Alpine Docker image to Alpine 3.22** Previously, the `uv:alpine` Docker image was based on Alpine 3.21. Now, this image is based on Alpine 3.22. The previous image can be recovered with `uv:alpine3.21` and will continue to be updated until a future release. - **Upgrade Debian Docker images to Debian 13 "Trixie"** Previously, the `uv:debian` and `uv:debian-slim` Docker images were based on Debian 12 "Bookworm". Now, these images are based on Debian 13 "Trixie". The previous images can be recovered with `uv:bookworm` and `uv:bookworm-slim` and will continue to be updated until a future release. - **Fix incorrect output path when a trailing `/` is used in `uv build`** ([#15133](https://github.com/astral-sh/uv/pull/15133)) When using `uv build` in a workspace, the artifacts are intended to be written to a `dist` directory in the workspace root. A bug caused workspace root determination to fail when the input path included a trailing `/` causing the `dist` directory to be placed in the child directory. This bug has been fixed in this release. For example, `uv build child/` is used, the output path will now be in `/dist/` rather than `/child/dist/`. ### Python - Add CPython 3.14.0 - Add CPython 3.13.8 ### Enhancements - Don't warn when a dependency is constrained by another dependency ([#16149](https://github.com/astral-sh/uv/pull/16149)) ### Bug fixes - Fix `uv python upgrade / install` output when there is a no-op for one request ([#16158](https://github.com/astral-sh/uv/pull/16158)) - Surface pinned-version hint when `uv tool upgrade` can’t move the tool ([#16081](https://github.com/astral-sh/uv/pull/16081)) - Ban pre-release versions in `uv python upgrade` requests ([#16160](https://github.com/astral-sh/uv/pull/16160)) - Fix `uv python upgrade` replacement of installed binaries on pre-release to stable ([#16159](https://github.com/astral-sh/uv/pull/16159)) ### Documentation - Update `uv pip compile` args in `layout.md` ([#16155](https://github.com/astral-sh/uv/pull/16155)) ## 0.8.x See [changelogs/0.8.x](./changelogs/0.8.x.md) ## 0.7.x See [changelogs/0.7.x](./changelogs/0.7.x.md) ## 0.6.x See [changelogs/0.6.x](./changelogs/0.6.x.md) ## 0.5.x See [changelogs/0.5.x](./changelogs/0.5.x.md) ## 0.4.x See [changelogs/0.4.x](./changelogs/0.4.x.md) ## 0.3.x See [changelogs/0.3.x](./changelogs/0.3.x.md) ## 0.2.x See [changelogs/0.2.x](./changelogs/0.2.x.md) ## 0.1.x See [changelogs/0.1.x](./changelogs/0.1.x.md) uv-0.9.17+ds1/CONTRIBUTING.md000066400000000000000000000202521520155276700151710ustar00rootroot00000000000000# Contributing ## Finding ways to help We label issues that would be good for a first time contributor as [`good first issue`](https://github.com/astral-sh/uv/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). These usually do not require significant experience with Rust or the uv code base. We label issues that we think are a good opportunity for subsequent contributions as [`help wanted`](https://github.com/astral-sh/uv/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22). These require varying levels of experience with Rust and uv. Often, we want to accomplish these tasks but do not have the resources to do so ourselves. You don't need our permission to start on an issue we have labeled as appropriate for community contribution as described above. However, it's a good idea to indicate that you are going to work on an issue to avoid concurrent attempts to solve the same problem. Please check in with us before starting work on an issue that has not been labeled as appropriate for community contribution. We're happy to receive contributions for other issues, but it's important to make sure we have consensus on the solution to the problem first. Outside of issues with the labels above, issues labeled as [`bug`](https://github.com/astral-sh/uv/issues?q=is%3Aopen+is%3Aissue+label%3A%22bug%22) are the best candidates for contribution. In contrast, issues labeled with `needs-decision` or `needs-design` are _not_ good candidates for contribution. Please do not open pull requests for issues with these labels. Please do not open pull requests for new features without prior discussion. While we appreciate exploration of new features, we will almost always close these pull requests immediately. Adding a new feature to uv creates a long-term maintenance burden and requires strong consensus from the uv team before it is appropriate to begin work on an implementation. ## Setup [Rust](https://rustup.rs/) (and a C compiler) are required to build uv. On Ubuntu and other Debian-based distributions, you can install a C compiler with: ```shell sudo apt install build-essential ``` On Fedora-based distributions, you can install a C compiler with: ```shell sudo dnf install gcc ``` ## Testing For running tests, we recommend [nextest](https://nexte.st/). If tests fail due to a mismatch in the JSON Schema, run: `cargo dev generate-json-schema`. ### Python Testing uv requires multiple specific Python versions; they can be installed with: ```shell cargo run python install ``` The storage directory can be configured with `UV_PYTHON_INSTALL_DIR`. (It must be an absolute path.) ### Snapshot testing uv uses [insta](https://insta.rs/) for snapshot testing. It's recommended (but not necessary) to use `cargo-insta` for a better snapshot review experience. See the [installation guide](https://insta.rs/docs/cli/) for more information. In tests, you can use `uv_snapshot!` macro to simplify creating snapshots for uv commands. For example: ```rust #[test] fn test_add() { let context = TestContext::new("3.12"); uv_snapshot!(context.filters(), context.add().arg("requests"), @""); } ``` To run and review a specific snapshot test: ```shell cargo test --package --test -- -- --exact cargo insta review ``` ### Git and Git LFS A subset of uv tests require both [Git](https://git-scm.com) and [Git LFS](https://git-lfs.com/) to execute properly. These tests can be disabled by turning off either `git` or `git-lfs` uv features. ### Local testing You can invoke your development version of uv with `cargo run -- `. For example: ```shell cargo run -- venv cargo run -- pip install requests ``` ## Running inside a Docker container Source distributions can run arbitrary code on build and can make unwanted modifications to your system (["Someone's Been Messing With My Subnormals!" on Blogspot](https://moyix.blogspot.com/2022/09/someones-been-messing-with-my-subnormals.html), ["nvidia-pyindex" on PyPI](https://pypi.org/project/nvidia-pyindex/)), which can even occur when just resolving requirements. To prevent this, there's a Docker container you can run commands in: ```console $ docker build -t uv-builder -f crates/uv-dev/builder.dockerfile --load . # Build for musl to avoid glibc errors, might not be required with your OS version cargo build --target x86_64-unknown-linux-musl --profile profiling docker run --rm -it -v $(pwd):/app uv-builder /app/target/x86_64-unknown-linux-musl/profiling/uv-dev resolve-many --cache-dir /app/cache-docker /app/scripts/popular_packages/pypi_10k_most_dependents.txt ``` We recommend using this container if you don't trust the dependency tree of the package(s) you are trying to resolve or install. ## Profiling and Benchmarking Please refer to Ruff's [Profiling Guide](https://github.com/astral-sh/ruff/blob/main/CONTRIBUTING.md#profiling-projects), it applies to uv, too. We provide diverse sets of requirements for testing and benchmarking the resolver in `test/requirements` and for the installer in `test/requirements/compiled`. You can use `scripts/benchmark` to benchmark predefined workloads between uv versions and with other tools, e.g., from the `scripts/benchmark` directory: ```shell uv run resolver \ --uv-pip \ --poetry \ --benchmark \ resolve-cold \ ../test/requirements/trio.in ``` ### Analyzing concurrency You can use [tracing-durations-export](https://github.com/konstin/tracing-durations-export) to visualize parallel requests and find any spots where uv is CPU-bound. Example usage, with `uv` and `uv-dev` respectively: ```shell RUST_LOG=uv=info TRACING_DURATIONS_FILE=target/traces/jupyter.ndjson cargo run --features tracing-durations-export --profile profiling -- pip compile test/requirements/jupyter.in ``` ```shell RUST_LOG=uv=info TRACING_DURATIONS_FILE=target/traces/jupyter.ndjson cargo run --features tracing-durations-export --bin uv-dev --profile profiling -- resolve jupyter ``` ### Trace-level logging You can enable `trace` level logging using the `RUST_LOG` environment variable, i.e. ```shell RUST_LOG=trace uv ``` ## Documentation To preview any changes to the documentation locally: 1. Install the [Rust toolchain](https://www.rust-lang.org/tools/install). 2. Run `cargo dev generate-all`, to update any auto-generated documentation. 3. Run the development server with: ```shell # For contributors. uvx --with-requirements docs/requirements.txt -- mkdocs serve -f mkdocs.public.yml # For members of the Astral org, which has access to MkDocs Insiders via sponsorship. uvx --with-requirements docs/requirements-insiders.txt -- mkdocs serve -f mkdocs.insiders.yml ``` The documentation should then be available locally at [http://127.0.0.1:8000/uv/](http://127.0.0.1:8000/uv/). To update the documentation dependencies, edit `docs/requirements.in` and `docs/requirements-insiders.in`, then run: ```shell uv pip compile docs/requirements.in -o docs/requirements.txt --universal -p 3.12 uv pip compile docs/requirements-insiders.in -o docs/requirements-insiders.txt --universal -p 3.12 ``` Documentation is deployed automatically on release by publishing to the [Astral documentation](https://github.com/astral-sh/docs) repository, which itself deploys via Cloudflare Pages. After making changes to the documentation, format the markdown files with: ```shell npx prettier --prose-wrap always --write "**/*.md" ``` Note that the command above requires Node.js and npm to be installed on your system. As an alternative, you can run this command using Docker: ```console $ docker run --rm -v .:/src/ -w /src/ node:alpine npx prettier --prose-wrap always --write "**/*.md" ``` ## Releases Releases can only be performed by Astral team members. Changelog entries and version bumps are automated. First, run: ```shell ./scripts/release.sh ``` Then, editorialize the `CHANGELOG.md` file to ensure entries are consistently styled. Then, open a pull request, e.g., `Bump version to ...`. Binary builds will automatically be tested for the release. After merging the pull request, run the [release workflow](https://github.com/astral-sh/uv/actions/workflows/release.yml) with the version tag. **Do not include a leading `v`**. The release will automatically be created on GitHub after everything else publishes. uv-0.9.17+ds1/Cargo.lock000066400000000000000000005414341520155276700146570ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", "cpufeatures", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "ambient-id" version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8cad022ed72ad2176498be1c097bb46e598193e92f3491ea0766980edeee168" dependencies = [ "astral-reqwest-middleware", "reqwest", "secrecy", "serde", "serde_json", "thiserror 2.0.17", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.59.0", ] [[package]] name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "approx" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" dependencies = [ "num-traits", ] [[package]] name = "arbitrary" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" dependencies = [ "derive_arbitrary", ] [[package]] name = "arcstr" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" [[package]] name = "arrayref" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "assert-json-diff" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" dependencies = [ "serde", "serde_json", ] [[package]] name = "assert_cmd" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" dependencies = [ "anstyle", "bstr", "doc-comment", "libc", "predicates", "predicates-core", "predicates-tree", "wait-timeout", ] [[package]] name = "assert_fs" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a652f6cb1f516886fcfee5e7a5c078b9ade62cfcb889524efe5a64d682dd27a9" dependencies = [ "anstyle", "doc-comment", "globwalk", "predicates", "predicates-core", "predicates-tree", "tempfile", ] [[package]] name = "astral-pubgrub" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6cb15b4f5096a3a1b41fdc2736a1c33d87c78f34d3c1ec2b669e766edadd559" dependencies = [ "astral-version-ranges", "indexmap", "log", "priority-queue", "rustc-hash", "thiserror 2.0.17", ] [[package]] name = "astral-reqwest-middleware" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "638d02e24aeb92f9537897cd1ff82e2bc98fd9ac9575a503e27bb07cdf64d4d7" dependencies = [ "anyhow", "async-trait", "http", "reqwest", "serde", "thiserror 2.0.17", "tower-service", ] [[package]] name = "astral-reqwest-retry" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7549bd00f62f73f2e7e76f3f77ccdabb31873f4f02f758ed88ad739d522867" dependencies = [ "anyhow", "astral-reqwest-middleware", "async-trait", "futures", "getrandom 0.2.16", "http", "hyper", "reqwest", "retry-policies", "thiserror 2.0.17", "tokio", "tracing", "wasmtimer", ] [[package]] name = "astral-tl" version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d90933ffb0f97e2fc2e0de21da9d3f20597b804012d199843a6fe7c2810d28f3" dependencies = [ "memchr", ] [[package]] name = "astral-tokio-tar" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5" dependencies = [ "filetime", "futures-core", "libc", "portable-atomic", "rustc-hash", "tokio", "tokio-stream", "xattr", ] [[package]] name = "astral-version-ranges" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7adc2308a566fab9de02bc0e05d18c5a21cb0e793684e4f64c8eb956969b074" dependencies = [ "smallvec", ] [[package]] name = "astral_async_http_range_reader" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ddaca0fbbf0d91103cca7c7611790c65f6eff1d456f7fe6bf565d436dc9b8f3" dependencies = [ "astral-reqwest-middleware", "bisection", "futures", "http-content-range", "itertools 0.13.0", "memmap2 0.9.7", "reqwest", "thiserror 1.0.69", "tokio", "tokio-stream", "tokio-util", "tracing", ] [[package]] name = "astral_async_zip" version = "0.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab72a761e6085828cc8f0e05ed332b2554701368c5dc54de551bfaec466518ba" dependencies = [ "async-compression", "crc32fast", "futures-lite", "pin-project", "thiserror 1.0.69", "tokio", "tokio-util", ] [[package]] name = "async-broadcast" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-channel" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-compression" version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06575e6a9673580f52661c92107baabffbf41e2141373441cbcdc47cb733003c" dependencies = [ "bzip2", "flate2", "futures-core", "futures-io", "memchr", "pin-project-lite", "tokio", "xz2", "zstd", "zstd-safe", ] [[package]] name = "async-recursion" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axoasset" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56b3b6c5d71b918c0f42f43f69b303d7529b4233a598d9d61759d75f0f2a44a2" dependencies = [ "camino", "image", "lazy_static", "miette", "mime", "reqwest", "serde", "serde_json", "thiserror 2.0.17", "url", "walkdir", ] [[package]] name = "axoprocess" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a4b4798a6c02e91378537c63cd6e91726900b595450daa5d487bc3c11e95e1b" dependencies = [ "miette", "thiserror 2.0.17", "tracing", ] [[package]] name = "axotag" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc923121fbc4cc72e9008436b5650b98e56f94b5799df59a1b4f572b5c6a7e6b" dependencies = [ "miette", "semver", "thiserror 2.0.17", ] [[package]] name = "axoupdater" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc482a1926df098f4e3806b834f3fe73a1ab54b24ab0ac481f72de479af5e982" dependencies = [ "axoasset", "axoprocess", "axotag", "camino", "homedir", "miette", "self-replace", "serde", "tempfile", "thiserror 2.0.17", "tokio", "url", ] [[package]] name = "backon" version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" dependencies = [ "fastrand", "gloo-timers", "tokio", ] [[package]] name = "backtrace" version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", ] [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bisection" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e079a1bab0ecce6cf4b4b74c0c37afa4a697136eb3b127875c84a8f04a8c3" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "blake2" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ "digest", ] [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "block-padding" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ "generic-array", ] [[package]] name = "block2" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ "objc2", ] [[package]] name = "boxcar" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" [[package]] name = "bstr" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytecheck" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" dependencies = [ "bytecheck_derive", "ptr_meta", "rancor", "simdutf8", ] [[package]] name = "bytecheck_derive" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "bytemuck" version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder-lite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bzip2" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" dependencies = [ "bzip2-sys", ] [[package]] name = "bzip2-sys" version = "0.1.13+1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" dependencies = [ "cc", "pkg-config", ] [[package]] name = "camino" version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" dependencies = [ "serde", ] [[package]] name = "cargo-util" version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f97c9ef0f8af69bfcecfe4c17a414d7bb978fe794bc1a38952e27b5c5d87492d" dependencies = [ "anyhow", "core-foundation 0.10.1", "filetime", "hex", "ignore", "jobserver", "libc", "miow", "same-file", "sha2", "shell-escape", "tempfile", "tracing", "walkdir", "windows-sys 0.60.2", ] [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cbc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ "cipher", ] [[package]] name = "cc" version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "jobserver", "libc", "shlex", ] [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "charset" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e" dependencies = [ "base64 0.22.1", "encoding_rs", ] [[package]] name = "ciborium" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", ] [[package]] name = "clap" version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size", ] [[package]] name = "clap_complete" version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a" dependencies = [ "clap", ] [[package]] name = "clap_complete_command" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da8e198c052315686d36371e8a3c5778b7852fc75cc313e4e11eeb7a644a1b62" dependencies = [ "clap", "clap_complete", "clap_complete_nushell", ] [[package]] name = "clap_complete_nushell" version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a0c951694691e65bf9d421d597d68416c22de9632e884c28412cb8cd8b73dce" dependencies = [ "clap", "clap_complete", ] [[package]] name = "clap_derive" version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "codspeed" version = "4.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe68fdd3fe25bc26de0230718d74eb150f09f70be3141c61ea7f9b00812054aa" dependencies = [ "anyhow", "cc", "colored", "glob", "libc", "nix", "serde", "serde_json", "statrs", "uuid", ] [[package]] name = "codspeed-criterion-compat" version = "4.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a06533c3c2c7b43b2efcca2dc0f228097b4c582c95f59cc187bbeb926b42cfb" dependencies = [ "clap", "codspeed", "codspeed-criterion-compat-walltime", "colored", "futures", "regex", "tokio", ] [[package]] name = "codspeed-criterion-compat-walltime" version = "4.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4102bd4dcfb09fae5cf5ca86c6c5ad0bd0cabe834cd84b5e7b7dd969fb9093e8" dependencies = [ "anes", "cast", "ciborium", "clap", "codspeed", "criterion-plot", "futures", "is-terminal", "itertools 0.10.5", "num-traits", "once_cell", "oorandom", "regex", "serde", "serde_derive", "serde_json", "tinytemplate", "tokio", "walkdir", ] [[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "colored" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", "windows-sys 0.59.0", ] [[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "configparser" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b" [[package]] name = "console" version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ "encode_unicode", "libc", "once_cell", "windows-sys 0.59.0", ] [[package]] name = "console" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" dependencies = [ "encode_unicode", "libc", "once_cell", "unicode-width 0.2.2", "windows-sys 0.61.0", ] [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const-random" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" dependencies = [ "const-random-macro", ] [[package]] name = "const-random-macro" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ "getrandom 0.2.16", "once_cell", "tiny-keccak", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ "crc-catalog", ] [[package]] name = "crc-catalog" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "criterion-plot" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools 0.10.5", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "csv" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ "csv-core", "itoa", "ryu", "serde", ] [[package]] name = "csv-core" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" dependencies = [ "memchr", ] [[package]] name = "ctrlc" version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" dependencies = [ "dispatch2", "nix", "windows-sys 0.61.0", ] [[package]] name = "cyclonedx-bom" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce2ec98a191e17f63b92b132f6852462de9eaee03ca8dbf2df401b9fd809bcac" dependencies = [ "base64 0.21.7", "cyclonedx-bom-macros", "fluent-uri", "indexmap", "once_cell", "ordered-float", "purl", "regex", "serde", "serde_json", "spdx 0.10.9", "strum", "thiserror 1.0.69", "time", "uuid", "xml-rs", ] [[package]] name = "cyclonedx-bom-macros" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c50341f21df64b412b4f917e34b7aa786c092d64f3f905f478cb76950c7e980c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "dashmap" version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", ] [[package]] name = "data-encoding" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "data-url" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" [[package]] name = "deadpool" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" dependencies = [ "deadpool-runtime", "lazy_static", "num_cpus", "tokio", ] [[package]] name = "deadpool-runtime" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" [[package]] name = "deranged" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] [[package]] name = "derive_arbitrary" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "const-oid", "crypto-common", "subtle", ] [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys 0.61.0", ] [[package]] name = "dispatch2" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.9.4", "block2", "libc", "objc2", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "dlv-list" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" dependencies = [ "const-random", ] [[package]] name = "doc-comment" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" [[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "dyn-clone" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "embed-manifest" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94cdc65b1cf9e871453ce2f86f5aaec24ff2eaa36a1fa3e02e441dddc3613b99" [[package]] name = "encode_unicode" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "encoding_rs_io" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" dependencies = [ "encoding_rs", ] [[package]] name = "endi" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] name = "enumflags2" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", "serde", ] [[package]] name = "enumflags2_derive" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "env_filter" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", ] [[package]] name = "env_home" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" [[package]] name = "env_logger" version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" dependencies = [ "serde", "typeid", ] [[package]] name = "errno" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", "windows-sys 0.60.2", ] [[package]] name = "etcetera" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" dependencies = [ "cfg-if", "windows-sys 0.61.0", ] [[package]] name = "event-listener" version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] [[package]] name = "filetime" version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", "windows-sys 0.60.2", ] [[package]] name = "fixedbitset" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "libz-rs-sys", "miniz_oxide", ] [[package]] name = "float-cmp" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" [[package]] name = "float-cmp" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" dependencies = [ "num-traits", ] [[package]] name = "fluent-uri" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "fontconfig-parser" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" dependencies = [ "roxmltree 0.20.0", ] [[package]] name = "fontdb" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff20bef7942a72af07104346154a70a70b089c572e454b41bef6eb6cb10e9c06" dependencies = [ "fontconfig-parser", "log", "memmap2 0.5.10", "ttf-parser", ] [[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "fs-err" version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ad492b2cf1d89d568a43508ab24f98501fe03f2f31c01e1d0fe7366a71745d2" dependencies = [ "autocfg", "tokio", ] [[package]] name = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand", "futures-core", "futures-io", "parking", "pin-project-lite", ] [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] [[package]] name = "gif" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" dependencies = [ "color_quant", "weezl", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", "log", "regex-automata", "regex-syntax", ] [[package]] name = "globwalk" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ "bitflags 2.9.4", "ignore", "walkdir", ] [[package]] name = "gloo-timers" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ "futures-channel", "futures-core", "js-sys", "wasm-bindgen", ] [[package]] name = "goblin" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4db6758c546e6f81f265638c980e5e84dfbda80cfd8e89e02f83454c8e8124bd" dependencies = [ "log", "plain", "scroll", ] [[package]] name = "h2" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", "http", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "half" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", ] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash 0.1.5", ] [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", "foldhash 0.2.0", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hkdf" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "homedir" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68df315d2857b2d8d2898be54a85e1d001bbbe0dbb5f8ef847b48dd3a23c4527" dependencies = [ "cfg-if", "nix", "widestring", "windows 0.61.3", ] [[package]] name = "html-escape" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" dependencies = [ "utf8-width", ] [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", ] [[package]] name = "http-body-util" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", "http", "http-body", "pin-project-lite", ] [[package]] name = "http-content-range" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63f67baaf67a9ae8fae78ecee69294d552b764dbcd6f8735d0a9c9be20ab0c82" [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "h2", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", "pin-utils", "smallvec", "tokio", "want", ] [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http", "hyper", "hyper-util", "rustls", "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", "webpki-roots", ] [[package]] name = "hyper-util" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", "futures-core", "futures-util", "http", "http-body", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", "socket2 0.6.0", "system-configuration", "tokio", "tower-service", "tracing", "windows-registry", ] [[package]] name = "icu_collections" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "ignore" version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ "crossbeam-deque", "globset", "log", "memchr", "regex-automata", "same-file", "walkdir", "winapi-util", ] [[package]] name = "image" version = "0.25.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" dependencies = [ "bytemuck", "byteorder-lite", "num-traits", ] [[package]] name = "imagesize" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b72ad49b554c1728b1e83254a1b1565aea4161e28dabbfa171fc15fe62299caf" [[package]] name = "indexmap" version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown 0.16.1", "serde", "serde_core", ] [[package]] name = "indicatif" version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" dependencies = [ "console 0.16.1", "portable-atomic", "unicode-width 0.2.2", "unit-prefix", "web-time", ] [[package]] name = "indoc" version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" dependencies = [ "rustversion", ] [[package]] name = "inout" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", "generic-array", ] [[package]] name = "insta" version = "1.43.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" dependencies = [ "console 0.15.11", "once_cell", "pest", "pest_derive", "regex", "serde", "similar", ] [[package]] name = "io-uring" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ "bitflags 2.9.4", "cfg-if", "libc", ] [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" dependencies = [ "memchr", "serde", ] [[package]] name = "is-docker" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" dependencies = [ "once_cell", ] [[package]] name = "is-terminal" version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", "windows-sys 0.59.0", ] [[package]] name = "is-wsl" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" dependencies = [ "is-docker", "once_cell", ] [[package]] name = "is_ci" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde_core", "windows-sys 0.61.0", ] [[package]] name = "jiff-static" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "jiff-tzdb" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" [[package]] name = "jiff-tzdb-platform" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" dependencies = [ "jiff-tzdb", ] [[package]] name = "jobserver" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ "getrandom 0.3.3", "libc", ] [[package]] name = "jpeg-decoder" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" [[package]] name = "js-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "junction" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72bbdfd737a243da3dfc1f99ee8d6e166480f17ab4ac84d7c34aacd73fc7bd16" dependencies = [ "scopeguard", "windows-sys 0.52.0", ] [[package]] name = "kurbo" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449" dependencies = [ "arrayvec", ] [[package]] name = "kurbo" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" dependencies = [ "arrayvec", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libmimalloc-sys" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" dependencies = [ "cc", "libc", ] [[package]] name = "libredox" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" dependencies = [ "bitflags 2.9.4", "libc", "redox_syscall", ] [[package]] name = "libz-rs-sys" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" dependencies = [ "zlib-rs", ] [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "lzma-rs" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" dependencies = [ "byteorder", "crc", ] [[package]] name = "lzma-sys" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" dependencies = [ "cc", "libc", "pkg-config", ] [[package]] name = "mailparse" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60819a97ddcb831a5614eb3b0174f3620e793e97e09195a395bfa948fd68ed2f" dependencies = [ "charset", "data-encoding", "quoted_printable", ] [[package]] name = "markdown" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" dependencies = [ "unicode-id", ] [[package]] name = "matchers" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ "regex-automata", ] [[package]] name = "md-5" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", "digest", ] [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" dependencies = [ "libc", ] [[package]] name = "memmap2" version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" dependencies = [ "libc", ] [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "miette" version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" dependencies = [ "cfg-if", "miette-derive", "owo-colors", "supports-color", "supports-hyperlinks", "supports-unicode", "terminal_size", "textwrap", "unicode-width 0.1.14", ] [[package]] name = "miette-derive" version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "mimalloc" version = "0.1.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" dependencies = [ "libmimalloc-sys", ] [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", ] [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", ] [[package]] name = "mio" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] [[package]] name = "miow" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" dependencies = [ "windows-sys 0.61.0", ] [[package]] name = "munge" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cce144fab80fbb74ec5b89d1ca9d41ddf6b644ab7e986f7d3ed0aab31625cb1" dependencies = [ "munge_macro", ] [[package]] name = "munge_macro" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "574af9cd5b9971cbfdf535d6a8d533778481b241c447826d976101e0149392a1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "nanoid" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" dependencies = [ "rand 0.8.5", ] [[package]] name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.9.4", "cfg-if", "cfg_aliases", "libc", "memoffset", ] [[package]] name = "normalize-line-endings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "nu-ansi-term" version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "num" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", "num-complex", "num-integer", "num-iter", "num-rational", "num-traits", ] [[package]] name = "num-bigint" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", ] [[package]] name = "num-complex" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-iter" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-rational" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ "num-bigint", "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "objc2" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] [[package]] name = "objc2-encode" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "oorandom" version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "open" version = "5.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" dependencies = [ "is-wsl", "libc", "pathdiff", ] [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-float" version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" dependencies = [ "num-traits", ] [[package]] name = "ordered-multimap" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list", "hashbrown 0.14.5", ] [[package]] name = "ordered-stream" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "os_str_bytes" version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" dependencies = [ "memchr", ] [[package]] name = "owo-colors" version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "path-slash" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pem" version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64 0.22.1", "serde_core", ] [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", "thiserror 2.0.17", "ucd-trie", ] [[package]] name = "pest_derive" version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" dependencies = [ "pest", "pest_generator", ] [[package]] name = "pest_generator" version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", "syn", ] [[package]] name = "pest_meta" version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" dependencies = [ "pest", "sha2", ] [[package]] name = "petgraph" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", "indexmap", "serde", ] [[package]] name = "pico-args" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plain" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "png" version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "poloto" version = "19.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "164dbd541c9832e92fa34452e9c2e98b515a548a3f8549fb2402fe1cd5e46b96" dependencies = [ "tagu", ] [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" dependencies = [ "zerovec", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "predicates" version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", "float-cmp 0.10.0", "normalize-line-endings", "predicates-core", "regex", ] [[package]] name = "predicates-core" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", ] [[package]] name = "pretty_assertions" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", ] [[package]] name = "priority-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5676d703dda103cbb035b653a9f11448c0a7216c7926bd35fcb5865475d0c970" dependencies = [ "autocfg", "equivalent", "indexmap", ] [[package]] name = "proc-macro-crate" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit 0.22.27", ] [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "procfs" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" dependencies = [ "bitflags 2.9.4", "flate2", "hex", "procfs-core", "rustix 0.38.44", ] [[package]] name = "procfs-core" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ "bitflags 2.9.4", "hex", ] [[package]] name = "ptr_meta" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9e76f66d3f9606f44e45598d155cb13ecf09f4a28199e48daf8c8fc937ea90" dependencies = [ "ptr_meta_derive", ] [[package]] name = "ptr_meta_derive" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca414edb151b4c8d125c12566ab0d74dc9cdba36fb80eb7b848c15f495fd32d1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "purl" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60ebe4262ae91ddd28c8721111a0a6e9e58860e211fc92116c4bb85c98fd96ad" dependencies = [ "hex", "percent-encoding", "thiserror 2.0.17", ] [[package]] name = "quick-xml" version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", "serde", ] [[package]] name = "quinn" version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash", "rustls", "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", "web-time", ] [[package]] name = "quinn-proto" version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ "bytes", "getrandom 0.3.3", "lru-slab", "rand 0.9.2", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", "thiserror 2.0.17", "tinyvec", "tracing", "web-time", ] [[package]] name = "quinn-udp" version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2 0.5.10", "tracing", "windows-sys 0.59.0", ] [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "quoted_printable" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rancor" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf5f7161924b9d1cea0e4cabc97c372cea92b5f927fc13c6bca67157a0ad947" dependencies = [ "ptr_meta", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core 0.6.4", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core 0.9.3", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.16", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.3", ] [[package]] name = "rayon" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "rcgen" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fae430c6b28f1ad601274e78b7dffa0546de0b73b4cd32f46723c0c2a16f7a5" dependencies = [ "pem", "ring", "rustls-pki-types", "time", "yasna", ] [[package]] name = "rctree" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" [[package]] name = "redox_syscall" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" dependencies = [ "bitflags 2.9.4", ] [[package]] name = "redox_users" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.16", "libredox", "thiserror 2.0.17", ] [[package]] name = "ref-cast" version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "reflink-copy" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23bbed272e39c47a095a5242218a67412a220006842558b03fe2935e8f3d7b92" dependencies = [ "cfg-if", "libc", "rustix 1.0.8", "windows 0.61.3", ] [[package]] name = "regex" version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rend" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a35e8a6bf28cd121053a66aa2e6a2e3eaffad4a60012179f0e864aa5ffeff215" dependencies = [ "bytecheck", ] [[package]] name = "reqsign" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea386ba750000b6e59f760a08bdcca9461809b95e6f8f209ce5724056802824f" dependencies = [ "reqsign-aws-v4", "reqsign-command-execute-tokio", "reqsign-core", "reqsign-file-read-tokio", "reqsign-http-send-reqwest", ] [[package]] name = "reqsign-aws-v4" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4510c2a3e42b653cf788d560a3d54b0ae4cc315a62aaba773554f18319c0db0b" dependencies = [ "anyhow", "async-trait", "bytes", "form_urlencoded", "http", "log", "percent-encoding", "quick-xml", "reqsign-core", "rust-ini", "serde", "serde_json", "serde_urlencoded", "sha1", ] [[package]] name = "reqsign-command-execute-tokio" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b53d033600f533135afec8e97be99c80fcf8177f6285da6c7300955d5377a1" dependencies = [ "async-trait", "reqsign-core", "tokio", ] [[package]] name = "reqsign-core" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39da118ccf3bdb067ac6cc40136fec99bc5ba418cbd388dc88e4ce0e5d0b1423" dependencies = [ "anyhow", "async-trait", "base64 0.22.1", "bytes", "form_urlencoded", "hex", "hmac", "http", "jiff", "log", "percent-encoding", "sha1", "sha2", "windows-sys 0.61.0", ] [[package]] name = "reqsign-file-read-tokio" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "669ea66036266a9ac371d2e63cc7d345e69994da0168b4e6f3487fe21e126f76" dependencies = [ "anyhow", "async-trait", "reqsign-core", "tokio", ] [[package]] name = "reqsign-http-send-reqwest" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46186bce769674f9200ad01af6f2ca42de3e819ddc002fff1edae135bfb6cd9c" dependencies = [ "anyhow", "async-trait", "bytes", "futures-channel", "http", "http-body-util", "reqsign-core", "reqwest", "wasm-bindgen-futures", ] [[package]] name = "reqwest" version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "async-compression", "base64 0.22.1", "bytes", "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-util", "js-sys", "log", "mime_guess", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", "webpki-roots", ] [[package]] name = "resvg" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76888219c0881e22b0ceab06fddcfe83163cd81642bd60c7842387f9c968a72e" dependencies = [ "gif", "jpeg-decoder", "log", "pico-args", "png", "rgb", "svgfilters", "svgtypes 0.10.0", "tiny-skia", "usvg", "usvg-text-layout", ] [[package]] name = "retry-policies" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5875471e6cab2871bc150ecb8c727db5113c9338cc3354dc5ee3425b6aa40a1c" dependencies = [ "rand 0.8.5", ] [[package]] name = "rgb" version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" dependencies = [ "bytemuck", ] [[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rkyv" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35a640b26f007713818e9a9b65d34da1cf58538207b052916a83d80e43f3ffa4" dependencies = [ "bytecheck", "bytes", "hashbrown 0.15.5", "indexmap", "munge", "ptr_meta", "rancor", "rend", "rkyv_derive", "smallvec", "tinyvec", "uuid", ] [[package]] name = "rkyv_derive" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd83f5f173ff41e00337d97f6572e416d022ef8a19f371817259ae960324c482" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "rmp" version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ "byteorder", "num-traits", "paste", ] [[package]] name = "rmp-serde" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" dependencies = [ "byteorder", "rmp", "serde", ] [[package]] name = "rosvgtree" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdc23d1ace03d6b8153c7d16f0708cd80b61ee8e80304954803354e67e40d150" dependencies = [ "log", "roxmltree 0.18.1", "simplecss", "siphasher", "svgtypes 0.9.0", ] [[package]] name = "roxmltree" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "862340e351ce1b271a378ec53f304a5558f7db87f3769dc655a8f6ecbb68b302" dependencies = [ "xmlparser", ] [[package]] name = "roxmltree" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rust-ini" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" dependencies = [ "cfg-if", "ordered-multimap", ] [[package]] name = "rust-netrc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e98097f62769f92dbf95fb51f71c0a68ec18a4ee2e70e0d3e4f47ac005d63e9" dependencies = [ "shellexpand", "thiserror 1.0.69", ] [[package]] name = "rustc-demangle" version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] [[package]] name = "rustix" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.9.4", "windows-sys 0.60.2", ] [[package]] name = "rustls" version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", "security-framework", ] [[package]] name = "rustls-pki-types" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", "zeroize", ] [[package]] name = "rustls-webpki" version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "rustversion" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "rustybuzz" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162bdf42e261bee271b3957691018634488084ef577dddeb6420a9684cab2a6a" dependencies = [ "bitflags 1.3.2", "bytemuck", "smallvec", "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", "unicode-general-category", "unicode-script", ] [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "schannel" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "schemars" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1317c3bf3e7df961da95b0a56a172a02abead31276215a0497241a7624b487ce" dependencies = [ "dyn-clone", "ref-cast", "schemars_derive", "serde", "serde_json", "url", ] [[package]] name = "schemars_derive" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f760a6150d45dd66ec044983c124595ae76912e77ed0b44124cb3e415cce5d9" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", "syn", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scroll" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" dependencies = [ "scroll_derive", ] [[package]] name = "scroll_derive" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22fc4f90c27b57691bbaf11d8ecc7cfbfe98a4da6dbe60226115d322aa80c06e" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "seahash" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "secrecy" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" dependencies = [ "zeroize", ] [[package]] name = "secret-service" version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dccff79e916a339eec808de579764e3459658c903960d5aa4f7959ee9f6d5f2b" dependencies = [ "aes", "cbc", "futures-util", "generic-array", "getrandom 0.2.16", "hkdf", "num", "once_cell", "serde", "sha2", "zbus", ] [[package]] name = "security-framework" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags 2.9.4", "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "self-replace" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7" dependencies = [ "fastrand", "tempfile", "windows-sys 0.52.0", ] [[package]] name = "semver" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde-untagged" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" dependencies = [ "erased-serde", "serde", "serde_core", "typeid", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_derive_internals" version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", "serde_core", ] [[package]] name = "serde_repr" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_spanned" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ "serde_core", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ "indexmap", "itoa", "ryu", "serde", "unsafe-libyaml", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shell-escape" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" [[package]] name = "shellexpand" version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" dependencies = [ "bstr", "dirs", "os_str_bytes", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simdutf8" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "simplecss" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" dependencies = [ "log", ] [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smawk" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "socket2" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "spdx" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" dependencies = [ "smallvec", ] [[package]] name = "spdx" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cf87c0efffc158b9dde4d6e0567a43e4383adc4c949e687a2039732db2f23a" dependencies = [ "smallvec", ] [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "statrs" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" dependencies = [ "approx", "num-traits", ] [[package]] name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" dependencies = [ "float-cmp 0.9.0", ] [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", "syn", ] [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "supports-color" version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" dependencies = [ "is_ci", ] [[package]] name = "supports-hyperlinks" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" [[package]] name = "supports-unicode" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "svg" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94afda9cd163c04f6bee8b4bf2501c91548deae308373c436f36aeff3cf3c4a3" [[package]] name = "svgfilters" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "639abcebc15fdc2df179f37d6f5463d660c1c79cd552c12343a4600827a04bce" dependencies = [ "float-cmp 0.9.0", "rgb", ] [[package]] name = "svgtypes" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9ee29c1407a5b18ccfe5f6ac82ac11bab3b14407e09c209a6c1a32098b19734" dependencies = [ "kurbo 0.8.3", "siphasher", ] [[package]] name = "svgtypes" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ffacedcdcf1da6579c907279b4f3c5492fbce99fbbf227f5ed270a589c2765" dependencies = [ "kurbo 0.9.5", "siphasher", ] [[package]] name = "syn" version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "sys-info" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" dependencies = [ "cc", "libc", ] [[package]] name = "system-configuration" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.9.4", "core-foundation 0.9.4", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "tagu" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddb6b06d20fba9ed21fca3d696ee1b6e870bca0bcf9fa2971f6ae2436de576a" [[package]] name = "tar" version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" dependencies = [ "filetime", "libc", "xattr", ] [[package]] name = "target-lexicon" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "temp-env" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050" dependencies = [ "parking_lot", ] [[package]] name = "tempfile" version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix 1.0.8", "windows-sys 0.59.0", ] [[package]] name = "terminal_size" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ "rustix 1.0.8", "windows-sys 0.59.0", ] [[package]] name = "termtree" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "test-case" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" dependencies = [ "test-case-macros", ] [[package]] name = "test-case-core" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" dependencies = [ "cfg-if", "proc-macro2", "quote", "syn", ] [[package]] name = "test-case-macros" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", "syn", "test-case-core", ] [[package]] name = "test-log" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e33b98a582ea0be1168eba097538ee8dd4bbe0f2b01b22ac92ea30054e5be7b" dependencies = [ "test-log-macros", "tracing-subscriber", ] [[package]] name = "test-log-macros" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451b374529930d7601b1eef8d32bc79ae870b6079b069401709c2a8bf9e75f36" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "textwrap" version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", "unicode-width 0.2.2", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl 2.0.17", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thiserror-impl" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thread_local" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", ] [[package]] name = "tikv-jemalloc-sys" version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d" dependencies = [ "cc", "libc", ] [[package]] name = "tikv-jemallocator" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865" dependencies = [ "libc", "tikv-jemalloc-sys", ] [[package]] name = "time" version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tiny-keccak" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ "crunchy", ] [[package]] name = "tiny-skia" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8493a203431061e901613751931f047d1971337153f96d0e5e363d6dbf6a67" dependencies = [ "arrayref", "arrayvec", "bytemuck", "cfg-if", "png", "tiny-skia-path", ] [[package]] name = "tiny-skia-path" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adbfb5d3f3dd57a0e11d12f4f13d4ebbbc1b5c15b7ab0a156d030b21da5f677c" dependencies = [ "arrayref", "bytemuck", "strict-num", ] [[package]] name = "tinystr" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "tinyvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "slab", "socket2 0.6.0", "tokio-macros", "tracing", "windows-sys 0.59.0", ] [[package]] name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", "tokio", ] [[package]] name = "tokio-stream" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", "tokio", "tokio-util", ] [[package]] name = "tokio-util" version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "toml" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "foldhash 0.2.0", "indexmap", "serde_core", "serde_spanned", "toml_datetime 0.7.3", "toml_parser", "toml_writer", "winnow", ] [[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_datetime" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "toml_datetime 0.6.11", "winnow", ] [[package]] name = "toml_edit" version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", "serde_core", "serde_spanned", "toml_datetime 0.7.3", "toml_parser", "toml_writer", "winnow", ] [[package]] name = "toml_parser" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] [[package]] name = "toml_writer" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tower" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", "sync_wrapper", "tokio", "tower-layer", "tower-service", ] [[package]] name = "tower-http" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags 2.9.4", "bytes", "futures-util", "http", "http-body", "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", ] [[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-durations-export" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e0c2cfee378f62291f2703bbb949b99213306c2729fe977799653c3c3404b5" dependencies = [ "anyhow", "fs-err", "itertools 0.14.0", "once_cell", "rustc-hash", "serde", "serde_json", "svg", "tracing", "tracing-subscriber", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-serde" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" dependencies = [ "serde", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex-automata", "serde", "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", "tracing-serde", ] [[package]] name = "tracing-test" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" dependencies = [ "tracing-core", "tracing-subscriber", "tracing-test-macro", ] [[package]] name = "tracing-test-macro" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", "syn", ] [[package]] name = "tracing-tree" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac87aa03b6a4d5a7e4810d1a80c19601dbe0f8a837e9177f23af721c7ba7beec" dependencies = [ "nu-ansi-term", "tracing-core", "tracing-log", "tracing-subscriber", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ttf-parser" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0609f771ad9c6155384897e1df4d948e692667cc0588548b68eb44d052b27633" [[package]] name = "typeid" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "ucd-trie" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uds_windows" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ "memoffset", "tempfile", "winapi", ] [[package]] name = "unicase" version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi-mirroring" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" [[package]] name = "unicode-ccc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" [[package]] name = "unicode-general-category" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" [[package]] name = "unicode-id" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-linebreak" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-script" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" [[package]] name = "unicode-vo" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unit-prefix" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "unscanny" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", ] [[package]] name = "usvg" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b6bb4e62619d9f68aa2d8a823fea2bff302340a1f2d45c264d5b0be170832e" dependencies = [ "base64 0.21.7", "data-url", "flate2", "imagesize", "kurbo 0.9.5", "log", "rctree", "rosvgtree", "strict-num", ] [[package]] name = "usvg-text-layout" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "195386e01bc35f860db024de275a76e7a31afdf975d18beb6d0e44764118b4db" dependencies = [ "fontdb", "kurbo 0.9.5", "log", "rustybuzz", "unicode-bidi", "unicode-script", "unicode-vo", "usvg", ] [[package]] name = "utf8-width" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ "getrandom 0.3.3", "js-sys", "wasm-bindgen", ] [[package]] name = "uv" version = "0.9.17" dependencies = [ "anstream", "anyhow", "arrayvec", "assert_cmd", "assert_fs", "astral-version-ranges", "axoupdater", "backon", "base64 0.22.1", "byteorder", "clap", "console 0.16.1", "ctrlc", "dotenvy", "dunce", "embed-manifest", "filetime", "flate2", "fs-err", "futures", "h2", "http", "ignore", "indexmap", "indicatif", "indoc", "insta", "itertools 0.14.0", "miette", "nix", "open", "owo-colors", "petgraph", "predicates", "regex", "reqwest", "rkyv", "rustc-hash", "self-replace", "serde", "serde_json", "sha2", "similar", "tar", "tempfile", "textwrap", "thiserror 2.0.17", "tokio", "tokio-util", "toml", "toml_edit 0.23.7", "tracing", "tracing-durations-export", "tracing-subscriber", "tracing-tree", "unicode-width 0.2.2", "url", "uuid", "uv-auth", "uv-bin-install", "uv-build-backend", "uv-build-frontend", "uv-cache", "uv-cache-info", "uv-cache-key", "uv-cli", "uv-client", "uv-configuration", "uv-console", "uv-dispatch", "uv-distribution", "uv-distribution-filename", "uv-distribution-types", "uv-extract", "uv-flags", "uv-fs", "uv-git", "uv-git-types", "uv-install-wheel", "uv-installer", "uv-logging", "uv-normalize", "uv-pep440", "uv-pep508", "uv-performance-memory-allocator", "uv-platform", "uv-platform-tags", "uv-preview", "uv-publish", "uv-pypi-types", "uv-python", "uv-redacted", "uv-requirements", "uv-requirements-txt", "uv-resolver", "uv-scripts", "uv-settings", "uv-shell", "uv-static", "uv-tool", "uv-torch", "uv-trampoline-builder", "uv-types", "uv-version", "uv-virtualenv", "uv-warnings", "uv-workspace", "walkdir", "which", "whoami", "windows 0.59.0", "wiremock", "zip", ] [[package]] name = "uv-auth" version = "0.0.7" dependencies = [ "anyhow", "arcstr", "astral-reqwest-middleware", "async-trait", "base64 0.22.1", "etcetera", "fs-err", "futures", "http", "insta", "jiff", "percent-encoding", "reqsign", "reqwest", "rust-netrc", "rustc-hash", "schemars", "serde", "serde_json", "tempfile", "test-log", "thiserror 2.0.17", "tokio", "toml", "tracing", "url", "uv-cache-key", "uv-fs", "uv-keyring", "uv-once-map", "uv-preview", "uv-redacted", "uv-small-str", "uv-state", "uv-static", "uv-warnings", "wiremock", ] [[package]] name = "uv-bench" version = "0.0.7" dependencies = [ "anyhow", "codspeed-criterion-compat", "jiff", "tokio", "uv-cache", "uv-client", "uv-configuration", "uv-dispatch", "uv-distribution", "uv-distribution-types", "uv-extract", "uv-install-wheel", "uv-pep440", "uv-pep508", "uv-platform-tags", "uv-preview", "uv-pypi-types", "uv-python", "uv-resolver", "uv-types", "uv-workspace", ] [[package]] name = "uv-bin-install" version = "0.0.7" dependencies = [ "astral-reqwest-middleware", "astral-reqwest-retry", "fs-err", "futures", "reqwest", "tempfile", "thiserror 2.0.17", "tokio", "tokio-util", "tracing", "url", "uv-cache", "uv-client", "uv-distribution-filename", "uv-extract", "uv-fs", "uv-pep440", "uv-platform", "uv-redacted", ] [[package]] name = "uv-build" version = "0.9.17" dependencies = [ "anstream", "anyhow", "tracing-subscriber", "uv-build-backend", "uv-logging", "uv-version", ] [[package]] name = "uv-build-backend" version = "0.0.7" dependencies = [ "astral-version-ranges", "base64 0.22.1", "csv", "flate2", "fs-err", "globset", "indoc", "insta", "itertools 0.14.0", "regex", "rustc-hash", "schemars", "serde", "sha2", "spdx 0.12.0", "tar", "tempfile", "thiserror 2.0.17", "toml", "tracing", "uv-distribution-filename", "uv-fs", "uv-globfilter", "uv-macros", "uv-normalize", "uv-options-metadata", "uv-pep440", "uv-pep508", "uv-platform-tags", "uv-pypi-types", "uv-version", "uv-warnings", "walkdir", "zip", ] [[package]] name = "uv-build-frontend" version = "0.0.7" dependencies = [ "anstream", "fs-err", "indoc", "insta", "itertools 0.14.0", "owo-colors", "regex", "rustc-hash", "serde", "serde_json", "tempfile", "thiserror 2.0.17", "tokio", "toml_edit 0.23.7", "tracing", "uv-auth", "uv-cache-key", "uv-configuration", "uv-distribution", "uv-distribution-types", "uv-fs", "uv-normalize", "uv-pep440", "uv-pep508", "uv-preview", "uv-pypi-types", "uv-python", "uv-static", "uv-types", "uv-virtualenv", "uv-warnings", "uv-workspace", ] [[package]] name = "uv-cache" version = "0.0.7" dependencies = [ "clap", "fs-err", "nanoid", "rmp-serde", "rustc-hash", "same-file", "serde", "tempfile", "tracing", "uv-cache-info", "uv-cache-key", "uv-dirs", "uv-distribution-types", "uv-fs", "uv-normalize", "uv-pypi-types", "uv-redacted", "uv-static", "walkdir", ] [[package]] name = "uv-cache-info" version = "0.0.7" dependencies = [ "anyhow", "fs-err", "globwalk", "schemars", "serde", "tempfile", "thiserror 2.0.17", "toml", "tracing", "uv-fs", "walkdir", ] [[package]] name = "uv-cache-key" version = "0.0.7" dependencies = [ "hex", "memchr", "percent-encoding", "seahash", "url", "uv-redacted", ] [[package]] name = "uv-cli" version = "0.0.7" dependencies = [ "anstream", "anyhow", "clap", "clap_complete_command", "fs-err", "insta", "serde", "url", "uv-auth", "uv-cache", "uv-configuration", "uv-distribution-types", "uv-install-wheel", "uv-normalize", "uv-pep508", "uv-preview", "uv-pypi-types", "uv-python", "uv-redacted", "uv-resolver", "uv-settings", "uv-static", "uv-torch", "uv-version", "uv-warnings", "uv-workspace", ] [[package]] name = "uv-client" version = "0.0.7" dependencies = [ "anyhow", "astral-reqwest-middleware", "astral-reqwest-retry", "astral-tl", "astral_async_http_range_reader", "astral_async_zip", "async-trait", "bytecheck", "fs-err", "futures", "h2", "html-escape", "http", "http-body-util", "hyper", "hyper-util", "insta", "itertools 0.14.0", "jiff", "percent-encoding", "rcgen", "reqwest", "rkyv", "rmp-serde", "rustc-hash", "rustls", "serde", "serde_json", "sys-info", "tempfile", "thiserror 2.0.17", "tokio", "tokio-rustls", "tokio-util", "tracing", "url", "uv-auth", "uv-cache", "uv-cache-key", "uv-configuration", "uv-distribution-filename", "uv-distribution-types", "uv-fs", "uv-metadata", "uv-normalize", "uv-pep440", "uv-pep508", "uv-platform-tags", "uv-preview", "uv-pypi-types", "uv-redacted", "uv-small-str", "uv-static", "uv-torch", "uv-version", "uv-warnings", "wiremock", ] [[package]] name = "uv-configuration" version = "0.0.7" dependencies = [ "anyhow", "clap", "either", "fs-err", "rayon", "rustc-hash", "same-file", "schemars", "serde", "serde-untagged", "thiserror 2.0.17", "tracing", "url", "uv-auth", "uv-cache", "uv-cache-info", "uv-distribution-types", "uv-git", "uv-normalize", "uv-pep440", "uv-pep508", "uv-platform-tags", "uv-static", ] [[package]] name = "uv-console" version = "0.0.7" dependencies = [ "console 0.16.1", ] [[package]] name = "uv-dev" version = "0.0.7" dependencies = [ "anstream", "anyhow", "clap", "fs-err", "futures", "itertools 0.14.0", "markdown", "owo-colors", "poloto", "pretty_assertions", "reqwest", "resvg", "schemars", "serde", "serde_json", "serde_yaml", "tagu", "tempfile", "textwrap", "tokio", "tokio-util", "tracing", "tracing-durations-export", "tracing-subscriber", "uv-cache", "uv-cli", "uv-client", "uv-configuration", "uv-distribution-filename", "uv-distribution-types", "uv-extract", "uv-installer", "uv-macros", "uv-options-metadata", "uv-pep508", "uv-performance-memory-allocator", "uv-preview", "uv-pypi-types", "uv-python", "uv-settings", "uv-static", "uv-workspace", "walkdir", ] [[package]] name = "uv-dirs" version = "0.0.7" dependencies = [ "assert_fs", "etcetera", "fs-err", "indoc", "tracing", "uv-static", ] [[package]] name = "uv-dispatch" version = "0.0.7" dependencies = [ "anyhow", "futures", "itertools 0.14.0", "rustc-hash", "thiserror 2.0.17", "tokio", "tracing", "uv-build-backend", "uv-build-frontend", "uv-cache", "uv-client", "uv-configuration", "uv-distribution", "uv-distribution-filename", "uv-distribution-types", "uv-git", "uv-install-wheel", "uv-installer", "uv-platform-tags", "uv-preview", "uv-pypi-types", "uv-python", "uv-resolver", "uv-types", "uv-version", "uv-workspace", ] [[package]] name = "uv-distribution" version = "0.0.7" dependencies = [ "anyhow", "astral-reqwest-middleware", "either", "fs-err", "futures", "indoc", "insta", "nanoid", "owo-colors", "reqwest", "rmp-serde", "rustc-hash", "serde", "tempfile", "thiserror 2.0.17", "tokio", "tokio-util", "toml", "tracing", "url", "uv-auth", "uv-cache", "uv-cache-info", "uv-client", "uv-configuration", "uv-distribution-filename", "uv-distribution-types", "uv-extract", "uv-flags", "uv-fs", "uv-git", "uv-git-types", "uv-metadata", "uv-normalize", "uv-pep440", "uv-pep508", "uv-platform-tags", "uv-pypi-types", "uv-redacted", "uv-types", "uv-workspace", "walkdir", "zip", ] [[package]] name = "uv-distribution-filename" version = "0.0.7" dependencies = [ "insta", "memchr", "rkyv", "serde", "smallvec", "thiserror 2.0.17", "uv-cache-key", "uv-normalize", "uv-pep440", "uv-platform-tags", "uv-small-str", ] [[package]] name = "uv-distribution-types" version = "0.0.7" dependencies = [ "arcstr", "astral-version-ranges", "bitflags 2.9.4", "fs-err", "http", "itertools 0.14.0", "jiff", "owo-colors", "percent-encoding", "petgraph", "rkyv", "rustc-hash", "schemars", "serde", "serde_json", "thiserror 2.0.17", "toml", "tracing", "url", "uv-auth", "uv-cache-info", "uv-cache-key", "uv-distribution-filename", "uv-fs", "uv-git-types", "uv-install-wheel", "uv-normalize", "uv-pep440", "uv-pep508", "uv-platform-tags", "uv-pypi-types", "uv-redacted", "uv-small-str", "uv-warnings", ] [[package]] name = "uv-extract" version = "0.0.7" dependencies = [ "astral-tokio-tar", "astral_async_zip", "async-compression", "blake2", "fs-err", "futures", "md-5", "rayon", "regex", "reqwest", "rustc-hash", "sha2", "tar", "thiserror 2.0.17", "tokio", "tokio-util", "tracing", "uv-configuration", "uv-distribution-filename", "uv-pypi-types", "uv-static", "xz2", "zip", "zstd", ] [[package]] name = "uv-flags" version = "0.0.7" dependencies = [ "bitflags 2.9.4", ] [[package]] name = "uv-fs" version = "0.0.7" dependencies = [ "backon", "dunce", "either", "encoding_rs_io", "fs-err", "junction", "path-slash", "percent-encoding", "rustix 1.0.8", "same-file", "schemars", "serde", "tempfile", "thiserror 2.0.17", "tokio", "tracing", "uv-static", "windows 0.59.0", ] [[package]] name = "uv-git" version = "0.0.7" dependencies = [ "anyhow", "astral-reqwest-middleware", "cargo-util", "dashmap", "fs-err", "owo-colors", "reqwest", "thiserror 2.0.17", "tokio", "tracing", "url", "uv-auth", "uv-cache-key", "uv-fs", "uv-git-types", "uv-redacted", "uv-static", "uv-version", "uv-warnings", "which", ] [[package]] name = "uv-git-types" version = "0.0.7" dependencies = [ "serde", "thiserror 2.0.17", "tracing", "url", "uv-redacted", "uv-static", ] [[package]] name = "uv-globfilter" version = "0.0.7" dependencies = [ "anstream", "fs-err", "globset", "insta", "owo-colors", "regex", "regex-automata", "tempfile", "thiserror 2.0.17", "tracing", "walkdir", ] [[package]] name = "uv-install-wheel" version = "0.0.7" dependencies = [ "anyhow", "assert_fs", "clap", "configparser", "csv", "data-encoding", "fs-err", "indoc", "mailparse", "owo-colors", "pathdiff", "reflink-copy", "regex", "rustc-hash", "same-file", "schemars", "self-replace", "serde", "serde_json", "sha2", "tempfile", "thiserror 2.0.17", "tracing", "uv-distribution-filename", "uv-flags", "uv-fs", "uv-normalize", "uv-pep440", "uv-preview", "uv-pypi-types", "uv-shell", "uv-trampoline-builder", "uv-warnings", "walkdir", ] [[package]] name = "uv-installer" version = "0.0.7" dependencies = [ "anyhow", "async-channel", "fs-err", "futures", "owo-colors", "rayon", "rustc-hash", "same-file", "tempfile", "thiserror 2.0.17", "tokio", "tracing", "url", "uv-cache", "uv-cache-info", "uv-cache-key", "uv-configuration", "uv-distribution", "uv-distribution-filename", "uv-distribution-types", "uv-fs", "uv-git-types", "uv-install-wheel", "uv-normalize", "uv-pep440", "uv-pep508", "uv-platform-tags", "uv-preview", "uv-pypi-types", "uv-python", "uv-redacted", "uv-static", "uv-types", "uv-warnings", "walkdir", ] [[package]] name = "uv-keyring" version = "0.0.7" dependencies = [ "async-trait", "byteorder", "doc-comment", "env_logger", "fastrand", "secret-service", "security-framework", "thiserror 2.0.17", "tokio", "windows 0.59.0", "zeroize", ] [[package]] name = "uv-logging" version = "0.0.7" dependencies = [ "jiff", "owo-colors", "tracing", "tracing-subscriber", ] [[package]] name = "uv-macros" version = "0.0.7" dependencies = [ "proc-macro2", "quote", "syn", "textwrap", ] [[package]] name = "uv-metadata" version = "0.0.7" dependencies = [ "astral_async_zip", "fs-err", "futures", "thiserror 2.0.17", "tokio", "tokio-util", "tracing", "uv-distribution-filename", "uv-normalize", "uv-pypi-types", "zip", ] [[package]] name = "uv-normalize" version = "0.0.7" dependencies = [ "rkyv", "schemars", "serde", "uv-small-str", ] [[package]] name = "uv-once-map" version = "0.0.7" dependencies = [ "dashmap", "futures", "tokio", ] [[package]] name = "uv-options-metadata" version = "0.0.7" dependencies = [ "serde", ] [[package]] name = "uv-pep440" version = "0.0.7" dependencies = [ "astral-version-ranges", "indoc", "rkyv", "serde", "tracing", "unicode-width 0.2.2", "unscanny", "uv-cache-key", ] [[package]] name = "uv-pep508" version = "0.0.7" dependencies = [ "arcstr", "astral-version-ranges", "boxcar", "indexmap", "insta", "itertools 0.14.0", "regex", "rkyv", "rustc-hash", "schemars", "serde", "serde_json", "smallvec", "thiserror 2.0.17", "tracing", "tracing-test", "unicode-width 0.2.2", "url", "uv-cache-key", "uv-fs", "uv-normalize", "uv-pep440", "uv-redacted", ] [[package]] name = "uv-performance-memory-allocator" version = "0.0.7" dependencies = [ "mimalloc", "tikv-jemallocator", ] [[package]] name = "uv-platform" version = "0.0.7" dependencies = [ "fs-err", "goblin", "indoc", "procfs", "regex", "target-lexicon", "thiserror 2.0.17", "tracing", "uv-fs", "uv-platform-tags", "uv-static", ] [[package]] name = "uv-platform-tags" version = "0.0.7" dependencies = [ "insta", "memchr", "rkyv", "rustc-hash", "serde", "thiserror 2.0.17", "uv-small-str", ] [[package]] name = "uv-preview" version = "0.0.7" dependencies = [ "bitflags 2.9.4", "thiserror 2.0.17", "uv-warnings", ] [[package]] name = "uv-publish" version = "0.0.7" dependencies = [ "ambient-id", "astral-reqwest-middleware", "astral-reqwest-retry", "astral-tokio-tar", "async-compression", "base64 0.22.1", "fastrand", "fs-err", "futures", "glob", "insta", "itertools 0.14.0", "reqwest", "rustc-hash", "serde", "serde_json", "thiserror 2.0.17", "tokio", "tokio-util", "tracing", "url", "uv-auth", "uv-cache", "uv-client", "uv-configuration", "uv-distribution-filename", "uv-distribution-types", "uv-extract", "uv-fs", "uv-metadata", "uv-pypi-types", "uv-redacted", "uv-static", "uv-warnings", ] [[package]] name = "uv-pypi-types" version = "0.0.7" dependencies = [ "anyhow", "hashbrown 0.16.1", "indexmap", "insta", "itertools 0.14.0", "jiff", "mailparse", "petgraph", "regex", "rkyv", "rustc-hash", "schemars", "serde", "serde-untagged", "thiserror 2.0.17", "toml_edit 0.23.7", "tracing", "url", "uv-cache-key", "uv-distribution-filename", "uv-git-types", "uv-normalize", "uv-pep440", "uv-pep508", "uv-redacted", "uv-small-str", ] [[package]] name = "uv-python" version = "0.0.7" dependencies = [ "anyhow", "assert_fs", "astral-reqwest-middleware", "astral-reqwest-retry", "clap", "configparser", "dunce", "fs-err", "futures", "indexmap", "indoc", "insta", "itertools 0.14.0", "owo-colors", "ref-cast", "regex", "reqwest", "rmp-serde", "rustc-hash", "same-file", "schemars", "serde", "serde_json", "sys-info", "target-lexicon", "temp-env", "tempfile", "test-log", "thiserror 2.0.17", "tokio", "tokio-util", "tracing", "url", "uv-cache", "uv-cache-info", "uv-cache-key", "uv-client", "uv-dirs", "uv-distribution-filename", "uv-extract", "uv-fs", "uv-install-wheel", "uv-pep440", "uv-pep508", "uv-platform", "uv-platform-tags", "uv-preview", "uv-pypi-types", "uv-redacted", "uv-state", "uv-static", "uv-trampoline-builder", "uv-warnings", "which", "windows 0.59.0", "windows-registry", ] [[package]] name = "uv-redacted" version = "0.0.7" dependencies = [ "ref-cast", "schemars", "serde", "thiserror 2.0.17", "url", ] [[package]] name = "uv-requirements" version = "0.0.7" dependencies = [ "anyhow", "configparser", "console 0.16.1", "fs-err", "futures", "rustc-hash", "serde", "thiserror 2.0.17", "toml", "tracing", "url", "uv-cache-key", "uv-client", "uv-configuration", "uv-console", "uv-distribution", "uv-distribution-filename", "uv-distribution-types", "uv-fs", "uv-git", "uv-normalize", "uv-pep508", "uv-pypi-types", "uv-redacted", "uv-requirements-txt", "uv-resolver", "uv-scripts", "uv-types", "uv-warnings", ] [[package]] name = "uv-requirements-txt" version = "0.0.7" dependencies = [ "anyhow", "assert_fs", "astral-reqwest-middleware", "fs-err", "indoc", "insta", "itertools 0.14.0", "memchr", "regex", "reqwest", "rustc-hash", "tempfile", "test-case", "thiserror 2.0.17", "tokio", "tracing", "unscanny", "url", "uv-client", "uv-configuration", "uv-distribution-types", "uv-fs", "uv-normalize", "uv-pep508", "uv-pypi-types", "uv-redacted", "uv-warnings", ] [[package]] name = "uv-resolver" version = "0.0.7" dependencies = [ "arcstr", "astral-pubgrub", "clap", "cyclonedx-bom", "dashmap", "either", "fs-err", "futures", "hashbrown 0.16.1", "indexmap", "insta", "itertools 0.14.0", "jiff", "owo-colors", "percent-encoding", "petgraph", "rkyv", "rustc-hash", "same-file", "schemars", "serde", "smallvec", "textwrap", "thiserror 2.0.17", "tokio", "tokio-stream", "toml", "toml_edit 0.23.7", "tracing", "url", "uv-cache-key", "uv-client", "uv-configuration", "uv-console", "uv-distribution", "uv-distribution-filename", "uv-distribution-types", "uv-flags", "uv-fs", "uv-git", "uv-git-types", "uv-metadata", "uv-normalize", "uv-once-map", "uv-pep440", "uv-pep508", "uv-platform-tags", "uv-preview", "uv-pypi-types", "uv-python", "uv-redacted", "uv-requirements-txt", "uv-small-str", "uv-static", "uv-torch", "uv-types", "uv-version", "uv-warnings", "uv-workspace", ] [[package]] name = "uv-scripts" version = "0.0.7" dependencies = [ "fs-err", "indoc", "memchr", "regex", "serde", "thiserror 2.0.17", "toml", "url", "uv-configuration", "uv-distribution-types", "uv-normalize", "uv-pep440", "uv-pep508", "uv-pypi-types", "uv-redacted", "uv-settings", "uv-warnings", "uv-workspace", ] [[package]] name = "uv-settings" version = "0.0.7" dependencies = [ "clap", "fs-err", "schemars", "serde", "textwrap", "thiserror 2.0.17", "toml", "tracing", "url", "uv-cache-info", "uv-client", "uv-configuration", "uv-dirs", "uv-distribution-types", "uv-flags", "uv-fs", "uv-install-wheel", "uv-macros", "uv-normalize", "uv-options-metadata", "uv-pep508", "uv-pypi-types", "uv-python", "uv-redacted", "uv-resolver", "uv-static", "uv-torch", "uv-warnings", "uv-workspace", ] [[package]] name = "uv-shell" version = "0.0.7" dependencies = [ "anyhow", "fs-err", "nix", "same-file", "temp-env", "tempfile", "tracing", "uv-fs", "uv-static", "windows 0.59.0", "windows-registry", ] [[package]] name = "uv-small-str" version = "0.0.7" dependencies = [ "arcstr", "rkyv", "schemars", "serde", ] [[package]] name = "uv-state" version = "0.0.7" dependencies = [ "fs-err", "tempfile", "uv-dirs", ] [[package]] name = "uv-static" version = "0.0.7" dependencies = [ "uv-macros", ] [[package]] name = "uv-tool" version = "0.0.7" dependencies = [ "fs-err", "pathdiff", "serde", "thiserror 2.0.17", "toml", "toml_edit 0.23.7", "tracing", "uv-cache", "uv-dirs", "uv-distribution-types", "uv-fs", "uv-install-wheel", "uv-installer", "uv-normalize", "uv-pep440", "uv-pep508", "uv-preview", "uv-pypi-types", "uv-python", "uv-settings", "uv-state", "uv-static", "uv-virtualenv", ] [[package]] name = "uv-torch" version = "0.0.7" dependencies = [ "clap", "either", "fs-err", "schemars", "serde", "thiserror 2.0.17", "tracing", "url", "uv-distribution-types", "uv-normalize", "uv-pep440", "uv-platform-tags", "uv-static", "wmi", ] [[package]] name = "uv-trampoline-builder" version = "0.0.7" dependencies = [ "anyhow", "assert_cmd", "assert_fs", "fs-err", "rcgen", "tempfile", "thiserror 2.0.17", "uv-fs", "which", "windows 0.59.0", "zip", ] [[package]] name = "uv-types" version = "0.0.7" dependencies = [ "anyhow", "dashmap", "rustc-hash", "thiserror 2.0.17", "uv-cache", "uv-configuration", "uv-distribution-filename", "uv-distribution-types", "uv-git", "uv-normalize", "uv-once-map", "uv-pep440", "uv-pypi-types", "uv-python", "uv-redacted", "uv-workspace", ] [[package]] name = "uv-version" version = "0.9.17" [[package]] name = "uv-virtualenv" version = "0.0.7" dependencies = [ "console 0.16.1", "fs-err", "itertools 0.14.0", "owo-colors", "pathdiff", "self-replace", "thiserror 2.0.17", "tracing", "uv-console", "uv-fs", "uv-preview", "uv-pypi-types", "uv-python", "uv-shell", "uv-version", "uv-warnings", ] [[package]] name = "uv-warnings" version = "0.0.7" dependencies = [ "anstream", "owo-colors", "rustc-hash", ] [[package]] name = "uv-workspace" version = "0.0.7" dependencies = [ "anyhow", "assert_fs", "clap", "fs-err", "glob", "insta", "itertools 0.14.0", "owo-colors", "regex", "rustc-hash", "schemars", "serde", "tempfile", "thiserror 2.0.17", "tokio", "toml", "toml_edit 0.23.7", "tracing", "uv-build-backend", "uv-cache-key", "uv-configuration", "uv-distribution-types", "uv-fs", "uv-git-types", "uv-macros", "uv-normalize", "uv-options-metadata", "uv-pep440", "uv-pep508", "uv-pypi-types", "uv-redacted", "uv-static", "uv-warnings", ] [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-streams" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", ] [[package]] name = "wasmtimer" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8d49b5d6c64e8558d9b1b065014426f35c18de636895d24893dbbd329743446" dependencies = [ "futures", "js-sys", "parking_lot", "pin-utils", "slab", "wasm-bindgen", ] [[package]] name = "web-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "web-time" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki-roots" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] [[package]] name = "weezl" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] name = "which" version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ "env_home", "regex", "rustix 1.0.8", "winsafe", ] [[package]] name = "whoami" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ "libredox", "wasite", "web-sys", ] [[package]] name = "widestring" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" dependencies = [ "windows-core 0.59.0", "windows-targets 0.53.2", ] [[package]] name = "windows" version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", "windows-core 0.61.2", "windows-future", "windows-link 0.1.3", "windows-numerics", ] [[package]] name = "windows-collections" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ "windows-core 0.61.2", ] [[package]] name = "windows-core" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" dependencies = [ "windows-implement 0.59.0", "windows-interface", "windows-result", "windows-strings 0.3.1", "windows-targets 0.53.2", ] [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface", "windows-link 0.1.3", "windows-result", "windows-strings 0.4.2", ] [[package]] name = "windows-future" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", "windows-threading", ] [[package]] name = "windows-implement" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-implement" version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", ] [[package]] name = "windows-registry" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link 0.1.3", "windows-result", "windows-strings 0.4.2", ] [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link 0.1.3", ] [[package]] name = "windows-strings" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ "windows-link 0.1.3", ] [[package]] name = "windows-strings" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link 0.1.3", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets 0.53.2", ] [[package]] name = "windows-sys" version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" dependencies = [ "windows-link 0.2.0", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", "windows_i686_gnullvm 0.53.0", "windows_i686_msvc 0.53.0", "windows_x86_64_gnu 0.53.0", "windows_x86_64_gnullvm 0.53.0", "windows_x86_64_msvc 0.53.0", ] [[package]] name = "windows-threading" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ "windows-link 0.1.3", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] name = "winsafe" version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wiremock" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" dependencies = [ "assert-json-diff", "base64 0.22.1", "deadpool", "futures", "http", "http-body-util", "hyper", "hyper-util", "log", "once_cell", "regex", "serde", "serde_json", "tokio", "url", ] [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags 2.9.4", ] [[package]] name = "wmi" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d9189bc72f0e4d814d812216ec06636ce3ea5597ff5f1ff9f9f0e5ec781c027" dependencies = [ "futures", "log", "serde", "thiserror 2.0.17", "windows 0.61.3", "windows-core 0.61.2", ] [[package]] name = "writeable" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "xattr" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" dependencies = [ "libc", "rustix 1.0.8", ] [[package]] name = "xml-rs" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" [[package]] name = "xmlparser" version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xz2" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" dependencies = [ "lzma-sys", ] [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yasna" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ "time", ] [[package]] name = "yoke" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zbus" version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597f45e98bc7e6f0988276012797855613cd8269e23b5be62cc4e5d28b7e515d" dependencies = [ "async-broadcast", "async-recursion", "async-trait", "enumflags2", "event-listener", "futures-core", "futures-lite", "hex", "nix", "ordered-stream", "serde", "serde_repr", "tokio", "tracing", "uds_windows", "windows-sys 0.59.0", "winnow", "zbus_macros", "zbus_names", "zvariant", ] [[package]] name = "zbus_macros" version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5c8e4e14dcdd9d97a98b189cd1220f30e8394ad271e8c987da84f73693862c2" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", "zbus_names", "zvariant", "zvariant_utils", ] [[package]] name = "zbus_names" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", "winnow", "zvariant", ] [[package]] name = "zerocopy" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zip" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" dependencies = [ "arbitrary", "bzip2", "crc32fast", "crossbeam-utils", "displaydoc", "flate2", "indexmap", "lzma-rs", "memchr", "thiserror 2.0.17", "xz2", "zopfli", "zstd", ] [[package]] name = "zlib-rs" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" [[package]] name = "zopfli" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" dependencies = [ "bumpalo", "crc32fast", "log", "simd-adler32", ] [[package]] name = "zstd" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" version = "2.0.15+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" dependencies = [ "cc", "pkg-config", ] [[package]] name = "zvariant" version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" dependencies = [ "endi", "enumflags2", "serde", "winnow", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", "zvariant_utils", ] [[package]] name = "zvariant_utils" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" dependencies = [ "proc-macro2", "quote", "serde", "static_assertions", "syn", "winnow", ] uv-0.9.17+ds1/Cargo.toml000066400000000000000000000352641520155276700147010ustar00rootroot00000000000000[workspace] members = ["crates/*"] exclude = [ "scripts", # Needs nightly "crates/uv-trampoline", ] resolver = "2" [workspace.package] edition = "2024" rust-version = "1.89" homepage = "https://pypi.org/project/uv/" repository = "https://github.com/astral-sh/uv" authors = ["uv"] license = "MIT OR Apache-2.0" [workspace.dependencies] uv-auth = { version = "0.0.7", path = "crates/uv-auth" } uv-bin-install = { version = "0.0.7", path = "crates/uv-bin-install" } uv-build-backend = { version = "0.0.7", path = "crates/uv-build-backend" } uv-build-frontend = { version = "0.0.7", path = "crates/uv-build-frontend" } uv-cache = { version = "0.0.7", path = "crates/uv-cache" } uv-cache-info = { version = "0.0.7", path = "crates/uv-cache-info" } uv-cache-key = { version = "0.0.7", path = "crates/uv-cache-key" } uv-cli = { version = "0.0.7", path = "crates/uv-cli" } uv-client = { version = "0.0.7", path = "crates/uv-client" } uv-configuration = { version = "0.0.7", path = "crates/uv-configuration" } uv-console = { version = "0.0.7", path = "crates/uv-console" } uv-dirs = { version = "0.0.7", path = "crates/uv-dirs" } uv-dispatch = { version = "0.0.7", path = "crates/uv-dispatch" } uv-distribution = { version = "0.0.7", path = "crates/uv-distribution" } uv-distribution-filename = { version = "0.0.7", path = "crates/uv-distribution-filename" } uv-distribution-types = { version = "0.0.7", path = "crates/uv-distribution-types" } uv-extract = { version = "0.0.7", path = "crates/uv-extract" } uv-flags = { version = "0.0.7", path = "crates/uv-flags" } uv-fs = { version = "0.0.7", path = "crates/uv-fs", features = ["serde", "tokio"] } uv-git = { version = "0.0.7", path = "crates/uv-git" } uv-git-types = { version = "0.0.7", path = "crates/uv-git-types" } uv-globfilter = { version = "0.0.7", path = "crates/uv-globfilter" } uv-install-wheel = { version = "0.0.7", path = "crates/uv-install-wheel", default-features = false } uv-installer = { version = "0.0.7", path = "crates/uv-installer" } uv-keyring = { version = "0.0.7", path = "crates/uv-keyring" } uv-logging = { version = "0.0.7", path = "crates/uv-logging" } uv-macros = { version = "0.0.7", path = "crates/uv-macros" } uv-metadata = { version = "0.0.7", path = "crates/uv-metadata" } uv-normalize = { version = "0.0.7", path = "crates/uv-normalize" } uv-once-map = { version = "0.0.7", path = "crates/uv-once-map" } uv-options-metadata = { version = "0.0.7", path = "crates/uv-options-metadata" } uv-performance-memory-allocator = { version = "0.0.7", path = "crates/uv-performance-memory-allocator" } uv-pep440 = { version = "0.0.7", path = "crates/uv-pep440", features = ["tracing", "rkyv", "version-ranges"] } uv-pep508 = { version = "0.0.7", path = "crates/uv-pep508", features = ["non-pep508-extensions"] } uv-platform = { version = "0.0.7", path = "crates/uv-platform" } uv-platform-tags = { version = "0.0.7", path = "crates/uv-platform-tags" } uv-preview = { version = "0.0.7", path = "crates/uv-preview" } uv-publish = { version = "0.0.7", path = "crates/uv-publish" } uv-pypi-types = { version = "0.0.7", path = "crates/uv-pypi-types" } uv-python = { version = "0.0.7", path = "crates/uv-python" } uv-redacted = { version = "0.0.7", path = "crates/uv-redacted" } uv-requirements = { version = "0.0.7", path = "crates/uv-requirements" } uv-requirements-txt = { version = "0.0.7", path = "crates/uv-requirements-txt" } uv-resolver = { version = "0.0.7", path = "crates/uv-resolver" } uv-scripts = { version = "0.0.7", path = "crates/uv-scripts" } uv-settings = { version = "0.0.7", path = "crates/uv-settings" } uv-shell = { version = "0.0.7", path = "crates/uv-shell" } uv-small-str = { version = "0.0.7", path = "crates/uv-small-str" } uv-state = { version = "0.0.7", path = "crates/uv-state" } uv-static = { version = "0.0.7", path = "crates/uv-static" } uv-tool = { version = "0.0.7", path = "crates/uv-tool" } uv-torch = { version = "0.0.7", path = "crates/uv-torch" } uv-trampoline-builder = { version = "0.0.7", path = "crates/uv-trampoline-builder" } uv-types = { version = "0.0.7", path = "crates/uv-types" } uv-version = { version = "0.9.17", path = "crates/uv-version" } uv-virtualenv = { version = "0.0.7", path = "crates/uv-virtualenv" } uv-warnings = { version = "0.0.7", path = "crates/uv-warnings" } uv-workspace = { version = "0.0.7", path = "crates/uv-workspace" } ambient-id = { version = "0.0.7", default-features = false, features = ["astral-reqwest-middleware"] } anstream = { version = "0.6.15" } anyhow = { version = "1.0.89" } arcstr = { version = "1.2.0" } arrayvec = { version = "0.7.6" } astral-tokio-tar = { version = "0.5.6" } async-channel = { version = "2.3.1" } async-compression = { version = "0.4.12", features = ["bzip2", "gzip", "xz", "zstd"] } async-trait = { version = "0.1.82" } async_http_range_reader = { version = "0.9.1", package = "astral_async_http_range_reader" } async_zip = { version = "0.0.17", package = "astral_async_zip", features = ["bzip2", "deflate", "lzma", "tokio", "xz", "zstd"] } axoupdater = { version = "0.9.0", default-features = false } backon = { version = "1.3.0" } base64 = { version = "0.22.1" } bitflags = { version = "2.6.0" } blake2 = { version = "0.10.6" } boxcar = { version = "0.2.5" } bytecheck = { version = "0.8.0" } cargo-util = { version = "0.2.14" } clap = { version = "4.5.17", features = ["derive", "env", "string", "wrap_help"] } clap_complete_command = { version = "0.6.1" } configparser = { version = "3.1.0" } console = { version = "0.16.0", default-features = false, features = ["std"] } csv = { version = "1.3.0" } ctrlc = { version = "3.4.5" } cyclonedx-bom = { version = "0.8.0" } dashmap = { version = "6.1.0" } data-encoding = { version = "2.6.0" } dotenvy = { version = "0.15.7" } dunce = { version = "1.0.5" } either = { version = "1.13.0" } encoding_rs_io = { version = "0.1.7" } embed-manifest = { version = "1.5.0" } etcetera = { version = "0.11.0" } fastrand = { version = "2.3.0" } flate2 = { version = "1.0.33", default-features = false, features = ["zlib-rs"] } fs-err = { version = "3.0.0", features = ["tokio"] } futures = { version = "0.3.30" } glob = { version = "0.3.1" } globset = { version = "0.4.15" } globwalk = { version = "0.9.1" } goblin = { version = "0.10.0", default-features = false, features = ["std", "elf32", "elf64", "endian_fd"] } h2 = { version = "0.4.7" } hashbrown = { version = "0.16.0" } hex = { version = "0.4.3" } html-escape = { version = "0.2.13" } http = { version = "1.1.0" } indexmap = { version = "2.5.0" } indicatif = { version = "0.18.0" } indoc = { version = "2.0.5" } itertools = { version = "0.14.0" } jiff = { version = "0.2.0", features = ["serde"] } junction = { version = "1.2.0" } mailparse = { version = "0.16.0" } md-5 = { version = "0.10.6" } memchr = { version = "2.7.4" } miette = { version = "7.2.0", features = ["fancy-no-backtrace"] } nanoid = { version = "0.4.0" } nix = { version = "0.30.0", features = ["signal"] } open = { version = "5.3.2" } owo-colors = { version = "4.1.0" } path-slash = { version = "0.2.1" } pathdiff = { version = "0.2.1" } percent-encoding = { version = "2.3.1" } petgraph = { version = "0.8.0" } proc-macro2 = { version = "1.0.86" } procfs = { version = "0.17.0", default-features = false, features = ["flate2"] } pubgrub = { version = "0.3.3" , package = "astral-pubgrub" } quote = { version = "1.0.37" } rayon = { version = "1.10.0" } ref-cast = { version = "1.0.24" } reflink-copy = { version = "0.1.19" } regex = { version = "1.10.6" } regex-automata = { version = "0.4.8", default-features = false, features = ["dfa-build", "dfa-search", "perf", "std", "syntax"] } reqsign = { version = "0.18.0", features = ["aws", "default-context"], default-features = false } reqwest = { version = "0.12.22", default-features = false, features = ["json", "gzip", "deflate", "zstd", "stream", "system-proxy", "rustls-tls", "rustls-tls-native-roots", "socks", "multipart", "http2", "blocking"] } reqwest-middleware = { version = "0.4.2", package = "astral-reqwest-middleware", features = ["multipart"] } reqwest-retry = { version = "0.7.0", package = "astral-reqwest-retry" } rkyv = { version = "0.8.8", features = ["bytecheck"] } rmp-serde = { version = "1.3.0" } rust-netrc = { version = "0.1.2" } rustc-hash = { version = "2.0.0" } rustix = { version = "1.0.0", default-features = false, features = ["fs", "std"] } same-file = { version = "1.0.6" } schemars = { version = "1.0.0", features = ["url2"] } seahash = { version = "4.1.0" } secret-service = { version = "5.0.0", features = ["rt-tokio-crypto-rust"] } security-framework = { version = "3" } self-replace = { version = "1.5.0" } serde = { version = "1.0.210", features = ["derive", "rc"] } serde-untagged = { version = "0.1.6" } serde_json = { version = "1.0.128" } sha2 = { version = "0.10.8" } smallvec = { version = "1.13.2" } spdx = { version = "0.12.0" } syn = { version = "2.0.77" } sys-info = { version = "0.9.1" } tar = { version = "0.4.43" } target-lexicon = { version = "0.13.0" } tempfile = { version = "3.14.0" } textwrap = { version = "0.16.1" } thiserror = { version = "2.0.0" } astral-tl = { version = "0.7.11" } tokio = { version = "1.40.0", features = ["fs", "io-util", "macros", "process", "rt", "signal", "sync", "time"] } tokio-stream = { version = "0.1.16" } tokio-util = { version = "0.7.12", features = ["compat", "io"] } toml = { version = "0.9.2", features = ["fast_hash"] } toml_edit = { version = "0.23.2", features = ["serde"] } tracing = { version = "0.1.40" } tracing-durations-export = { version = "0.3.0", features = ["plot"] } tracing-subscriber = { version = "0.3.18" } # Default feature set for uv_build, uv activates extra features tracing-test = { version = "0.2.5" } tracing-tree = { version = "0.4.0" } unicode-width = { version = "0.2.0" } unscanny = { version = "0.1.0" } url = { version = "2.5.2", features = ["serde"] } uuid = { version = "1.16.0" } version-ranges = { version = "0.1.3", package = "astral-version-ranges" } walkdir = { version = "2.5.0" } which = { version = "8.0.0", features = ["regex"] } windows = { version = "0.59.0", features = ["std", "Win32_Globalization", "Win32_System_LibraryLoader", "Win32_System_Console", "Win32_System_Kernel", "Win32_System_Diagnostics_Debug", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_Registry", "Win32_System_IO", "Win32_System_Ioctl"] } windows-registry = { version = "0.5.0" } wiremock = { version = "0.6.4" } wmi = { version = "0.16.0", default-features = false } xz2 = { version = "0.1.7" } zeroize = { version = "1.8.1" } zip = { version = "2.2.3", default-features = false, features = ["deflate", "zstd", "bzip2", "lzma", "xz"] } zstd = { version = "0.13.3" } # dev-dependencies assert_cmd = { version = "2.0.16" } assert_fs = { version = "1.1.2" } byteorder = { version = "1.5.0" } filetime = { version = "0.2.25" } http-body-util = { version = "0.1.2" } hyper = { version = "1.4.1", features = ["server", "http1"] } hyper-util = { version = "0.1.8", features = ["tokio", "server", "http1"] } ignore = { version = "0.4.23" } insta = { version = "1.40.0", features = ["json", "filters", "redactions"] } predicates = { version = "3.1.2" } rcgen = { version = "0.14.5", features = ["crypto", "pem", "ring"], default-features = false } rustls = { version = "0.23.29", default-features = false } similar = { version = "2.6.0" } temp-env = { version = "0.3.6" } test-case = { version = "3.3.1" } test-log = { version = "0.2.16", features = ["trace"], default-features = false } tokio-rustls = { version = "0.26.2", default-features = false } whoami = { version = "1.6.0" } [workspace.metadata.cargo-shear] ignored = ["flate2", "xz2", "h2", "uv-performance-memory-allocator"] [workspace.lints.rust] unsafe_code = "warn" unreachable_pub = "warn" [workspace.lints.clippy] pedantic = { level = "warn", priority = -2 } # Allowed pedantic lints char_lit_as_u8 = "allow" collapsible_else_if = "allow" collapsible_if = "allow" implicit_hasher = "allow" map_unwrap_or = "allow" match_same_arms = "allow" missing_errors_doc = "allow" missing_panics_doc = "allow" module_name_repetitions = "allow" must_use_candidate = "allow" similar_names = "allow" struct_excessive_bools = "allow" too_many_arguments = "allow" too_many_lines = "allow" used_underscore_binding = "allow" # Disallowed restriction lints print_stdout = "warn" print_stderr = "warn" dbg_macro = "warn" empty_drop = "warn" empty_structs_with_brackets = "warn" exit = "warn" get_unwrap = "warn" rc_buffer = "warn" rc_mutex = "warn" rest_pat_in_fully_bound_structs = "warn" if_not_else = "allow" use_self = "warn" # Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved. large_stack_arrays = "allow" [profile.release] strip = true lto = "fat" # This profile is meant to mimic the `release` profile as closely as # possible, but using settings that are more beneficial for iterative # development. That is, the `release` profile is intended for actually # building the release, where as `profiling` is meant for building `uv` # for running benchmarks. # # The main differences here are to avoid stripping debug information # and disabling lto. This does result in a mismatch between our release # configuration and our benchmarking configuration, which is unfortunate. # But compile times with `lto = true` are completely untenable: # # $ cargo b --profile profiling -p uv # Compiling uv-cli v0.0.1 (/home/andrew/astral/uv/crates/uv-cli) # Compiling uv v0.2.34 (/home/andrew/astral/uv/crates/uv) # Finished `profiling` profile [optimized + debuginfo] target(s) in 3m 47s # # Using `lto = "thin"` brings a massive improvement, but it's still slow: # # $ cargo b --profile profiling -p uv # Compiling uv v0.2.34 (/home/andrew/astral/uv/crates/uv) # Finished `profiling` profile [optimized + debuginfo] target(s) in 53.98s # # But with `lto = false`: # # $ cargo b --profile profiling -p uv # Compiling uv v0.2.34 (/home/andrew/astral/uv/crates/uv) # Finished `profiling` profile [optimized + debuginfo] target(s) in 30.09s # # We get more reasonable-ish compile times. At least, it's not enough # time to get up and get a cup of coffee before it completes. # # This setup does risk that we are measuring something in benchmarks # that we are shipping, but in order to make those two the same, we'd # either need to make compile times way worse for development, or take # a hit to binary size and a slight hit to runtime performance in our # release builds. [profile.profiling] inherits = "release" strip = false debug = "full" lto = false [profile.fast-build] inherits = "dev" opt-level = 1 debug = 0 strip = "debuginfo" # Profile to build a minimally sized binary for uv-build [profile.minimal-size] inherits = "release" opt-level = "z" # This will still show a panic message, we only skip the unwind panic = "abort" codegen-units = 1 # The profile that 'cargo dist' will build with. [profile.dist] inherits = "release" uv-0.9.17+ds1/Dockerfile000066400000000000000000000034621520155276700147360ustar00rootroot00000000000000FROM --platform=$BUILDPLATFORM ubuntu AS build ENV HOME="/root" WORKDIR $HOME RUN apt update \ && apt install -y --no-install-recommends \ build-essential \ curl \ python3-venv \ && apt clean \ && rm -rf /var/lib/apt/lists/* # Setup zig as cross compiling linker RUN python3 -m venv $HOME/.venv RUN .venv/bin/pip install cargo-zigbuild ENV PATH="$HOME/.venv/bin:$PATH" # Install rust ARG TARGETPLATFORM RUN case "$TARGETPLATFORM" in \ "linux/arm64") echo "aarch64-unknown-linux-musl" > rust_target.txt ;; \ "linux/amd64") echo "x86_64-unknown-linux-musl" > rust_target.txt ;; \ *) exit 1 ;; \ esac # Temporarily using nightly-2025-11-02 for bundled musl v1.2.5 # Ref: https://github.com/rust-lang/rust/pull/142682 # TODO(samypr100): Remove when toolchain updates to 1.93 COPY < Shows a bar chart with benchmark results.

Installing Trio's dependencies with a warm cache.

## Highlights - 🚀 A single tool to replace `pip`, `pip-tools`, `pipx`, `poetry`, `pyenv`, `twine`, `virtualenv`, and more. - âš¡ï¸ [10-100x faster](https://github.com/astral-sh/uv/blob/main/BENCHMARKS.md) than `pip`. - ðŸ—‚ï¸ Provides [comprehensive project management](#projects), with a [universal lockfile](https://docs.astral.sh/uv/concepts/projects/layout#the-lockfile). - â‡ï¸ [Runs scripts](#scripts), with support for [inline dependency metadata](https://docs.astral.sh/uv/guides/scripts#declaring-script-dependencies). - ðŸ [Installs and manages](#python-versions) Python versions. - ðŸ› ï¸ [Runs and installs](#tools) tools published as Python packages. - 🔩 Includes a [pip-compatible interface](#the-pip-interface) for a performance boost with a familiar CLI. - 🢠Supports Cargo-style [workspaces](https://docs.astral.sh/uv/concepts/projects/workspaces) for scalable projects. - 💾 Disk-space efficient, with a [global cache](https://docs.astral.sh/uv/concepts/cache) for dependency deduplication. - ⬠Installable without Rust or Python via `curl` or `pip`. - ðŸ–¥ï¸ Supports macOS, Linux, and Windows. uv is backed by [Astral](https://astral.sh), the creators of [Ruff](https://github.com/astral-sh/ruff). ## Installation Install uv with our standalone installers: ```bash # On macOS and Linux. curl -LsSf https://astral.sh/uv/install.sh | sh ``` ```bash # On Windows. powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` Or, from [PyPI](https://pypi.org/project/uv/): ```bash # With pip. pip install uv ``` ```bash # Or pipx. pipx install uv ``` If installed via the standalone installer, uv can update itself to the latest version: ```bash uv self update ``` See the [installation documentation](https://docs.astral.sh/uv/getting-started/installation/) for details and alternative installation methods. ## Documentation uv's documentation is available at [docs.astral.sh/uv](https://docs.astral.sh/uv). Additionally, the command line reference documentation can be viewed with `uv help`. ## Features ### Projects uv manages project dependencies and environments, with support for lockfiles, workspaces, and more, similar to `rye` or `poetry`: ```console $ uv init example Initialized project `example` at `/home/user/example` $ cd example $ uv add ruff Creating virtual environment at: .venv Resolved 2 packages in 170ms Built example @ file:///home/user/example Prepared 2 packages in 627ms Installed 2 packages in 1ms + example==0.1.0 (from file:///home/user/example) + ruff==0.5.0 $ uv run ruff check All checks passed! $ uv lock Resolved 2 packages in 0.33ms $ uv sync Resolved 2 packages in 0.70ms Audited 1 package in 0.02ms ``` See the [project documentation](https://docs.astral.sh/uv/guides/projects/) to get started. uv also supports building and publishing projects, even if they're not managed with uv. See the [publish guide](https://docs.astral.sh/uv/guides/publish/) to learn more. ### Scripts uv manages dependencies and environments for single-file scripts. Create a new script and add inline metadata declaring its dependencies: ```console $ echo 'import requests; print(requests.get("https://astral.sh"))' > example.py $ uv add --script example.py requests Updated `example.py` ``` Then, run the script in an isolated virtual environment: ```console $ uv run example.py Reading inline script metadata from: example.py Installed 5 packages in 12ms ``` See the [scripts documentation](https://docs.astral.sh/uv/guides/scripts/) to get started. ### Tools uv executes and installs command-line tools provided by Python packages, similar to `pipx`. Run a tool in an ephemeral environment using `uvx` (an alias for `uv tool run`): ```console $ uvx pycowsay 'hello world!' Resolved 1 package in 167ms Installed 1 package in 9ms + pycowsay==0.0.0.2 """ ------------ < hello world! > ------------ \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || ``` Install a tool with `uv tool install`: ```console $ uv tool install ruff Resolved 1 package in 6ms Installed 1 package in 2ms + ruff==0.5.0 Installed 1 executable: ruff $ ruff --version ruff 0.5.0 ``` See the [tools documentation](https://docs.astral.sh/uv/guides/tools/) to get started. ### Python versions uv installs Python and allows quickly switching between versions. Install multiple Python versions: ```console $ uv python install 3.10 3.11 3.12 Searching for Python versions matching: Python 3.10 Searching for Python versions matching: Python 3.11 Searching for Python versions matching: Python 3.12 Installed 3 versions in 3.42s + cpython-3.10.14-macos-aarch64-none + cpython-3.11.9-macos-aarch64-none + cpython-3.12.4-macos-aarch64-none ``` Download Python versions as needed: ```console $ uv venv --python 3.12.0 Using Python 3.12.0 Creating virtual environment at: .venv Activate with: source .venv/bin/activate $ uv run --python pypy@3.8 -- python --version Python 3.8.16 (a9dbdca6fc3286b0addd2240f11d97d8e8de187a, Dec 29 2022, 11:45:30) [PyPy 7.3.11 with GCC Apple LLVM 13.1.6 (clang-1316.0.21.2.5)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>>> ``` Use a specific Python version in the current directory: ```console $ uv python pin 3.11 Pinned `.python-version` to `3.11` ``` See the [Python installation documentation](https://docs.astral.sh/uv/guides/install-python/) to get started. ### The pip interface uv provides a drop-in replacement for common `pip`, `pip-tools`, and `virtualenv` commands. uv extends their interfaces with advanced features, such as dependency version overrides, platform-independent resolutions, reproducible resolutions, alternative resolution strategies, and more. Migrate to uv without changing your existing workflows — and experience a 10-100x speedup — with the `uv pip` interface. Compile requirements into a platform-independent requirements file: ```console $ uv pip compile docs/requirements.in \ --universal \ --output-file docs/requirements.txt Resolved 43 packages in 12ms ``` Create a virtual environment: ```console $ uv venv Using Python 3.12.3 Creating virtual environment at: .venv Activate with: source .venv/bin/activate ``` Install the locked requirements: ```console $ uv pip sync docs/requirements.txt Resolved 43 packages in 11ms Installed 43 packages in 208ms + babel==2.15.0 + black==24.4.2 + certifi==2024.7.4 ... ``` See the [pip interface documentation](https://docs.astral.sh/uv/pip/index/) to get started. ## Platform support See uv's [platform support](https://docs.astral.sh/uv/reference/platforms/) document. ## Versioning policy See uv's [versioning policy](https://docs.astral.sh/uv/reference/versioning/) document. ## 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](https://github.com/astral-sh/uv/blob/main/CONTRIBUTING.md) to get started. ## FAQ #### How do you pronounce uv? It's pronounced as "you - vee" ([`/juË viË/`](https://en.wikipedia.org/wiki/Help:IPA/English#Key)) #### How should I stylize uv? Just "uv", please. See the [style guide](./STYLE.md#styling-uv) for details. ## Acknowledgements uv's dependency resolver uses [PubGrub](https://github.com/pubgrub-rs/pubgrub) under the hood. We're grateful to the PubGrub maintainers, especially [Jacob Finkelman](https://github.com/Eh2406), for their support. uv's Git implementation is based on [Cargo](https://github.com/rust-lang/cargo). Some of uv's optimizations are inspired by the great work we've seen in [pnpm](https://pnpm.io/), [Orogene](https://github.com/orogene/orogene), and [Bun](https://github.com/oven-sh/bun). We've also learned a lot from Nathaniel J. Smith's [Posy](https://github.com/njsmith/posy) and adapted its [trampoline](https://github.com/njsmith/posy/tree/main/src/trampolines/windows-trampolines/posy-trampoline) for Windows support. ## License uv is licensed under either of - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in uv by you, as defined in the Apache-2.0 license, shall be dually licensed as above, without any additional terms or conditions. uv-0.9.17+ds1/SECURITY.md000066400000000000000000000021261520155276700145310ustar00rootroot00000000000000# Security policy ## Scope of security vulnerabilities uv is a Python package manager. Due to the design of the Python packaging ecosystem and the dynamic nature of Python itself, there are many cases where uv can execute arbitrary code. For example: - uv invokes Python interpreters on the system to retrieve metadata - uv builds source distributions as described by PEP 517 - uv may build packages from the requested package indexes These are not considered vulnerabilities in uv. If you think uv's stance in these areas can be hardened, please file an issue for a new feature. ## Reporting a vulnerability If you have found a possible vulnerability that is not excluded by the above [scope](#scope-of-security-vulnerabilities), please email `security at astral dot sh`. ## Bug bounties While we sincerely appreciate and encourage reports of suspected security problems, please note that Astral does not currently run any bug bounty programs. ## Vulnerability disclosures Critical vulnerabilities will be disclosed via GitHub's [security advisory](https://github.com/astral-sh/uv/security) system. uv-0.9.17+ds1/STYLE.md000066400000000000000000000123341520155276700141640ustar00rootroot00000000000000# Style guide _The following is a work-in-progress style guide for our user-facing messaging in the CLI output and documentation_. ## General 1. Use of "e.g." and "i.e." should always be wrapped in commas, e.g., as shown here. 1. Em-dashes are okay, but not recommended when using monospace fonts. Use "—", not "--" or "-". 1. Always wrap em-dashes in spaces, e.g., "hello — world" not "hello—world". 1. Hyphenate compound words, e.g., use "platform-specific" not "platform specific". 1. Use backticks to escape: commands, code expressions, package names, and file paths. 1. Use less than and greater than symbols to wrap bare URLs, e.g., `` (unless it is an example; then, use backticks). 1. Avoid bare URLs outside of reference documentation, prefer labels, e.g., `[name](url)`. 1. If a message ends with a single relevant value, precede it with a colon, e.g., `This is the value: value`. If the value is a literal, wrap it in backticks. 1. Markdown files should be wrapped at 100 characters. 1. Use a space, not an equals sign, for command-line arguments with a value, e.g. `--resolution lowest`, not `--resolution=lowest`. ## Styling uv Just uv, please. 1. Do not escape with backticks, e.g., `uv`, unless referring specifically to the `uv` executable. 1. Do not capitalize, e.g., "Uv", even at the beginning of a sentence. 1. Do not uppercase, e.g., "UV", unless referring to an environment variable, e.g., `UV_PYTHON`. ## Terminology 1. Use "lockfile" not "lock file". 2. Use "pre-release", not "prerelease" (except in code, in which case: use `Prerelease`, not `PreRelease`; and `prerelease`, not `pre_release`). ## Documentation 1. Use periods at the end of all sentences, including lists unless they enumerate single items. 1. Avoid language that patronizes the reader, e.g., "simply do this". 1. Only refer to "the user" in internal or contributor documentation. 1. Avoid "we" in favor of "uv" or imperative language. ### Sections The documentation is divided into: 1. Guides 2. Concepts 3. Reference documentation #### Guides 1. Should assume no previous knowledge about uv. 1. May assume basic knowledge of the domain. 1. Should refer to relevant concept documentation. 1. Should have a clear flow. 1. Should be followed by a clear call to action. 1. Should cover the basic behavior needed to get started. 1. Should not cover behavior in detail. 1. Should not enumerate all possibilities. 1. Should avoid linking to reference documentation unless not covered in a concept document. 1. May generally ignore platform-specific behavior. 1. Should be written from second-person point of view. 1. Should use the imperative voice. #### Concepts 1. Should cover behavior in detail. 1. Should not enumerate all possibilities. 1. Should cover most common configuration. 1. Should refer to the relevant reference documentation. 1. Should discuss platform-specific behavior. 1. Should be written from the third-person point of view, not second-person (i.e., avoid "you"). 1. Should not use the imperative voice. #### Reference documentation 1. Should enumerate all options. 1. Should generally be generated from documentation in the code. 1. Should be written from the third-person point of view, not second-person (i.e., avoid "you"). 1. Should not use the imperative voice. ### Code blocks 1. All code blocks should have a language marker. 1. When using `console` syntax, use `$` to indicate commands — everything else is output. 1. Never use the `bash` syntax when displaying command output. 1. Prefer `console` with `$` prefixed commands over `bash`. 1. Command output should rarely be included — it's hard to keep up-to-date. 1. Use `title` for example files, e.g., `pyproject.toml`, `Dockerfile`, or `example.py`. ## CLI 1. Do not use periods at the end of sentences :), unless the message spans more than a single sentence. 1. May use the second-person point of view, e.g., "Did you mean...?". ### Colors and style 1. All CLI output must be interpretable and understandable _without_ the use of color and other styling. (For example: even if a command is rendered in green, wrap it in backticks.) 1. `NO_COLOR` must be respected when using any colors or styling. 1. `UV_NO_PROGRESS` must be respected when using progress-styling like bars or spinners. 1. In general, use: - Green for success. - Red for error. - Yellow for warning. - Cyan for hints. - Cyan for file paths. - Cyan for important user-facing literals (e.g., a package name in a message). - Green for commands. ### Logging 1. `warn`, `info`, `debug`, and `trace` logs are all shown with the `--verbose` flag. - Note that the displayed level is controlled with `RUST_LOG`. 1. All logging should be to stderr. ### Output 1. Text can be written to stdout if it is "data" that could be piped to another program. ### Warnings 1. `warn_user` and `warn_user_once` are shown without the `--verbose` flag. - These methods should be preferred over tracing warnings when the warning is actionable. - Deprecation warnings should use these methods. 1. Deprecation warnings must be actionable. ### Hints 1. Errors may be followed by hints suggesting a solution. 1. Hints should be separated from errors by a blank newline. 1. Hints should be stylized as `hint: `. uv-0.9.17+ds1/_typos.toml000066400000000000000000000007471520155276700151610ustar00rootroot00000000000000[files] extend-exclude = [ "**/snapshots/", "test/ecosystem/**", "test/requirements/**/*.in", "crates/uv-build-frontend/src/pipreqs/mapping", ] ignore-hidden = false [default] extend-ignore-re = [ "FRiENDlY-\\.\\.\\.-_-BARd", "FrIeNdLy-\\._\\.-bArD", "I borken you cache", "eb1ba5f5", "e8208120cae3ba69", "github_pat_[0-9a-zA-Z_]+", "LICENSEs", "astroid", ] [default.extend-identifiers] seeked = "seeked" # special term used for streams uv-0.9.17+ds1/assets/000077500000000000000000000000001520155276700142415ustar00rootroot00000000000000uv-0.9.17+ds1/assets/badge/000077500000000000000000000000001520155276700153035ustar00rootroot00000000000000uv-0.9.17+ds1/assets/badge/v0.json000066400000000000000000000073711520155276700165330ustar00rootroot00000000000000{ "label": "", "message": "uv", "logoSvg": "", "logoWidth": 10, "labelColor": "grey", "color": "#261230" } uv-0.9.17+ds1/assets/png/000077500000000000000000000000001520155276700150255ustar00rootroot00000000000000uv-0.9.17+ds1/assets/png/Astral.png000066400000000000000000000074321520155276700167670ustar00rootroot00000000000000‰PNG  IHDR0ÔÏUÐsRGB®ÎéDeXIfMM*‡i   0½ò_„IDATxí]|UÿvS6=›BB DŒH‘¢`¤X°¡`A¼ù©X@ð<ð,§¢‡g½COQA žg±âY@Ž"TŒ@BBÙôž]ßÿm&;[fgvwvãû~¿Íμùæ½oþ³ó½¯½‰e'e'†ÛâØŒ4•lÔËÀí‚€6¢zƒÍ¶Èúè>Kî:ð TÂ(~#Ó%Õ:Ç€7¬6š“gÙ±Äc²ˆŒ†yÞ˜Å1€@@  ›ÍÖhk°õ3r÷GË‚G ¨ À<EÑ¥FÄTTxÅa€@@ àÆ^F¨õ/Á*¨"À¬£Q•K0ŠÅGÀ»@@  Ž€P,ê €@ÀG„bñ0Á.¨# ‹:F‚C ð¡X|L° êŢޑà|D ÜG~ÁÞ6!¦?A)Ý#¾š¦F+mù¸‚^¾ÿY[‰Â# tã#½iÄÄDŠˆ |Þ:VÜD¯.,¢_UµËzÚù‰4÷™>íûJ;¿©¢§n9 tØ­}ÊÜî4yvº[»¿ Uå-ôîÓÅôõ»å¼‹î}Ltë?2©ÏÀ t™¯•-ÊÉÛQGKï, òâfsy2Íd¸«Ñÿ×UÐÒùjlŠ%`C«ƒ¨X#Ýýï(:.L7Á/¹9ŠòhÝËGéºÑE7¦éÖ·9-‚îaòΰ“šê­¼ß“ЮF±ly­/ͰÑÒ¯Ö>Ñן–ô¥7×Pé¡&šóL  õtU¾‘DRK“ž¸þWŠŒÒ†IL‚o˜¨ ¡Àø”¢Ð±hNzf›tU*ÒUfŽá›Ò·Ô®Ç7†îY‘ztÕé}„…(k@4·oFz ‘}Šw=ûÔ£/a±èbõa0vÌ«v¤~¥o½!A¿á‘:z*i}@Óz›˜õÔMU”²ÃÍÜSeô“A„­Ÿñ³åÓ Ajè¢XàóϯþÀî:úú»Oé GjϺä‡ûÆß‹¨¥™½&FθØLýGÆñ£…ûêé‹•Ç8‰Sà >²+µ²þ+Ž6ss>©M™“N‰Ý¼›×_¬,£Â} J]´·'¤„Óå·»Ëó-¥-´y…Ž4µŸƒ“O£Ó/2ó¶ «Ê¹íÄÀvzž`¢ó¯³?,¹ßVÓ÷_Tº²„Ä~Me 5ÖZ¹ëY] EÅé–'³´°ržÌþÑšøs7Tu¨bÑ,pbÔE±@©L™cêª[iË'T_m÷‡åXMš•΂cŽî­'{U,3Ì`Rª•¾y¯œš<+¢¸$¦XÚd)ßÞ³©šþ9s?•—4Ë›ùö¹L¥Œ»Éêv°­a÷ÆjMŠ%>)LU–ëô¢'™o¼å‡bÈßSGó—eS·^‘t깉tÇØ½ÔÚâ|½³eÑ)cæšç(‰ôí¯>THŸ½VFK· j¿ÇA/´P3º(ùh1ña„‡ôÃJåÍ„ áyצ:µyÛé74Æé?{äfÚ°Úâí4~lãšr*+²[¦˜0êÑ×DƒFÇÓÀ3ãé¡wshÞø½Š í×]u´›Í`žèHA£§f¯mÈ–ïwX9)=#éš»zò™úÆG2™bÉm?¿¾ÆJ/°ˆýoH˜m/½5V-v(±W%s¥‚V,($ËwÙÞYˆllf%³Š¥ˆK©­h ‘+b] ¼ô,]rS:­}±”l²ÉvÂ5©›NÇ7i2Ͼ"™ß¡í_VRÑ/ 4‰¹Pã®NѤX>Z^J{¾«qºÃƒ™bY¸*‡²NަqS“ݪ=ßUÓ+:ȬœýLYÉ ÊÖÒˆæË­°mŸW2Ëì}E Mc è›÷ËfÍo¤ÚÓ‰0Ý?£LÞeÈn¯xP;ÖqæÎÉh„,˜A$¸®Š1ˆQ““¨Ï€y¡™Å*Ú/uÒÍöäòûÑÌÜ÷FˆqžÒ¦XX q (–á ñ‹ªc¾Ï^¹ªy-Ä0ÖÇ')*oréu¬™Õ~€jØ,,W*RÿËî=DCdzkM§é÷gпf ©óíVN#K¹.¾£@bíïå÷$¸žrB ¹ßÐXyS@Û˜hN:Í?óÖêX>ZVJ•e¾ßsoýús¬$¿‘Þ^tØíÔ9¬¦Æ¨CPü“Giß÷Î"ÜÞqSSÜÆ ö] â!ï?[Bó^ÈfŠ ­]± ¸‹c¨u@pW-X·%¥‡=½ˆàäÑÂ&ÂeŠ6r…³ŽY$þÐ!¦  X¤ôŸ§>’Ò#èÄáî¬1ŽÑqŽÐ}2».釲jq‰'ø8Ëÿzãë uÀôŸ'SÉß]2)4"–UyÔùA>í<³®Šåd”ÇG 5)‰æŽÚ£…Õ/öi¡Ê²fZÿ¦{á¶§û9n±[WÖV™éîvÔÑüÛUή>~óÒïÅÁé¾Õ '÷A|hÑY±˜èÙ¹ål–íÅ ú Œ¦ü=õ,V`Ï­fRZoõzÉ *>Ð@‡µ?D0ÿñãÏ4KK“ÝRˆ3+_öf)áãJKþœÏƒ®íjû}Ôß#Ë’;X¯+»3Ȭ»*…†Ÿ“H³ŸÊâ} þ³æyÏÊÈã ~6.Z?€UÑ:? f–u;^Ôû¤hêÁêoüQìZdÎÛQ«…P£óÒƒÝx½M”-ÍVþ ¸ä¡á¦Ç3iú‚ §#p›C‘týµDņñ²nd+f>šÉÊê%%4dlU”6Ó—ÿ=FÃÆ{¯r°‘`&¦z/ € °rQj D(EÈe1°·+±8µC†ôL“S›ë{K=¯²E;á{Ï·'\y]÷qM‰®!º¯«b‘0ÀL|õ_z²d2ˆÐþáKGxù±Ä£ô=œ¥YãíbÁ$—hÓÚ šµÈÊÖ¢0ÓÍäp |¥ð¶z Ä6”hã ½Ìâ@zâ#®Á[m¡ãþ•ýhæ]Tmq(ù¸pá ŽžœL?o«¡ý¹õòÃA¹uCÛêlô°–Å}n–KgN2Ó=+ú©v+÷áiyª|z2 ³;›ä‰’ÎÛÓx¢Xë¬Ü]r9iDÕ³ûãWŽzß­mì•7ä±µÎn„drÂçôG±däDññ ößtûú*.,¼Á,8'·Ì\‘\kǮü¿í³JZxõ/öEˆl†W#ÄO:jýJ*³¼ÎùƒzàuM?|i¿'jòª‡Åí:æÎ¯«5[-jýzK#\åóÖç¾ïkéÐÏŽr o¼òc¢X0ÀZÉŸÌ Ö|úœY0µ•žge¹0¨u1Ñ^yŠÌI˜‹tÍ6žžEš¶ÿˆXúi«6ßc ÝöJ»ÙúÓVû2sWþ¶¶dÛg4ê²džvF½‡kÉ;Î9³M©¸ž¿›¿¡¤ßÛú¤¾ƒbOôÃÿª|V,×·µ¹ö×ʲ.Ëî=ÈSéòc¨Fåòñ&Ô±|õv9û¢X°ÞIKzË+BU± lëCÁ¥Þ„Êñx¶Dåw¯Xv·Í<®%æXd÷â]pG‘‘T¿wIH„„ÍþÜ:ÂMÛ!{©t\ú^Í2Nx·(™½¨NP¥¥½_ÞÐöGë"DÌ j‹!»B0VºFW~,B„O¿õÓJòÔßöõ•,˜ë\öÄëÚw(íÏx(ƒ¦±œžï? ¥ëïê²êb±  O+CNp%<•q#ªýúÃErVÛû¶Õ>®„ÊLOýºò)íË×ä(ñhm‡è¯,ÈJáÓÕ ™¿¸®’[íê7ËëSÛûÑ©8¥ë  ¡ôÇéba‘uÉûE=K0¯ØƒÌ%ƒP,…ôïd¸³zâ=%²w×ä³rĨŽ7[íÈñÆC>¾.®¼C±Ü p­¥Š/‚ñêU UÇxõJX†õa‘êXøI^þ ;…—iKï»+ÖJ=77Ÿ¿L 5¥š¶Åzæ¶kÁuY ìiá—b­QSƒº¹‹ õKZú––9`1«~_±BMš?dÈIzüU¿?’‹sàDÀf{\¸BÁyk„TF@(–¾}Bx@p" KpÞ!•@ ¤Š%¤oŸ^ œÅœ÷EH%i„b éÛ'„'B±ç}R B#{•ž¨c é[(„V2Xd õ•Á%·F j¬Eøo(﵌B8€@ d`P#5ÐÆ[õߨNÇýÓ–D*Š€ óóêw†Y,æèØ•FC¤‰[z“â Œ@œ/ü>°‘­Á`£Íléä¬_,»ÞÄUÿø)ôkDy"OIEND®B`‚uv-0.9.17+ds1/assets/png/install-cold.png000066400000000000000000004420471520155276700201330ustar00rootroot00000000000000‰PNG  IHDR@è†{2ƒCîIDATxíà$I’$I‹ª™»GDDfffVUUUUwwwww÷ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌtwwwwWWUUUUffFFD„»›™ ÏLfWwuwwOÏÌÌÌÌL¢l›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«þß¹õÖ[ù™Ÿùþú¯ÿš[o½•ÝÝ]þú¯ÿ€×~í×àµ_ûµyé—~iÞê­ÞŠÿÉvwwù›¿ùèµ^ëµøÏð×ý×\ºt‰û=èAâÁ~0ÿ‘vwwù›¿ùè¥^ê¥8~ü8Wý÷ºõÖ[yÆ3žÁýŽ;ÆK¿ôKóüüÎïüôR/õR?~œÿ~çw~‡z­×z-®ºêª«®ºêª«®ºê8dÛ\uÕUW]uÕUW]õÿÆ÷|Ï÷ðÕ_ýÕüõ_ÿ5/ªãÇóÑýÑ|ÔG}Ççšßþíßæu^çux Ûügxí×~m~çw~‡û}Ög}Ÿýٟͤßþíßæu^çux ßú­ßâµ_ûµùÿfww—¿þë¿æµ_ûµùŸà³?û³ùœÏùî÷Z¯õZüöoÿ6Ï$è·~ë·xí×~mþ'ÙÝÝåÖ[oå¥_ú¥ya$ñ@¶¹êª«®ºêª«®ºêªÿámsÕUW]uÕUW]uÕÿy¿ýÛ¿ÍÇ|ÌÇð×ý×ü[=øÁ櫾ê«xë·~kþ'ùíßþm^çu^‡²Í†×~í׿w~çw¸ßg}ÖgñÙŸýÙüGúíßþm^çu^‡ú­ßú-^ûµ_›ÿO¾æk¾†ÏþìÏæ£>ê£øìÏþlþ'øìÏþl>çs>‡û½Ök½¿ýÛ¿Íó#‰ú­ßú-^ûµ_›ÿ)¾ç{¾‡þèæ£>ê£øìÏþl^Iæcøë¿þk>ë³>‹ÏþìÏæ‚ÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛë³>‹ÏþìÏæ‚ÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛû³?›æ³?û³y ÏþìÏæª«®ºêª«®ºêª«þ‡C¶ÍUW]uÕUW]uÕUÿçüôOÿ4oó6oÃsû®ïú.Þû½ß›­÷~ï÷æ{¾ç{x ?øÁ<ýéOç¿Ûoÿöoó:¯ó:ë³>‹ÏþìÏæ?Òoÿöoó:¯ó:<ÐoýÖoñÚ¯ýÚüðÚ¯ýÚüÎïü÷û¬Ïú,>û³?›ÿ >û³?›ÏùœÏá~¯õZ¯ÅoÿöoóüHâ~ë·~‹×~í׿¿Ûk¿ökó;¿ó;Üï³>ë³øìÏþl®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]õÊîî./ó2/í·ÞÊ}×w}ïýÞïÍ¿ÕK¿ôKó7ó7<Ðw}×wñÞïýÞüwúíßþm^çu^‡²Í†×~í׿w~çw¸ßg}ÖgñÙŸýÙüGúíßþm^çu^‡ú­ßú-^ûµ_›ÿ^ûµ_›ßùßá~ŸõYŸÅgögó?Ágögó9Ÿó9Üïµ^ëµøíßþmžI<ÐoýÖoñÚ¯ýÚüw{í×~m~çw~‡û}Ög}ŸýÙŸÍUW]uÕUW]uÕUWýƒl›«®ºêª«®ºêª«þOùê¯þj>æc>†ú¨ú(¾ú«¿šßþíßæu^çux ?øÁ<ýéOç¿Óoÿöoó:¯ó:ë³>‹ÏþìÏæ?Òoÿöoó:¯ó:<ÐoýÖoñÚ¯ýÚüðÚ¯ýÚüÎïü÷û¬Ïú,>û³?›ÿ >û³?›ÏùœÏá~¯õZ¯ÅoÿöoóüHâ~ë·~‹×~í׿¿Ûk¿ökó;¿ó;Üï³>ë³øìÏþl®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]õÊCòn½õVîwìØ1n½õVŽ?ο×[¿õ[ó3?ó3<Ð_ýÕ_ñÒ/ýÒüküõ_ÿ5—.]â¯ÿú¯ÙÝÝåµ_ûµx­×z-þµ~û·›×y×ál󯱻»ËßüÍßð×ý×ìîîòÒ/ýÒ<èAâ¥_ú¥y ×~í׿w~çw¸ßg}ÖgñÙŸýÙüGúíßþm^çu^‡ú­ßú-^ûµ_›«ÝÝ]þæoþ†ÝÝ]þú¯ÿš—~é—æøñã¼Ök½ÿ^»»»üÍßü ·Þz+·Þz+Çç¥_ú¥9vì/ýÒ/Í¿Ök¿ökó;¿ó;Üï³>ë³øìÏþlþ­~çw~€¿þë¿fww—?øÁ<øÁæØ±c¼ôK¿4ÿŸýÙŸÍç|Îçp¿×z­×â·û·y~$ñ@¿õ[¿Åk¿ökó¯õ;¿ó;üõ_ÿ5»»»<øÁæÁ~0ÇŽã¥_ú¥ù×zí×~m~çw~‡û}Ög}ŸýٟͶÝÝ]þæoþ†ÝÝ]þú¯ÿš?øÁ<øÁæAz~ðƒùÏð×ý×\ºt‰¿þë¿à¥_ú¥yЃăü`®ºêª«®ºêª«®ú?Ù6W]uÕUW]uÕUWýŸñÓ?ýÓ¼ÍÛ¼ ô^ïõ^|÷w7ÿ~ú§š·y›·áµ^ëµxí×~m^ûµ_›—~é—æøñãüKn½õV>çs>‡ŸþéŸfww—ä­ßú­ù¨ú(^ûµ_›Åoÿöoó:¯ó:çs>‡û½Ök½¿ýÛ¿Íó#‰ú­ßú-^ûµ_›Éîî.ßó=ßÃOÿôOóÛ¿ýÛ¼0Ççµ_ûµù¨ú(^ûµ_›äµ_ûµùßù^¯õZ¯Åoÿöoó@’x Û¼¨¾ç{¾‡¯þê¯æ¯ÿú¯yAüàóÞïýÞ|ÔG}ÇçEñÚ¯ýÚüÎïü÷³Íý¾æk¾†¯þê¯æÖ[oåùyé—~i>ú£?š÷z¯÷⪫®ºêª«®ºêªÿ³msÕUW]uÕUW]uÕÿýÑÍ×|Í×ð@¿õ[¿Åk¿ökóßés>çsøìÏþlþ5^ûµ_›Ÿú©Ÿâøñã¼0¿ýÛ¿Íë¼Îëð@¶ù—|ÌÇ| _ýÕ_Í‹âµ_ûµù©Ÿú)Þú­ßšßùßá~ŸõYŸÅgögóé·û·y×yè·~ë·xí×~mžŸßþíßæu^çu¸ßg}ÖgñÙŸýÙ|Í×| ŸýÙŸÍîî./Нúª¯â£?ú£ù—ìîîò:¯ó:üõ_ÿ5ÿ¯ýÚ¯ÍOýÔOqüøqžÛk¿ökó;¿ó;¼(^ëµ^‹ßþíßæùùš¯ù>û³?›ÝÝ]þµÞû½ß›ïú®ïâ…ùìÏþl>çs>‡û½Ök½¿ýÛ¿Íó#‰ú­ßú-^ûµ_›滿û»ù˜ùvwwù×zï÷~o¾ë»¾‹ççµ_ûµùßù^¯õZ¯Åoÿöoó@’x ÛüKþú¯ÿš·y›·áÖ[oåEuüøq¾ê«¾Š÷~ï÷æ_òÚ¯ýÚüÎïü÷³Í­·ÞÊÛ¼ÍÛð×ý×¼(^ú¥_šßú­ßâøñã\uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ú?ã!y·Þz+d›ÿ.»»»¼Îë¼ý×ͿŃü`~ê§~Š—~é—æùíßþm^çu^‡²Í ó>ïó>|÷w7ÿ/ýÒ/mþæoþ†û}Ög}Ÿýٟͤßþíßæu^çux ßú­ßâµ_ûµy~~û·›×y×á~ŸõYŸÅñãÇù˜ùþµ¾ë»¾‹÷~ï÷æÙÝÝåu^çuøë¿þkþ-^ú¥_šßú­ßâøñã<Ðk¿ökó;¿ó;¼(^ëµ^‹ßþíßæ¹½Ïû¼ßýÝßÍ¿Ç[¿õ[óS?õS¼ ŸýÙŸÍç|Îçp¿×z­×â·û·y~$ñ@¿õ[¿Åk¿ökó‚¼Ïû¼ßýÝßÍ¿Ç[¿õ[óS?õS<·×~í׿w~çwxQ¼Ök½¿ýÛ¿ÍIâlóÂ|÷w7ïó>ïÿÕGôGóU_õU¼0¯ýÚ¯ÍïüÎïp¿‹/ò‡<„ÝÝ]þ5^ú¥_šßú­ßâøñã\uÕUW]uÕUW]õ ²m®ºêª«®ºêª«®ú?CôZ¯õZüöoÿ6ÿ]ÞæmÞ†ŸþéŸæŽ;Æ{¿÷{óÖoýÖ¼ôK¿4Çç·û·ùë¿þk¾ú«¿šg<ã<ÐñãÇyúÓŸÎñãÇy~~û·›×y×áló‚|ôG4_ó5_Ã;vŒ÷~ï÷æ­ßú­yí×~mn½õVþú¯ÿšïþîïæg~ægxA>ë³>‹ÏþìÏæ?Òoÿöoó:¯ó:<ÐoýÖoñÚ¯ýÚïÃ;vŒ÷~ï÷æ­ßú­yé—~iŽ?À_ÿõ_ó×ý×|õW5ó7Ãsû­ßú-^ûµ_›çç³?û³ùœÏùî÷Z¯õZüöoÿ6Ï$è·~ë·xí×~mžŸŸþéŸæmÞæmx cÇŽñÞïýÞ¼õ[¿5/ýÒ/ÍñãÇøë¿þkþú¯ÿš¯þê¯æoþæoxn¿õ[¿Åk¿ökó@ßýÝßÍ­·Þ Àw÷wóŒg<ƒû½Ök½¯ýÚ¯ÍýüàóÞïýÞ<$È6/Èoÿöoó:¯ó:<·×z­×â½ßû½yé—~i^ú¥_š[o½•¿þë¿æ§ú§ùžïùžÛg}ÖgñÙŸýÙ¼ ¯ýÚ¯ÍïüÎïp¿—~é—æ¯ÿú¯¹ß{½×{ñÚ¯ýÚ<øÁàÖ[o廿û»ùßùžÛg}ÖgñÙŸýÙ\uÕUW]uÕUW]õ ²m®ºêª«®ºêª«®ú?á¯ÿú¯y™—yè³>ë³øìÏþlþ;|ög6Ÿó9ŸÃ½ÔK½?ýÓ?̓ü`žŸÝÝ]>ú£?šïùžïá^ú¥_š¿ú«¿âùùíßþm^çu^‡²ÍóóÛ¿ýÛ¼Îë¼ôR/õRüôOÿ4~ðƒy~¾û»¿›÷yŸ÷áùù¬Ïú,>û³?›ÿH¿ýÛ¿Íë¼Îëð@¿õ[¿Åk¿ökóüüöoÿ6¯ó:¯ÃóóYŸõY|ög6ÏÏîî.oýÖoÍïüÎïð@_õU_ÅGôGóÜvww9qâô]ßõ]¼÷{¿7/Èîî.¯ýÚ¯ÍßüÍßp¿ãÇsñâE^×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼ »»»<ä!aww—û½ÔK½¿ýÛ¿ÍñãÇya¾ú«¿šù˜áÞë½Þ‹ïþîïæùùìÏþl>çs>‡û½Ök½¿ýÛ¿Íó#‰ú­ßú-^ûµ_›ç¶»»Ë˼ÌËpë­·r¿—z©—â·û·9~ü8/ÌWõWó1ó1<Ð{½×{ñÝßýݼ ¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³ya$ñ@¶y~vwwyÈCÂîî.ôU_õU|ôG4/ÈoÿöoóÖoýÖ\ºt‰ú­ßú-^ûµ_›ççµ_ûµùßùžÛK½ÔKñÝßýݼôK¿4ÏÏw÷wó>ïó><Ѓü`žþô§sÕUW]uÕUW]uÕÿ)ȶ¹êª«®ºêª«®ºêÿ„ßþíßæu^çux Ïú¬Ïâ³?û³ù¯¶»»ËCòvww¹ßK½ÔKñÛ¿ýÛ?~œÉ{¿÷{ó=ßó=<Ðw}×wñÞïýÞ<·ßþíßæu^çux Ûû³?›ÏùœÏá~ïõ^ïÅw÷wó¢xé—~iþæoþ†×z­×âøñã|ôG4¯ýÚ¯ÍóóÚ¯ýÚüÎïü÷û¬Ïú,>û³?›ä­ßú­ù™Ÿùî÷^ïõ^|÷w7/ª¯þê¯æc>æcx Ûæc>†²Í òÚ¯ýÚüÎïü÷û¬Ïú,>û³?›Fd›ç¶»»ËCòvww¹ß[½Õ[ñÓ?ýÓ¼¨¾ú«¿šù˜á~ë·~‹×~í׿¹½ök¿6¿ó;¿ÃýÔOýoýÖoÍ¿äÖ[oå!yô[¿õ[¼ök¿6W]uÕUW]uÕUWýŸl›«®ºêª«®ºêª«þOøíßþm^çu^‡ú­ßú-^ûµ_›ÿj/ó2/Ã_ÿõ_s¿·z«·â§ú§ù×øîïþnÞç}Þ‡zúӟ΃ü`è·û·y×yÈ6Ïí»¿û»yŸ÷yîwìØ1vwwyQýõ_ÿ5/ó2/Ã}Ög}Ÿýٟͤßþíßæu^çux ßú­ßâµ_ûµy~~û·›×y×á>ë³>‹ÏþìÏæEñÝßýݼÏû¼d›çöÕ_ýÕ|ÌÇ| ÷;~ü8OúÓ9~ü8ÿ‘^ûµ_›ßùßá~ŸõYŸÅgögó‚üõ_ÿ5¿ýÛ¿Íîî.ý×ÍGôGóÚ¯ýÚ¼¨~û·›×y×álóü|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæù‘ÄýÖoý¯ýÚ¯Ísûë¿þk~û·›ÝÝ]þú¯ÿš÷~ï÷æ­ßú­yQýöoÿ6¯ó:¯ÃÙæyí×~m~çw~‡û}Ög}ŸýÙŸÍ #‰²ÍsûéŸþiÞæmÞ†zúӟ΃ü`þ5üàóŒg<ƒû½×{½ßýÝßÍs{í×~m~çw~‡û;vŒÝÝ]^TÇçÒ¥KÜï³>ë³øìÏþl®ºêª«®ºêª«®ú?Ù6W]uÕUW]uÕUWýŸðÛ¿ýÛ¼Îë¼ô[¿õ[¼ök¿6ÿ•vww9qâôS?õS¼õ[¿5ÿ»»»œ8q‚ú®ïú.Þû½ß›úíßþm^çu^‡²Ís{ï÷~o¾ç{¾‡û½Õ[½?ýÓ?Ϳƃü`žñŒgp¿Ïú¬Ïâ³?û³ùôÛ¿ýÛ¼Îë¼ô[¿õ[¼ök¿6ÏÏoÿöoó:¯ó:<ÐoýÖoñÚ¯ýÚ¼(~û·›×y×álóÜþú¯ÿš—y™—á^ú¥_š¯úª¯âµ_ûµùòÚ¯ýÚüÎïü÷û¬Ïú,>û³?›ÿ,¿ýÛ¿Íë¼Îëð@¶y~>û³?›ÏùœÏá~¯õZ¯ÅoÿöoóüHâ~ë·~‹×~í׿?Úoÿöoó:¯ó:ë³øìÏþl®ºêª«®ºêª«®ú?Ù6W]uÕUW]uÕUWýŸðÛ¿ýÛ¼Îë¼ôU_õU|ôG4ÿ•~û·›×y×ážþô§óà?˜­—~é—æoþæo¸ßG}ÔGñÕ_ýÕ<Ðoÿöoó:¯ó:ë³øìÏþlþ5Þú­ßšŸù™Ÿá~ŸõYŸÅgögóé·û·y×yè·~ë·xí×~mžŸßþíßæu^çux ‹/rüøq^¿ýÛ¿Íë¼Îëð@¶y~üàóŒg<ƒçvüøqÞú­ßš×~í׿­Þê­8~ü8ÿV¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³ùö×ý×üÎïü?ýÓ?Íoÿöoó@¶y~>û³?›ÏùœÏá~¯õZ¯ÅoÿöoóüHâ~ë·~‹×~í׿?Ê_ÿõ_ó;¿ó;üôOÿ4¿ýÛ¿ÍÙæyí×~m~çw~‡û}Ög}ŸýÙŸÍ #‰²Ís{í×~m~çw~‡û}ÔG}_ýÕ_Í¿ÖOÿôOó6oó6ë³øìÏþl^T¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³¹êª«®ºêª«®ºêÿ dÛ\uÕUW]uÕUW]õÂoÿöoó:¯ó:<Ðg}ÖgñÙŸýÙüWúéŸþiÞæmÞ†²Í¿Å[¿õ[ó3?ó3Üïµ^ëµøíßþmè·û·y×yÈ6ÏMôS?õS¼õ[¿5ÿŸýÙŸÍç|Îçp¿Ïú¬Ïâ³?û³ùôÛ¿ýÛ¼Îë¼ô[¿õ[¼ök¿6ÏÏoÿöoó:¯ó:û³?›‹ÝÝ]þæoþ†[o½•[o½•¿þë¿æÖ[oå¯ÿú¯yalóü|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæù‘ÄýÖoý¯ýگͿÆîî.ó7í·ÞÊ­·ÞÊ_ÿõ_së­·ò×ý×¼0¶yA^ûµ_›ßùßá~ŸõYŸÅgögóÂHâlóÜò‡pë­·r¿Ïú¬Ïâ³?û³ù×úíßþm^çu^‡ú­ßú-^ûµ_›zí×~m~çw~‡û}Ög}ŸýÙŸÍ‹êµ_ûµùßùî÷YŸõY|ög6W]uÕUW]uÕUWýŸl›«®ºêª«®ºêª«þÏÄ}ÔG}_ýÕ_Í¥ÏþìÏæs>çs¸ß±cÇØÝÝåßâ³?û³ùœÏùî÷Z¯õZüöoÿ6ôÛ¿ýÛ¼Îë¼d›ç&‰ú­ßú-^ûµ_›ÏþìÏæs>çs¸ßg}ÖgñÙŸýÙüGúíßþm^çu^‡ú­ßú-^ûµ_›çç·û·y×yÈ6/ªßþíßæu^çux Û¼ ?ýÓ?Í{¿÷{séÒ%^Çç­ßú­ù¨ú(^ú¥_šÉk¿ökó;¿ó;Üï³>ë³øìÏþl^»»»üÌÏü ?ýÓ?Íoÿöo³»»Ë¿…mžŸÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛë³øìÏþl^T¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³¹êª«®ºêª«®ºêÿ dÛ\uÕUW]uÕUW]õƃü`žñŒgp¿—~é—æ¯þê¯ø¯ôÙŸýÙ|Îç|÷{­×z-~û·›‹ÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛ<Ðoÿöoó:¯ó:çs¸ßñãǹxñ"ÿŸýÙŸÍç|Îçp¿×z­×â·û·y ßþíßæu^çux Û<7I<ÐoýÖoñÚ¯ýÚük|õW5ó1Ãý>ë³>‹ÏþìÏæ?Òoÿöoó:¯ó:<ÐoýÖoñÚ¯ýÚë³>‹ÏþìÏæª«®ºêª«®ºêªÿ3msÕUW]uÕUW]uÕÿ»»»œ8q‚zí×~m~ë·~‹ÿßýÝßÍû¼Ïûð@õQÅWõWs¿ßþíßæu^çux ‹/rüøqþµ^æe^†¿þë¿æ~õQÅWõWó@¿ýÛ¿Íë¼Îëð@¶yn~ðƒyÆ3žÁý>ë³>‹ÏþìÏæ_ã£?ú£ùš¯ùî÷YŸõY|ög6ÿ‘~û·›×y×á~ë·~‹×~í׿ùùíßþm^çu^‡²Í‹ê·û·y×yÈ6ÿ~ú§šïþîïæg~ægxnOúÓyðƒÌs{í×~m~çw~‡û}Ög}ŸýÙŸÍ râÄ vww¹ß±cÇøîïþnÞú­ßšÅOÿôOó6oó6çs¸ßk½ÖkñÛ¿ýÛë³øìÏþl^T¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³¹êª«®ºêª«®ºêÿ dÛ\uÕUW]uÕUW]õÊ{¿÷{ó=ßó=<ÐoýÖoñÚ¯ýÚü{½Ì˼ ý×ÍýÕ_ý/ýÒ/Íývww9qâôS?õS¼õ[¿5ÿZ’x ¯úª¯â£?ú£y ßþíßæu^çux Û<··~ë·æg~æg¸ß[½Õ[ñÓ?ýÓük¼ök¿6¿ó;¿Ãý>ë³>‹ÏþìÏæ?Òoÿöoó:¯ó:<ÐoýÖoñÚ¯ýÚçs>‡²ÍóóÙŸýÙ|Îç|÷{­×z-~û·›çGô[¿õ[¼ök¿6ôÓ?ýÓ¼ÍÛ¼ ôU_õU|ôG4/ªÏþìÏæs>çsx Û¼ ¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³ya$ñ@¶ynïýÞïÍ÷|Ï÷p¿—~é—æ¯þê¯ø×úèþh¾æk¾†û;vŒÝÝ]žÛk¿ökó;¿ó;Üï³>ë³øìÏþl^T¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³¹êª«®ºêª«®ºêÿ dÛ\uÕUW]uÕUW]õÊoÿöoó:¯ó:<ÐK¿ôKóWõWü{|õW5ó1ýÖk½¿ýÛ¿Ís{ðƒÌ3žñ î÷^ïõ^|÷w7ÿßýÝßÍû¼Ïûð@õWÅK¿ôKó@¿ýÛ¿Íë¼Îëð@¶yn_ýÕ_ÍÇ|ÌÇð@/^äøñã¼(vww9qâôYŸõY|ög6ÿ‘~û·›×y×á~ë·~‹×~í׿ùùíßþm^çu^‡²Í‹ê·û·y×yÈ6të­·ò=ßó=üöoÿ6»»»üõ_ÿ5/^äøñ㼨$ñ@ŸõYŸÅgögóÜ^ûµ_›ßùßá~ŸõYŸÅgögóü|ög6Ÿó9ŸÃÙæ_ã­ßú­ù™ŸùÈ6ÏÏgögó9Ÿó9Üïµ^ëµøíßþmžI<ÐoýÖoñÚ¯ýÚ<Ðgögó9Ÿó9ë³>‹ÏþìÏæ…‘ÄÙæ¹}÷w7ïó>ïÃ=ýéOçÁ~0ÿyÈC¸õÖ[¹ß[½Õ[ñÓ?ýÓ<·×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼¨^ûµ_›ßùßá~ŸõYŸÅgögsÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêÿœ×~í׿w~çwx ÷~ï÷æ»¾ë»ø·øë¿þk^çu^‡ÝÝ]è·~ë·xí×~mžÛgögó9Ÿó9<ÐÓŸþtüàó¢z™—yþú¯ÿšû=èAâÖ[oå¹ýöoÿ6¯ó:¯ÃÙæ¹íîîrâÄ è«¾ê«øèþh^ŸýÙŸÍç|Îçð@ŸõYŸÅgögóé·û·y×yè·~ë·xí×~mžŸßþíßæu^çux Û¼¨~û·›×y×álóÜ$ñ@ßõ]ßÅ{¿÷{ó¢:qâ»»»Üï§~ê§xë·~kžÛk¿ökó;¿ó;Üï³>ë³øìÏþlžŸÏþìÏæs>çsx Û¼¨þú¯ÿš—y™—á¹ÙæùùìÏþl>çs>‡û½Ök½¿ýÛ¿Íó#‰ú­ßú-^ûµ_›úìÏþl>çs>‡²Í‹êÖ[oå!yÏÍ6/Èk¿ökó;¿ó;Üï³>ë³øìÏþl^Iû³?›çç³?û³ùœÏùèéO:~ðƒyQ¼Ì˼ ý×Ís³ÍóóÙŸýÙ|Îç|÷{­×z-~û·›çGô[¿õ[¼ök¿6ôÙŸýÙ|Îç|ôWõW¼ôK¿4/Š—y™—á¯ÿú¯yn¶yA^ûµ_›ßùßá~õQÅWõWóÂHâlóü¼÷{¿7ßó=ßÃýÖoý¯ýگͿdww—‡<ä!ìîîr¿cÇŽ±»»ËóóÚ¯ýÚüÎïü÷û¬Ïú,>û³?›Õk¿ökó;¿ó;Üï³>ë³øìÏþl®ºêª«®ºêª«®ú?Ù6W]uÕUW]uÕUWýŸôÝßýݼÏû¼Ïíøñã|ög6ïõ^ïÅñãÇyAn½õV>çs>‡ïþîïæ¹=èAâ¯ÿú¯9~ü8/ÈGôGó5_ó5<Ð[¿õ[ó]ßõ]?~œäk¾ækøèþhèAzý×ÍñãÇyn¿ýÛ¿Íë¼Îëð@¶y~n½õV^ú¥_šK—.q¿ãÇó[¿õ[¼ôK¿4ÏÏ­·ÞÊÛ¼ÍÛð×ý×<·Ïú¬Ïâ³?û³ùôÛ¿ýÛ¼Îë¼ô[¿õ[¼ök¿6ÏÏoÿöoó:¯ó:û³?›Õk¿ökó;¿ó;Üï³>ë³øìÏþl®ºêª«®ºêª«®ú?Ù6W]uÕUW]uÕUWýŸõÝßýݼÏû¼/È[¿õ[óÒ/ýÒ¼ôK¿4Çç¯ÿú¯ÙÝÝå·û·ùíßþmžŸcÇŽñÛ¿ýÛ¼ôK¿4ÿ’—~é—æoþæox ãÇóÙŸýÙ¼Õ[½~ðƒØÝÝåw~çwøê¯þj~û·›çöWõW¼ôK¿4ÏÏoÿöoó:¯ó:æcx ãÇóÑýѼ×{½~ðƒ¸õÖ[ùžïù¾ú«¿šÝÝ]žŸÏú¬Ïâ³?û³ùôÛ¿ýÛ¼Îë¼ô[¿õ[¼ök¿6ÏÏoÿöoó:¯ó:çs¸ßk½ÖkñÛ¿ýÛçs>‡zðƒÌK¿ôKð =ˆ¯þê¯æ$ñ@¶yA¾û»¿›÷yŸ÷á¹½÷{¿7oýÖoÍ[½Õ[q¿¿þë¿æ{¾ç{øîïþnvwwy ÷z¯÷⻿û»yA^ûµ_›ßùßá~ŸõYŸÅgögó¢zí×~m~çw~‡û}Ög}ŸýÙŸÍUW]uÕUW]uÕUÿg Ûæª«®ºêª«®ºêªÿÓ¾û»¿›þèæÒ¥Kü{½ÔK½ßýÝßÍK¿ôKó¢ØÝÝåµ_ûµù›¿ùþ-Ž;Æw÷wóÖoýÖ¼ ¿ýÛ¿Íë¼Îëð@¶yaÞû½ß›ïùžïá_ãØ±c¼õ[¿5ßó=ßÃý>ë³>‹ÏþìÏæ?Òoÿöoó:¯ó:<ÐoýÖoñÚ¯ýÚïó>ü[½Õ[½ßýÝßÍñãÇyA^ûµ_›ßùßá~ŸõYŸÅgögó¢zí×~m~çw~‡û}Ög}ŸýÙŸÍUW]uÕUW]uÕUÿg Ûæª«®ºêª«®ºêªÿóþú¯ÿšþèæw~çwø·ú¬Ïú,>û³?›­ÝÝ]Þû½ß›Ÿù™Ÿá_ã¥^ê¥øîïþn^ú¥_šæ·û·y×yÈ6ÿ’¯þê¯æc>æcxQ;vŒßþíßæ§ú§ùœÏùî÷YŸõY|ög6ÿ‘~û·›×y×á~ë·~‹×~í׿ùùíßþm^çu^‡²Í‹ê·û·y×yÈ6/È_ÿõ_óÞïýÞüÍßü ÿÇŽã£?ú£ùìÏþlþ%ïýÞïÍ÷|Ï÷ð‚\¼x‘ãÇs¿ïþîïæ}Þç}ø×x­×z-¾û»¿›?øÁ¼÷{¿7ßó=ßÃýÞë½Þ‹ïþîïæ¹}ög6Ÿó9ŸÃý^ëµ^‹ßþíßæù‘ÄýÖoý¯ýÚ¯ÍóóÝßýݼÏû¼ÿ¯õZ¯ÅWõWóÒ/ýÒ¼÷{¿7ßó=ßÃýÞë½Þ‹ïþîïæyï÷~o¾ç{¾‡äéO:~ðƒ¹Ÿ$È6ÿ’ïþîïæ£?ú£¹téÿŸõYŸÅgögó/yí×~m~çw~‡û}Ög}ŸýÙŸÍ‹êµ_ûµùßùî÷YŸõY|ög6W]uÕUW]uÕUWýŸl›«®ºêª«®ºêª«þßøíßþm¾û»¿›ŸþéŸæÒ¥KüKô ñÞïýÞ¼÷{¿7~ðƒù÷øíßþm¾ú«¿šŸù™Ÿá…y©—z)>ú£?š÷~ï÷æEñÛ¿ýÛ¼Îë¼d›Å­·ÞÊgögó=ßó=¼ oõVoÅw÷wsüøq>û³?›ÏùœÏá~ŸõYŸÅgögóé·û·y×yè·~ë·xí×~mžŸßþíßæu^çux Û¼¨~û·›×y×áló/ùê¯þj¾û»¿›¿ù›¿á…yЃÄk¿ökóÙŸýÙ<øÁæEõÑýÑ|÷w7—.]â¹ýÖoý¯ýÚ¯Íýõ_ÿ5ýÑÍïüÎïð¼Õ[½ýÑÍk¿öks¿ŸþéŸæmÞæmx §?ýé<øÁæ>û³?›ÏùœÏá~¯õZ¯ÅoÿöoóüHâ~ë·~‹×~í׿ùë¿þk>ú£?šßùßá…y«·z+>ú£?š×~í׿~¿ýÛ¿Íë¼Îëð@OúÓyðƒÌ òÙŸýÙ|õW5—.]â¹ýÖoý¯ýÚ¯Íý$ñ@¶yQìîîòÕ_ýÕ|÷w7ÏxÆ3xAŽ;Æ[¿õ[óÙŸýÙ<øÁæEñÚ¯ýÚüÎïü÷û¬Ïú,>û³?›Õk¿ökó;¿ó;Üï³>ë³øìÏþl®ºêª«®ºêª«®ú?Ù6W]uÕUW]uÕUWý¿ô×ý×Üzë­üõ_ÿ5ÏíÁ~0¯ýگ̓ü`þ£íîîò×ý×üöoÿ6ôÒ/ýÒ¼ôK¿4~ðƒù¯¶»»Ëoÿöoó×ý×Üï¥_ú¥yí×~mŽ?ÎU/Ø­·ÞÊ_ÿõ_ó×ý×<Ѓü`^ú¥_š—~é—æßã·û·y —~é—æøñã¼ ·Þz+ý×Í_ÿõ_ó@¯ýÚ¯ÍK¿ôKsüøqþ'»õÖ[ùë¿þkþú¯ÿšzí×~m^ú¥_šãÇóé·û·y —~é—æøñãüGûë¿þkþú¯ÿš[o½•û?~œ—~é—æµ_ûµ¹êª«®ºêª«®ºêªÿ`ȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUÿCíîîò9Ÿó9|÷w7ïýÞïÍW}ÕWqÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õB Ûæª«®ºê¨þèæk¾ækx ú¨â«¿ú«¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®zmsÕUW]õ?Ô‰'ØÝÝåŽ?ÎÅ‹¹êªIk¿ýÛ¿åe^æe¸êªg<ãlnnrúôi®ºêEµ\.yÚӞƋ½Ø‹qÕUÿOyÊS8sæ ÇŽ㪫^T{{{ÜsÏ=<ò‘䪫þ5÷¸Çñà?˜ ®ºêEuþüyöööxÈCÂUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏÏû½ösÕÿ.z©Ó|ÂW¿ÿ•V«_ýÕ_Í'ò'sÕUÿ?û³?ËÍ7ßÌ˼ÌËpÕU/ª{ï½—ŸüÉŸäC>äC¸êªüÁä^áxÄ#ÁUW½¨žúÔ§ò‡ø‡¼Ç{¼W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žŸ÷{íçªÿ]õR§ù„¯~-þ+­V+¾ú«¿šOþäO檫þ5~ög–›o¾™—y™—᪫^T÷Þ{/?ù“?ɇ|ȇpÕUÿ?øƒ?È+¼Â+ðˆG<‚«®zQ=õ©Oåÿðy÷x®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñü¼ßkÿ8Wýïò¨—:Í'|õkñ_iµZñÕ_ýÕ|ò'2W]õ¯ñ³?û³Ü|óͼÌ˼ W]õ¢º÷Þ{ùÉŸüI>äC>„«®ú×øÁüA^á^G<â\uÕ‹ê©O}*ø‡È{¼Ç{pÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâùy¿×þq®úßåQ/ušOøê×â¿Òjµâ«¿ú«ùäOþd®ºê_ãgög¹ùæ›y™—y®ºêEuï½÷ò“?ù“|ȇ|W]õ¯ñƒ?øƒ¼Â+¼xÄ#¸êªÕSŸúTþðÿ÷x÷ફþ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\õ¿Ë£^ê4ŸðկťÕjÅWõWóÉŸüÉ\uÕ¿ÆÏþìÏróÍ7ó2/ó2\uÕ‹êÞ{ïå'ò'ùù®ºê_ãðy…WxñˆGpÕU/ª§>õ©üáþ!ïñïÁUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰ççý^ûǹê—G½Ôi>á«_‹ÿJ«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏÏû½ösÕÿ.z©Ó|ÂW¿ÿ•V«_ýÕ_Í'ò'sÕUÿ?û³?ËÍ7ßÌ˼ÌËpÕU/ª{ï½—ŸüÉŸäC>äC¸êªüÁä^áxÄ#ÁUW½¨žúÔ§ò‡ø‡¼Ç{¼W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žŸ÷{íçªÿ]õR§ù„¯~-þ+­V+¾ú«¿šOþäO檫þ5~ög–›o¾™—y™—᪫^T÷Þ{/?ù“?ɇ|ȇpÕUÿ?øƒ?È+¼Â+ðˆG<‚«®zQ=õ©Oåÿðy÷x®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iá«_‹ÿJ«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«þWyí×~m~çw~Û¼0’x­×z-~û·€ŸþéŸæmÞæmx¯÷z/¾û»¿›æ£?ú£ùš¯ù~ê§~Š·~ë·æ¿Š$žŸ÷{íçªÿ]õR§ù„¯~-þ+­V+¾ú«¿šOþäO檫þ5~ög–›o¾™—y™—᪫^T÷Þ{/?ù“?ɇ|ȇpÕUÿ?øƒ?È+¼Â+ðˆG<‚«®zQ=õ©Oåÿðy÷x®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ú_åµ_ûµùßùlóÂHàµ^ëµøíßþmî÷à?˜g<ã\¼x‘ãÇó‚œ8q‚ÝÝ]Ž;Æîî.ÿ•$ñü¼ßkÿ8Wýïò¨—:Í'|õkñ_iµZñÕ_ýÕ|ò'2W]õ¯ñ³?û³Ü|óͼÌ˼ W]õ¢º÷Þ{ùÉŸüI>äC>„«®ú×øÁüA^á^G<â\uÕ‹ê©O}*ø‡È{¼Ç{pÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕÿ*¯ýÚ¯ÍïüÎï`›F¯õZ¯Åoÿöos¿þèæk¾ækø®ïú.Þû½ß›çç§ú§y›·y>ê£>Нþê¯æ¿’$žŸ÷{íçªÿ]õR§ù„¯~-þ+­V+¾ú«¿šOþäO檫þ5~ög–›o¾™—y™—᪫^T÷Þ{/?ù“?ɇ|ȇpÕUÿ?øƒ?È+¼Â+ðˆG<‚«®zQ=õ©Oåÿðy÷x®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ú_åµ_ûµùßùlóÂHàµ^ëµøíßþmîwë­·ò‡<€·z«·â§ú§y~Þû½ß›ïùžïà¯þê¯xé—~iþ+Iâùy¿×þq®úßåQ/ušOøê×â¿Òjµâ«¿ú«ùäOþd®ºê_ãgög¹ùæ›y™—y®ºêEuï½÷ò“?ù“|ȇ|W]õ¯ñƒ?øƒ¼Â+¼xÄ#¸êªÕSŸúTþðÿ÷x÷ફþ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêªÿU^ûµ_›ßùßÀ6/Œ$^ëµ^‹ßþíßæ^ú¥_š¿ù›¿àéO:~ðƒy ÝÝ]Nœ8ÀK½ÔKñ×ý×üW“Äóó~¯ýã\õ¿Ë£^ê4ŸðկťÕjÅWõWóÉŸüÉ\uÕ¿ÆÏþìÏróÍ7ó2/ó2\uÕ‹êÞ{ïå'ò'ùù®ºê_ãðy…WxñˆGpÕU/ª§>õ©üáþ!ïñïÁUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUÿ«¼ök¿6¿ó;¿€m^I¼Ök½¿ýÛ¿Í}÷w7ïó>ïÀW}ÕWñÑýÑ<Ðw÷wó>ïó>|ÕW}ýÑÍ¿×îî.Ÿó9ŸÃw÷w³»»Ë¿Õû½ösÕÿ.z©Ó|ÂW¿ÿ•V«_ýÕ_Í'ò'sÕUÿ?û³?ËÍ7ßÌ˼ÌËpÕU/ª{ï½—ŸüÉŸäC>äC¸êªüÁä^áxÄ#ÁUW½¨žúÔ§ò‡ø‡¼Ç{¼W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUWý¯òÚ¯ýÚüÎïü¶ya$ðZ¯õZüöoÿ6´»»Ëƒü`.]ºÄK¿ôKóWõW<Ðë¼ÎëðÛ¿ýÛ\¼x‘ãÇóïõÑýÑ|Í×| ÿ^ï÷Ú?ÎUÿ»<ê¥Nó _ýZüWZ­V|õW5ŸüÉŸÌUWýküìÏþ,7ß|3/ó2/ÃUW½¨î½÷^~ò'’ù᪫þ5~ðWx…WàxW]õ¢zêSŸÊþáòïñ\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]õ¿Êk¿ökó;¿ó;Øæ…‘Àk½ÖkñÛ¿ýÛ<·÷~ï÷æ{¾ç{xúӟ΃ü`n½õVò‡ðVoõVüôOÿ4ÿNœ8Áîî.ÿ^ï÷Ú?ÎUÿ»<ê¥Nó _ýZüWZ­V|õW5ŸüÉŸÌUWýküìÏþ,7ß|3/ó2/ÃUW½¨î½÷^~ò'’ù᪫þ5~ðWx…WàxW]õ¢zêSŸÊþáòïñ\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]õ¿Êk¿ökó;¿ó;ØæÙÝÝåĉ¼Ök½¿ýÛ¿Ísûíßþm^çu^€ú¨â«¿ú«øê¯þj>æc>€Ÿú©Ÿâ­ßú­ùpüøq.]ºÄ¿×û½ösÕÿ.z©Ó|ÂW¿ÿ•V«_ýÕ_Í'ò'sÕUÿ?û³?ËÍ7ßÌ˼ÌËpÕU/ª{ï½—ŸüÉŸäC>äC¸êªüÁä^áxÄ#ÁUW½¨žúÔ§ò‡ø‡¼Ç{¼W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUWý¯òÚ¯ýÚüÎïü¶yA~û·›×y×àµ^ëµøíßþmžŸ?øÁ<ãÏàÁ~0OúÓxÈC­·Þʃô n½õVþ£|ôG4_ó5_ÿ×û½ösÕÿ.z©Ó|ÂW¿ÿ•V«_ýÕ_Í'ò'sÕUÿ?û³?ËÍ7ßÌ˼ÌËpÕU/ª{ï½—ŸüÉŸäC>äC¸êªüÁä^áxÄ#ÁUW½¨žúÔ§ò‡ø‡¼Ç{¼W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUWý¯òÚ¯ýÚüÎïü¶yA~û·›×y×àµ^ëµøíßþmžŸÏþìÏæs>çsø«¿ú+^æe^€ú¨â«¿ú«ùôÑýÑ|÷w7—.]âßêý^ûǹê—G½Ôi>á«_‹ÿJ«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«þWyí×~m~çw~€ßú­ßâµ_ûµy~>û³?›ÏùœÏàµ^ëµøíßþmžŸ[o½•‡<ä!|ÔG}_ó5_ÀÓŸþtüàóßEÏÏû½ösÕÿ.z©Ó|ÂW¿ÿ•V«_ýÕ_Í'ò'sÕUÿ?û³?ËÍ7ßÌ˼ÌËpÕU/ª{ï½—ŸüÉŸäC>äC¸êªüÁä^áxÄ#ÁUW½¨žúÔ§ò‡ø‡¼Ç{¼W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUWý¯òÑýÑ|Í×| ŸõYŸÅgögóÜvwwyÈCÂîî.¯õZ¯Åoÿöoó‚¼õ[¿5?ó3?Ãü`n½õV^ê¥^Š¿þë¿æ¿“$žŸ÷{íçªÿ]õR§ù„¯~-þ+­V+¾ú«¿šOþäO檫þ5~ög–›o¾™—y™—᪫^T÷Þ{/?ù“?ɇ|ȇpÕUÿ?øƒ?È+¼Â+ðˆG<‚«®zQ=õ©Oåÿðy÷x®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ú_å§ú§y›·yî÷Õ_ýÕ|ÔG}÷ûíßþm>æc>†¿þë¿æ~¯õZ¯Åoÿöoó‚|÷w7ïó>ïÃ}×w}ïýÞïÍ'IäC>„«®ú×øÁüA^á^G<â\uÕ‹ê©O}*ø‡È{¼Ç{pÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕÿ:»»»¼õ[¿5¿ó;¿ÃóóQõQ|õW5¯ýÚ¯ÍïüÎïðZ¯õZüöoÿ6/Ì{¿÷{ó=ßó=¼×{½ßýÝßÍ7Iõ©üáþ!ïñïÁUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰ççý^ûǹê—G½Ôi>á«_‹ÿJ«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏÏû½ösÕÿ.z©Ó|ÂW¿ÿ•V«_ýÕ_Í'ò'sÕUÿ?û³?ËÍ7ßÌ˼ÌËpÕU/ª{ï½—ŸüÉŸäC>äC¸êªüÁä^áxÄ#ÁUW½¨žúÔ§ò‡ø‡¼Ç{¼W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žŸ÷{íçªÿ]õR§ù„¯~-þ+­V+¾ú«¿šOþäO檫þ5~ög–›o¾™—y™—᪫^T÷Þ{/?ù“?ɇ|ȇpÕUÿ?øƒ?È+¼Â+ðˆG<‚«®zQ=õ©Oåÿðy÷x®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñü¼ßkÿ8Wýïò¨—:Í'|õkñ_iµZñÕ_ýÕ|ò'2W]õ¯ñ³?û³Ü|óͼÌ˼ W]õ¢º÷Þ{ùÉŸüI>äC>„«®ú×øÁüA^á^G<â\uÕ‹ê©O}*ø‡È{¼Ç{pÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâùy¿×þq®úßåQ/ušOøê×â¿Òjµâ«¿ú«ùäOþd®ºê_ãgög¹ùæ›y™—y®ºêEuï½÷ò“?ù“|ȇ|W]õ¯ñƒ?øƒ¼Â+¼xÄ#¸êªÕSŸúTþðÿ÷x÷ફþ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\õ¿Ë£^ê4ŸðկťÕjÅWõWóÉŸüÉ\uÕ¿ÆÏþìÏróÍ7ó2/ó2\uÕ‹êÞ{ïå'ò'ùù®ºê_ãðy…WxñˆGpÕU/ª§>õ©üáþ!ïñïÁUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰ççý^ûǹê—G½Ôi>á«_‹ÿJ«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏÏû½ösÕÿ.z©Ó|ÂW¿ÿ•V«_ýÕ_Í'ò'sÕUÿ?û³?ËÍ7ßÌ˼ÌËpÕU/ª{ï½—ŸüÉŸäC>äC¸êªüÁä^áxÄ#ÁUW½¨žúÔ§ò‡ø‡¼Ç{¼W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žŸ÷{íçªÿ]õR§ù„¯~-þ+­V+¾ú«¿šOþäO檫þ5~ög–›o¾™—y™—᪫^T÷Þ{/?ù“?ɇ|ȇpÕUÿ?øƒ?È+¼Â+ðˆG<‚«®zQ=õ©Oåÿðy÷x®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(IÃUÏß½÷ÞËOýÔOñÁüÁ\uÕ¿ÆüÀðŠ¯øŠ<âફ^TO}êSù£?ú#ÞýÝß«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏÏû½ösÕUÿ’¨ë_ùIÜùû᪫þ5N>úNÖ»Þs‚«ží¾ê5yÔKŸáªçïÞ{ïå§~ê§øàþ`®ºê_ã~àxÅW|EñˆGpÕU/ª§>õ©üÑýïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰ççý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'}'ëÝ ï9ÁUÏö _õš<ê¥ÏpÕówï½÷òS?õS|ð0W]õ¯ñ?ð¼â+¾"xÄ#¸êªÕSŸúTþèþˆw÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\uÕ¿$jãúW~wþþc¸êª“¾“õî‡÷œàªgû„¯zMõÒg¸êù»÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]õÿÔoÿöoó;¿ó;¼¨Ž?Î[½Õ[ñà?˜äÖ[oå{¾ç{xQ½ôK¿4¯õZ¯ÅñãÇù—Üzë­|Ï÷|ôZ¯õZ¼ök¿6ÿ¿ýÛ¿ÍïüÎïp¿=èA¼÷{¿7ÿIâùy¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGßÉzwƒÃ{NpÕ³}ÂW½&zé3\õüÝ{ï½üÔOýüÁÌUWýküÀü¯øŠ¯È#ñ®ºêEõÔ§>•?ú£?âÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®úê³?û³ùœÏùþµ^ûµ_›Ïú¬Ïâµ_ûµyn¿ýÛ¿Íë¼Îëð¯õÖoýÖ|ÕW}~ðƒyA~û·›×y×á^ú¥_š¿ú«¿â_ãe^æeøë¿þkî÷Z¯õZüöoÿ6ÿIâùy¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGßÉzwƒÃ{NpÕ³}ÂW½&zé3\õüÝ{ï½üÔOýüÁÌUWýküÀü¯øŠ¯È#ñ®ºêEõÔ§>•?ú£?âÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®úê³?û³ùœÏùþ­~ê§~Š·~ë·æ~û·›×y×áßâøñã|×w}oýÖoÍóóÛ¿ýÛ¼Îë¼ÏíéO:~ðƒyQÜzë­<ä!á^ëµ^‹ßþíßæ"Iú£?šÅWõWó1ó1<Ðk½ÖkñÛ¿ýÛüO$‰ççý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'}'ëÝ ï9ÁUÏö _õš<ê¥ÏpÕówï½÷òS?õS|ð0W]õ¯ñ?ð¼â+¾"xÄ#¸êªÕSŸúTþèþˆw÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêÿ©ÏþìÏæs>çsø¬Ïú,>û³?›É_ÿõ_óÚ¯ýÚ\ºt €¯úª¯â£?ú£¹ßoÿöoó:¯ó:¼Ök½¿ýÛ¿Í¿äÖ[oå­ßú­ù›¿ùüàóô§?çöÛ¿ýÛ¼Îë¼/õR/ÅßüÍßðÒ/ýÒüÕ_ý/Š—y™—á¯ÿú¯yЃÄ3žñ ^ëµ^‹ßþíßæ"Iû³?›ÏùœÏàµ^ëµøíßþmî÷Û¿ýÛ¼Î뼯õZ¯Åoÿöoó¢øë¿þk^ûµ_›K—.ðYŸõY|ög6ôÛ¿ýÛ¼Î뼯õZ¯ÅñãÇù™Ÿùžþô§óà?˜æÖ[oå!yõQÅ×|Í×ðZ¯õZüöoÿ6ÿIâùy¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGßÉzwƒÃ{NpÕ³}ÂW½&zé3\õüÝ{ï½üÔOýüÁÌUWýküÀü¯øŠ¯È#ñ®ºêEõÔ§>•?ú£?âÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®úê³?û³ùœÏù>ë³>‹ÏþìÏæEñÛ¿ýÛ¼Îë¼ÇçâÅ‹Üï·û·y×y^ëµ^‹ßþíßæEõÝßýݼÏû¼ÇçâÅ‹<Ðoÿöoó:¯ó:¼Ök½ïýÞïÍû¼ÏûðU_õU|ôG4/ÌWõWó1ó1;vŒŸþéŸæu^çux­×z-~û·›ÿ‰$ñü¼ßkÿ8W]õ/‰Ú¸þ•ŸÄ¿ÿ®ºê_ãä£ïd½»Áá='¸êÙ>á«^“G½ô®zþî½÷^~ê§~Šþà檫þ5~à~€W|ÅWäxW]õ¢zêSŸÊýÑñîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUWý?õÙŸýÙ|Îç|ŸõYŸÅgögó¢øíßþm^çu^‡ûÙæ~¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿÇçÒ¥KüÕ_ý/ýÒ/Íý~û·›×y×àµ^ëµøíßþmŽ?Î¥K—xé—~iþê¯þŠæ!y·Þz+ïõ^ïÅ{¿÷{ó:¯ó:¼Ök½¿ýÛ¿ÍÿD’x~Þﵜ«®ú—Dm\ÿÊOâÎß W]õ¯qòÑw²ÞÝàðž\õlŸðU¯É£^ú W=÷Þ{/?õS?ÅðsÕUÿ?ð?À+¾â+òˆG<‚«®zQ=õ©Oåþèx÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«þŸúìÏþl>çs>€Ïú¬Ïâ³?û³yQ|÷w7ïó>ïÀk½ÖkñÛ¿ýÛÜï·û·y×y^ëµ^‹ßþíßæ_ã­ßú­ù™Ÿù¾ê«¾Šþèæ~¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ïýÞïÍ÷|Ï÷ðô§??øÁçs>€Ïú¬Ïâ³?û³ù—ìîîò2/ó2Üzë­|ÔG}_ýÕ_Íý~û·›×y×àµ^ëµøíßþmþ5>û³?›ÏùœÏà³>ë³øìÏþlî÷Û¿ýÛ¼Î뼯õZ¯ÅoÿöoóÓ?ýÓ¼ÍÛ¼ _õU_ÅGôGóü|ôG4_ó5_Ãô n½õV~û·›×y×àµ^ëµøíßþmþ'’Äóó~¯ýã\uÕ¿$jãúW~wþþc¸êª“¾“õî‡÷œàªgû„¯zMõÒg¸êù»÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]õÿÔgögó9Ÿó9|Ög}ŸýÙŸÍ ó×ý×|ÌÇ| ¿ýÛ¿Íýžþô§óà?˜ûýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛük|ög6Ÿó9ŸÀk½ÖkñÛ¿ýÛÜï·û·y×y^ëµ^‹ßþíßàøñã\ºt‰—~é—æ¯þê¯x~ò‡pë­·òQõQ|õW5¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿIâùy¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGßÉzwƒÃ{NpÕ³}ÂW½&zé3\õüÝ{ï½üÔOýüÁÌUWýküÀü¯øŠ¯È#ñ®ºêEõÔ§>•?ú£?âÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®úê³?û³ùœÏùüàóà?˜ä¯ÿú¯ÙÝÝå¾ê«¾Šþèæ~û·›×y×àµ^ëµøíßþmþ5>û³?›ÏùœÏàµ^ëµøíßþmî÷Û¿ýÛ¼Î뼯õZ¯ÅoÿöoðÞïýÞ|Ï÷|OúÓyðƒÌýõ_ÿ5/ó2/À_ýÕ_ñÒ/ýÒüöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛüO$‰ççý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'}'ëÝ ï9ÁUÏö _õš<ê¥ÏpÕówï½÷òS?õS|ð0W]õ¯ñ?ð¼â+¾"xÄ#¸êªÕSŸúTþèþˆw÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêÿ©ÏþìÏæs>çsø·øª¯ú*>ú£?šçöÛ¿ýÛ¼Î뼯õZ¯Åoÿöoó¯ñÙŸýÙ|Îç|¯õZ¯Åoÿöos¿ßþíßæu^çux­×z-~û·€ŸþéŸæmÞæmøª¯ú*>ú£?šúèþh¾æk¾†=èAÜzë­üöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛüWØÝÝås>çsøîïþnvwwù·z¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGßÉzwƒÃ{NpÕ³}ÂW½&zé3\õüÝ{ï½üÔOýüÁÌUWýküÀü¯øŠ¯È#ñ®ºêEõÔ§>•?ú£?âÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®úê³?û³ùœÏù^/õR/Ńü`^ûµ_›÷~ï÷æøñãøƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]õÿÔgögó9Ÿó9|Ög}ŸýÙŸÍ¿×oÿöoó:¯ó:¼Ök½¿ýÛ¿Í¿Æ{¿÷{ó=ßó=|Ög}ŸýÙŸÍý~û·›×y×àµ^ëµøíßþmî÷ÞïýÞ|Ï÷|OúÓyðƒ Àoÿöoó:¯ó:<ýéOçÁ~0¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿNœ8Áîî.ÿ^ï÷Ú?ÎUWýK¢6®å'qçï?†«®ú×8ùè;YïnpxÏ ®z¶Oøª×äQ/}†«ž¿{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUÿO}ög6Ÿó9ŸÀg}ÖgñÙŸýÙü{ýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛük<ä!áÖ[oà§~ê§xë·~kî÷Û¿ýÛ¼Î뼯õZ¯Åoÿöos¿ŸþéŸæmÞæmøª¯ú*>ú£?€÷~ï÷æ{¾ç{x©—z)þú¯ÿšûýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛüW8~ü8—.]âßëý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'}'ëÝ ï9ÁUÏö _õš<ê¥ÏpÕówï½÷òS?õS|ð0W]õ¯ñ?ð¼â+¾"xÄ#¸êªÕSŸúTþèþˆw÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêÿ©ÏþìÏæs>çsø¬Ïú,>û³?›¯ßþíßæu^çux­×z-~û·›Õ­·ÞÊCòî÷ô§??øÁÜï·û·y×y^ëµ^‹ßþíßæŽ?Î¥K—xé—~iþê¯þ €'N°»»ËW}ÕWñÑýÑÜï·û·y×y^ëµ^‹ßþíßæ¿ÂGôGó5_ó5ü{½ßkÿ8W]õ/‰Ú¸þ•ŸÄ¿ÿ®ºê_ãä£ïd½»Áá='¸êÙ>á«^“G½ô®zþî½÷^~ê§~Šþà檫þ5~à~€W|ÅWäxW]õ¢zêSŸÊýÑñîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUWý?õÙŸýÙ|Îç|ŸõYŸÅgögóïõÛ¿ýÛ¼Î뼯õZ¯Åoÿöoó¢zï÷~o¾ç{¾€·z«·â§ú§y ßþíßæu^çux­×z-~û·›zï÷~o¾ç{¾€§?ýéüõ_ÿ5oó6oÀÓŸþtüàs¿ßþíßæu^çux­×z-~û·›ÿ*ýÑÍw÷wséÒ%þ­Þﵜ«®ú—Dm\ÿÊOâÎß W]õ¯qòÑw²ÞÝàðž\õlŸðU¯É£^ú W=÷Þ{/?õS?ÅðsÕUÿ?ð?À+¾â+òˆG<‚«®zQ=õ©Oåþèx÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«þŸúìÏþl>çs>€Ïú¬Ïâ³?û³ù÷úíßþm^çu^€×z­×â·û·yQüôOÿ4oó6oÃý~ë·~‹×~í׿~û·›×y×àµ^ëµøíßþmè§ú§y›·y¾ê«¾Š¿þë¿æ{¾ç{x©—z)þú¯ÿšúíßþm^çu^€×z­×â·û·ùŸHÏÏû½ösÕUÿ’¨ë_ùIÜùû᪫þ5N>úNÖ»Þs‚«ží¾ê5yÔKŸáªçïÞ{ïå§~ê§øàþ`®ºê_ã~àxÅW|EñˆGpÕU/ª§>õ©üÑýïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕÿSŸýÙŸÍç|ÎçðYŸõY|ög6ÿ^¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿ’¯ùš¯á³?û³ÙÝÝà­Þê­øéŸþižÛoÿöoó:¯ó:¼Ök½¿ýÛ¿Ís;~ü8—.]âµ^ëµø›¿ùvwwùª¯ú*>ú£?šúíßþm^çu^€×z­×â·û·ùŸHÏÏû½ösÕUÿ’¨ë_ùIÜùû᪫þ5N>úNÖ»Þs‚«ží¾ê5yÔKŸáªçïÞ{ïå§~ê§øàþ`®ºê_ã~àxÅW|EñˆGpÕU/ª§>õ©üÑýïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕÿSŸýÙŸÍç|ÎçðYŸõY|ög6ÿ^¿ýÛ¿Íë¼Îëðà?˜÷~ï÷æùë¿þkþú¯ÿš[o½•û½ÔK½¿ýÛ¿ÍñãÇyn¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6Ïí£?ú£ùš¯ùèâÅ‹?~œúíßþm^çu^€×z­×â·û·ùŸHÏÏû½ösÕUÿ’¨ë_ùIÜùû᪫þ5N>úNÖ»Þs‚«ží¾ê5yÔKŸáªçïÞ{ïå§~ê§øàþ`®ºê_ã~àxÅW|EñˆGpÕU/ª§>õ©üÑýïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕÿSŸýÙŸÍç|ÎçðYŸõY|ög6ÿ^¿ýÛ¿Íë¼ÎëðoñZ¯õZüôOÿ4Ççùùíßþm^çu^€×z­×â·û·yný×Í˼ÌËp¿·z«·â§ú§yn¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿIâùy¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGßÉzwƒÃ{NpÕ³}ÂW½&zé3\õüÝ{ï½üÔOýüÁÌUWýküÀü¯øŠ¯È#ñ®ºêEõÔ§>•?ú£?âÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®úê³?û³ùœÏù>ë³>‹ÏþìÏæßë·û·y×y^TÇŽã­ßú­yï÷~o^ûµ_›æ·û·y×y^ëµ^‹ßþíßæùyðƒÌ3žñ ¾ë»¾‹÷~ï÷æ¹ýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛüO$‰ççý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'}'ëÝ ï9ÁUÏö _õš<ê¥ÏpÕówï½÷òS?õS|ð0W]õ¯ñ?ð¼â+¾"xÄ#¸êªÕSŸúTþèþˆw÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêÿ©[o½•[o½€?øÁ<øÁæßkww—¿þë¿æEñà?˜?øÁ¼¨vwwùë¿þkŽ?ÎK¿ôKóüüõ_ÿ5»»»¼ôK¿4Çç¹íîîò×ý×?~œ—~é—æ"IÃUÏß½÷ÞËOýÔOñÁüÁ\uÕ¿ÆüÀðŠ¯øŠ<âફ^TO}êSù£?ú#ÞýÝß«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏÏû½ösÕUÿ’¨ë_ùIÜùû᪫þ5N>úNÖ»Þs‚«ží¾ê5yÔKŸáªçïÞ{ïå§~ê§øàþ`®ºê_ã~àxÅW|EñˆGpÕU/ª§>õ©üÑýïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰ççý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'}'ëÝ ï9ÁUÏö _õš<ê¥ÏpÕówï½÷òS?õS|ð0W]õ¯ñ?ð¼â+¾"xÄ#¸êªÕSŸúTþèþˆw÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\uÕ¿$jãúW~wþþc¸êª“¾“õî‡÷œàªgû„¯zMõÒg¸êù»÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâùy¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGßÉzwƒÃ{NpÕ³}ÂW½&zé3\õüÝ{ï½üÔOýüÁÌUWýküÀü¯øŠ¯È#ñ®ºêEõÔ§>•?ú£?âÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñü¼ßkÿ8W]õ/‰Ú¸þ•ŸÄ¿ÿ®ºê_ãä£ïd½»Áá='¸êÙ>á«^“G½ô®zþî½÷^~ê§~Šþà檫þ5~à~€W|ÅWäxW]õ¢zêSŸÊýÑñîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]uÕÿP’x~Þﵜ«®ú—Dm\ÿÊOâÎß W]õ¯qòÑw²ÞÝàðž\õlŸðU¯É£^ú W=÷Þ{/?õS?ÅðsÕUÿ?ð?À+¾â+òˆG<‚«®zQ=õ©Oåþèx÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(IÃUÏß=÷ÜÃOÿôOóÁüÁ\uÕ¿ÆüÀðJ¯ôJ<üá窫^TOyÊSøã?þcÞýÝß«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏÏû½ösÕUÿ’¨ë_ùIÜùû᪫þ5N>úÖ»›Þs‚«ží¾ê5yÔKŸáªçïž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰ççý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'}ëÝMï9ÁUÏö _õš<ê¥ÏpÕówÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóc›«®ú—¬V+¾ú«¿šOþäO檫þ5~æg~†=èA¼ôK¿4W]õ¢ºçž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ãô ^ú¥_š«®zQÝsÏ=üôOÿ4üÁÌUWýküÀü¯ôJ¯ÄÃþp®ºêEõ”§<…?þã?æÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸù™ŸáAz/ýÒ/ÍUW½¨î¹ç~ú§šþà檫þ5~à~€Wz¥Wâá8W]õ¢zÊSžÂÿñóîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]uÕÿP’x~lsÕUÿ’ÕjÅWõWóÉŸüÉ\uÕ¿ÆÏüÌÏð =ˆ—~é—æª«^T÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iù“?™«®ú×ø™Ÿùô ñÒ/ýÒ\uÕ‹êž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰çÇ6W]õ/Y­V|õW5ŸüÉŸÌUWýküÌÏü zЃxé—~i®ºêEuÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóc›«®ú—¬V+¾ú«¿šOþäO檫þ5~æg~†=èA¼ôK¿4W]õ¢ºçž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ãô ^ú¥_š«®zQÝsÏ=üôOÿ4üÁÌUWýküÀü¯ôJ¯ÄÃþp®ºêEõ”§<…?þã?æÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸù™ŸáAz/ýÒ/ÍUW½¨î¹ç~ú§šþà檫þ5~à~€Wz¥Wâá8W]õ¢zÊSžÂÿñóîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]uÕÿP’x~lsÕUÿ’ÕjÅWõWóÉŸüÉ\uÕ¿ÆÏüÌÏð =ˆ—~é—æª«^T÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iù“?™«®ú×ø™Ÿùô ñÒ/ýÒ\uÕ‹êž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰çÇ6W]õ/Y­V|õW5ŸüÉŸÌUWýküÌÏü zЃxé—~i®ºêEuÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóc›«®ú—¬V+¾ú«¿šOþäO檫þ5~æg~†=èA¼ôK¿4W]õ¢ºçž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ãô ^ú¥_š«®zQÝsÏ=üôOÿ4üÁÌUWýküÀü¯ôJ¯ÄÃþp®ºêEõ”§<…?þã?æÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸù™ŸáAz/ýÒ/ÍUW½¨î¹ç~ú§šþà檫þ5~à~€Wz¥Wâá8W]õ¢zÊSžÂÿñóîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]uÕÿP’x~lsÕUÿ’ÕjÅWõWóÉŸüÉ\uÕ¿ÆÏüÌÏð =ˆ—~é—æª«^T÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iù“?™«®ú×ø™Ÿùô ñÒ/ýÒ\uÕ‹êž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰çÇ6W]õ/Y­V|õW5ŸüÉŸÌUWýküÌÏü zЃxé—~i®ºêEuÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóc›«®ú—¬V+¾ú«¿šOþäO檫þ5~æg~†=èA¼ôK¿4W]õ¢ºçž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ãô ^ú¥_š«®zQÝsÏ=üôOÿ4üÁÌUWýküÀü¯ôJ¯ÄÃþp®ºêEõ”§<…?þã?æÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸù™ŸáAz/ýÒ/ÍUW½¨î¹ç~ú§šþà檫þ5~à~€Wz¥Wâá8W]õ¢zÊSžÂÿñóîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]uÕÿP’x~lsÕUÿ’ÕjÅWõWóÉŸüÉ\uÕ¿ÆÏüÌÏð =ˆ—~é—æª«^T÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iù“?™«®ú×ø™Ÿùô ñÒ/ýÒ\uÕ‹êž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰çÇ6W]õ/Y­V|õW5ŸüÉŸÌUWýküÌÏü zЃxé—~i®ºêEuÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóc›«®ú—¬V+¾ú«¿šOþäO檫þ5~æg~†=èA¼ôK¿4W]õ¢ºçž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ãô ^ú¥_š«®zQÝsÏ=üôOÿ4üÁÌUWýküÀü¯ôJ¯ÄÃþp®ºêEõ”§<…?þã?æÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸù™ŸáAz/ýÒ/ÍUW½¨î¹ç~ú§šþà檫þ5~à~€Wz¥Wâá8W]õ¢zÊSžÂÿñóîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]uÕÿP’x~lsÕUÿ’ÕjÅWõWóÉŸüÉ\uÕ¿ÆÏüÌÏð =ˆ—~é—æª«^T÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iù“?™«®ú×ø™Ÿùô ñÒ/ýÒ\uÕ‹êž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰çÇ6W]õ/Y¯×|Í×| Ÿø‰ŸÈUWýküìÏþ,·Ür /ýÒ/ÍUW½¨î½÷^~æg~†üÀ䪫þ5~ðW|ÅWäá8W]õ¢zêSŸÊÿñónïön\uÕ¿Æ·~ë·ò–où–\wÝu\uÕ‹êoþæo¸õÖ[y«·z+®ºêª«þA¶ÍUW]uÕÿP’x~lsÕUÿ’ÕjÅWõWóÉŸüÉ\uÕ¿ÆÏüÌÏð =ˆ—~é—æª«^T÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iù“?™«®ú×ø™Ÿùô ñÒ/ýÒ\uÕ‹êž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰çÇ6W]õ/Y­V|õW5ŸüÉŸÌUWýküÌÏü zЃxé—~i®ºêEuÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóc›«®ú—¬V+¾ú«¿šOþäO檫þ5~æg~†=èA¼ôK¿4W]õ¢ºçž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ãô ^ú¥_š«®zQÝsÏ=üôOÿ4üÁÌUWýküÀü¯ôJ¯ÄÃþp®ºêEõ”§<…?þã?æÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸù™ŸáAz/ýÒ/ÍUW½¨î¹ç~ú§šþà檫þ5~à~€Wz¥Wâá8W]õ¢zÊSžÂÿñóîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]uÕÿP’x~lsÕUÿ’ÕjÅWõWóÉŸüÉ\uÕ¿ÆÏüÌÏð =ˆ—~é—æª«^T÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iù“?™«®ú×ø™Ÿùô ñÒ/ýÒ\uÕ‹êž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰çÇ6W]õ/Y­V|õW5ŸüÉŸÌUWýküÌÏü zЃxé—~i®ºêEuÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóc›«®ú—¬V+¾ú«¿šOþäO檫þ5~æg~†=èA¼ôK¿4W]õ¢ºçž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ãô ^ú¥_š«®zQÝsÏ=üôOÿ4üÁÌUWýküÀü¯ôJ¯ÄÃþp®ºêEõ”§<…?þã?æÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸù™ŸáAz/ýÒ/ÍUW½¨î¹ç~ú§šþà檫þ5~à~€Wz¥Wâá8W]õ¢zÊSžÂÿñóîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]uÕÿP’x~lsÕUÿ’ÕjÅWõWóÉŸüÉ\uÕ¿ÆÏüÌÏð =ˆ—~é—æª«^T÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iù“?™«®ú×ø™Ÿùô ñÒ/ýÒ\uÕ‹êž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰çÇ6W]õ/Y­V|õW5ŸüÉŸÌUWýküÌÏü zЃxé—~i®ºêEuÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóc›«®ú—¬V+¾ú«¿šOþäO檫þ5~æg~†=èA¼ôK¿4W]õ¢ºçž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ãô ^ú¥_š«®zQÝsÏ=üôOÿ4üÁÌUWýküÀü¯ôJ¯ÄÃþp®ºêEõ”§<…?þã?æÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸù™ŸáAz/ýÒ/ÍUW½¨î¹ç~ú§šþà檫þ5~à~€Wz¥Wâá8W]õ¢zÊSžÂÿñóîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]uÕÿP’x~lsÕUÿ’ÕjÅWõWóÉŸüÉ\uÕ¿ÆÏüÌÏð =ˆ—~é—æª«^T÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iù“?™«®ú×ø™Ÿùô ñÒ/ýÒ\uÕ‹êž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰çÇ6W]õ/Y­V|õW5ŸüÉŸÌUWýküÌÏü zЃxé—~i®ºêEuÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóc›«®ú—¬V+¾ú«¿šOþäO檫þ5~æg~†=èA¼ôK¿4W]õ¢ºçž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ãô ^ú¥_š«®zQÝsÏ=üôOÿ4üÁÌUWýküÀü¯ôJ¯ÄÃþp®ºêEõ”§<…?þã?æÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸù™ŸáAz/ýÒ/ÍUW½¨î¹ç~ú§šþà檫þ5~à~€Wz¥Wâá8W]õ¢zÊSžÂÿñóîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]uÕÿP’x~lsÕUÿ’ÕjÅWõWóÉŸüÉ\uÕ¿ÆÏüÌÏð =ˆ—~é—æª«^T÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iù“?™«®ú×ø™Ÿùô ñÒ/ýÒ\uÕ‹êž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰çÇ6W]õ/Y­V|õW5ŸüÉŸÌUWýküÌÏü zЃxé—~i®ºêEuÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóc›«®ú—¬V+¾ú«¿šOþäO檫þ5~æg~†=èA¼ôK¿4W]õ¢ºçž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ãô ^ú¥_š«®zQÝsÏ=üôOÿ4üÁÌUWýküÀü¯ôJ¯ÄÃþp®ºêEõ”§<…?þã?æÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸù™ŸáAz/ýÒ/ÍUW½¨î¹ç~ú§šþà檫þ5~à~€Wz¥Wâá8W]õ¢zÊSžÂÿñóîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]uÕÿP’x~Þﵜ«®ú—Dm\ÿÊOâÎß W]õ¯qòÑw°ÞÝäðžü{|ÂW½&zé3\õÿÃ=÷ÜÃOÿôOóÁüÁ\uÕ¿ÆüÀðJ¯ôJ<üá窫^TOyÊSøã?þcÞýÝß«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏÏû½ösÕUÿ’¨ë_ùIÜùû᪫þ5N>úÖ»›Þs‚Oøª×äQ/}†«þ¸çž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâùy¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGßÁzw“Ã{Nðïñ _õš<ê¥ÏpÕÿ÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iá«^“G½ô®úÿáž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰ççý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'}ëÝMï9Á¿Ç'|Õkò¨—>ÃUÿ?ÜsÏ=üôOÿ4üÁÌUWýküÀü¯ôJ¯ÄÃþp®ºêEõ”§<…?þã?æÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ú_às>çsø×xé—~i^ëµ^‹ãÇsÕÿ^’x~Þﵜ«®ú—Dm\ÿÊOâÎß W]õ¯qòÑw°ÞÝäðžü{|ÂW½&zé3\õÿÃ=÷ÜÃOÿôOóÁüÁ\uÕ¿ÆüÀðJ¯ôJ<üá窫^TOyÊSøã?þcÞýÝß«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êªÿ$ñoñÞïýÞ|ÕW}Ç烿þë¿æ}Þç}ø«¿ú+®Iá«^“G½ô®úÿáž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕÿ’ø·zé—~i~ë·~‹ãÇó?Ùgögó9Ÿó9Øæ*Äóó~¯ýã\uÕ¿$jãúW~wþþc¸êª“¾ƒõî&‡÷œàßã¾ê5yÔKŸáªÿî¹ç~ú§šþà檫þ5~à~€Wz¥Wâá8W]õ¢zÊSžÂÿñóîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUWý/ ‰ûýÖoý¯ýÚ¯Íóó×ý×ìîîòÝßýÝ|Ï÷|÷{¯÷z/¾û»¿›ÿÉ^ûµ_›ßùßÀ6W$žŸ÷{í窫þ%Q׿ò“¸ó÷ÃUWýkœ|ô¬w79¼çÿŸðU¯É£^ú WýÿpÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêIÜï·~ë·xí×~mþ%_ýÕ_ÍÇ|ÌÇp¿§?ýé<øÁæª×~í׿w~çw°ÍU ‰ççý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'}ëÝMï9Á¿Ç'|Õkò¨—>ÃUÿ?ÜsÏ=üôOÿ4üÁÌUWýküÀü¯ôJ¯ÄÃþp®ºêEõ”§<…?þã?æÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ú_@÷û­ßú-^ûµ_›Ńü`žñŒgðU_õU|ôG4ÿS½ök¿6¿ó;¿€m®Iá«^“G½ô®úÿáž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕÿ’¸ßoýÖoñÚ¯ýÚ¼(Þû½ß›ïùžïà³>ë³øìÏþlžŸÝÝ]~æg~†[o½•¿þë¿æÁ~0Ççµ_ûµy­×z-^T»»»üÌÏü ·Þz+ý×̓ü`üàóZ¯õZ¼ôK¿4ÏÏ_ÿõ_séÒ%>ú£?š¿þë¿à·û·8vì/ýÒ/ À­·ÞÊ3žñ ^ëµ^ €ïùžïá§ú§yé—~i^ëµ^‹—~é—æoþæo8vì/ýÒ/Í¿äw~çw8vì/ýÒ/Íÿ$’x~Þﵜ«®ú—Dm\ÿÊOâÎß W]õ¯qòÑw°ÞÝäðžü{|ÂW½&zé3\õÿÃ=÷ÜÃOÿôOóÁüÁ\uÕ¿ÆüÀðJ¯ôJ<üá窫^TOyÊSøã?þcÞýÝß«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êªÿ$q¿ßú­ßâµ_ûµyQ|ög6Ÿó9ŸÀg}ÖgñÙŸýÙ<·ÏùœÏá«¿ú«ÙÝÝåùyí×~m¾ê«¾Š—~é—æ…ùœÏù¾ú«¿šÝÝ]žŸ·~ë·æ»¾ë»8~ü8ôÚ¯ýÚüÎïüÏÏk½ÖkñÛ¿ýÛ|ög6Ÿó9Ÿ€m^æe^†¿þë¿æ¾ê«¾Šù˜àøñã\¼x‘æ§ú§y›·y>ê£>Нþê¯æIá«^“G½ô®úÿáž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕÿ’¸ßoýÖoñÚ¯ýÚ¼(^ûµ_›ßùßà«¾ê«øèþhè}Þç}øîïþnî÷ =ˆ?øÁüõ_ÿ5—.]àøñã|ÕW}ïýÞïÍóó6oó6üôOÿ4÷{Ѓăü`þú¯ÿšK—.ðÒ/ýÒüÔOý~ðƒ¹ßk¿ökó;¿ó;çs>€÷z¯÷⻿û»¹ß­·ÞÊCòŽ;Æ_ÿõ_óà?˜çç­ßú­ù™Ÿù¾ë»¾‹÷~ï÷æ^ûµ_›ßùßÀ6Ïí³?û³ùœÏùî÷[¿õ[¼ök¿6ÏÏñãǹté/^äøñã<·ïþîïæ}Þç}ø®ïú.Þû½ß›ÿi$ñü¼ßkÿ8W]õ/‰Ú¸þ•ŸÄ¿ÿ®ºê_ãä£ï`½»Éá='ø÷ø„¯zMõÒg¸êÿ‡{ŸþéŸæƒ?øƒ¹êªøà•^é•xøÃÎUW½¨žò”§ðÇüǼû»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUÿ Hâ~/ýÒ/ÍñãÇyA~û·›:vìý×̓ü`î÷Õ_ýÕ|ÌÇ| õQÅWõWóÂ?~œK—.pñâEŽ?ÀGôGó5_ó5|Ög}ŸýÙŸÍ rë­·ò‡<€·z«·â§ú§y ×~í׿w~çw°ÍsûìÏþl>çs>€—z©—â¯ÿú¯yA>ú£?š¯ùš¯໾ë»xï÷~ožÛë¼ÎëðÛ¿ýÛ\¼x‘ãÇó?$žŸ÷{í窫þ%Q׿ò“¸ó÷ÃUWýkœ|ô¬w79¼çÿŸðU¯É£^ú WýÿpÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêIü[¼ÔK½ßýÝßÍK¿ôKó@¯ýÚ¯ÍïüÎïð[¿õ[¼ök¿6/Ì{¿÷{ó=ßó=üÔOýoýÖo Àk¿ökó;¿ó;üÕ_ý/ýÒ/Í süøq.]º€mèµ_ûµùßùlóÜ>û³?›ÏùœÏà£>ê£øê¯þj^¿þë¿æe^æex«·z+~ú§šºõÖ[yÈCÀ{½×{ñÝßýÝüWØÝÝås>çsøîïþnvwwù·z¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGßÁzw“Ã{Nðïñ _õš<ê¥ÏpÕÿ÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«þÄ‹êµ^ëµxé—~i^ûµ_›·~ë·æùy™—yþú¯ÿÛüK>û³?›ÏùœÏà³>ë³øìÏþl$q¿ÏþìÏæ_òÝßýÝÜzë­<ýéOçÁ~0÷{í×~m~çw~Û<·ÏþìÏæs>çsø¬Ïú,>û³?›æ¥_ú¥ù›¿ùžþô§óà?˜û}õW5ó1ÀOýÔOñÖoýÖüWøèþh¾æk¾†¯÷{í窫þ%Q׿ò“¸ó÷ÃUWýkœ|ô¬w79¼çÿŸðU¯É£^ú WýÿpÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêIÜï·~ë·xí×~mþ=$q?ÛüK~û·›×y×à³>ë³øìÏþl$ñoõ[¿õ[¼ök¿6÷{í×~m~çw~Û<·ÏþìÏæs>çsø©Ÿú)Þú­ßšæ«¿ú«ù˜ù¾ê«¾Šþèæ~yÈC¸õÖ[yЃÄ­·ÞÊ•'N°»»Ë¿×û½ösÕUÿ’¨ë_ùIÜùû᪫þ5N>úÖ»›Þs‚Oøª×äQ/}†«þ¸çž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]õ¿€$î÷[¿õ[¼ök¿6ÿ’¸Ÿmþ%¿ýÛ¿Íë¼ÎëðYŸõY|ög6’8vì/ýÒ/Í¿ÆWõWóÒ/ýÒÜïµ_ûµùßùlóÜ>û³?›ÏùœÏà·~ë·xí×~m^˜ÝÝ]Nœ8ÀK¿ôKóWõWüõ_ÿ5/ó2/ÀG}ÔGñÕ_ýÕüW9~ü8—.]âßëý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'}ëÝMï9Á¿Ç'|Õkò¨—>ÃUÿ?ÜsÏ=üôOÿ4üÁÌUWýküÀü¯ôJ¯ÄÃþp®ºêEõ”§<…?þã?æÝßýݹêªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ú_@÷û­ßú-^ûµ_›?øÁ<ãÏÀ6ÿ’ÏþìÏæs>çsø¬Ïú,>û³?IÜÏ6ÿ¯ýÚ¯ÍïüÎï`›çöÙŸýÙ|Îç|¿õ[¿Åk¿ökó/yï÷~o¾ç{¾€¿ú«¿â¥_ú¥ùèþh¾æk¾€§?ýé<øÁæ¿ÊGôGó5_ó5ü{½ßkÿ8W]õ/‰Ú¸þ•ŸÄ¿ÿ®ºê_ãä£ï`½»Éá='ø÷ø„¯zMõÒg¸êÿ‡{ŸþéŸæƒ?øƒ¹êªøà•^é•xøÃÎUW½¨žò”§ðÇüǼû»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUÿ Hâ~¿õ[¿Åk¿ökóïñÚ¯ýÚüÎïü¿õ[¿Åk¿ökó¼÷{¿7ßó=ßÀOýÔOñÖoýÖ¼ôK¿4ó7ÀÓŸþtüàóoõÚ¯ýÚüÎïü¶ynŸýÙŸÍç|Îçð[¿õ[¼ök¿6ÿ’ŸþéŸæmÞæmø¨ú(¾ú«¿š‡<ä!Üzë­¼ÔK½ý×͵þèæ»¿û»¹téÿVï÷Ú?ÎUWýK¢6®å'qçï?†«®ú×8ùè;XïnrxÏ þ=>á«^“G½ô®úÿáž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕÿ’¸ßoýÖoñÚ¯ýÚü{|õW5ó1ÀG}ÔGñÕ_ýÕ¼0'Nœ`ww€§?ýé<øÁà£?ú£ùš¯ù>ë³>‹ÏþìÏæÙÝÝå!yÇçÁ~0¿õ[¿Å½ök¿6¿ó;¿€mžÛgögó9Ÿó9üÖoý¯ýگ͋âÁ~0ÏxÆ3xé—~i¾ë»¾‹—y™—໾ë»xï÷~oþ'“Äóó~¯ýã\uÕ¿$jãúW~wþþc¸êª“¾ƒõî&‡÷œàßã¾ê5yÔKŸáªÿî¹ç~ú§šþà檫þ5~à~€Wz¥Wâá8W]õ¢zÊSžÂÿñóîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUWý/ ‰ûýÖoý¯ýگͿǭ·ÞÊCòŽ?ÎoýÖoñÒ/ýÒú£?š¯ùš¯à½Þë½øžïù.^¼ÈñãÇùŸLÏÏû½ösÕUÿ’¨ë_ùIÜùû᪫þ5N>úÖ»›Þs‚Oøª×äQ/}†«þ¸çž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]õ¿€$î÷[¿õ[¼ök¿6ÿ^ýÑÍ×|Í×püøq~ê§~Š×~í׿~»»»|Í×| ŸýÙŸ À±cÇøíßþm^ú¥_šzï÷~o¾ç{¾€ãÇóS?õS¼ök¿6÷ÛÝÝåk¾ækøìÏþlî÷WõW¼ôK¿4ôÚ¯ýÚüÎïüŸýÙŸÍ[½Õ[ðÒ/ýÒ|ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ¼(n½õVò‡ð@ïõ^ïÅw÷wó?$žŸ÷{í窫þ%Q׿ò“¸ó÷ÃUWýkœ|ô¬w79¼çÿŸðU¯É£^ú WýÿpÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêIÜï·~ë·xí×~mþ#¼÷{¿7ßó=ßÃýüàóà?€¿þë¿fww€cÇŽñÕ_ýÕ¼÷{¿7Ïmww—×~í׿oþæo¸ßƒü`üàð×ý×ìîîr¿ïú®ïâ½ßû½ynýÑÍ×|Í×ð@ÇçâÅ‹|ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ¼¨^ú¥_š¿ù›¿á~?õS?Å[¿õ[ó?$žŸ÷{í窫þ%Q׿ò“¸ó÷ÃUWýkœ|ô¬w79¼çÿŸðU¯É£^ú WýÿpÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêIÜï·~ë·xí×~mþ£|ög6_ýÕ_Í¥K—x~^ëµ^‹¯þê¯æ¥_ú¥ya>û³?›¯þê¯æÒ¥Kû³?›ÏùœÏà·~ë·xí×~m^TßýÝßÍû¼Ïûð =ˆ[o½•ÿ $ñü¼ßkÿ8W]õ/‰Ú¸þ•ŸÄ¿ÿ®ºê_ãä£ï`½»Éá='ø÷ø„¯zMõÒg¸êÿ‡{ŸþéŸæƒ?øƒ¹êªøà•^é•xøÃÎUW½¨žò”§ðÇüǼû»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUÿ üöoÿ6÷{é—~iŽ?ΤÝÝ]~ú§š[o½•¿þë¿æÁ~0Çç­ßú­yé—~i^T»»»üôOÿ4·Þz+ý×ÍñãÇyðƒÌK¿ôKóÖoýÖ¼(~ú§š¿þë¿àÁ~0oýÖoÍñãǹõÖ[¹õÖ[xé—~iŽ?΋ê¯ÿú¯y™—y>ê£>Нþê¯æIá«^“G½ô®úÿáž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUÿç}ög6Ÿó9ŸÀÓŸþtüàó¿$žŸ÷{í窫þ%Q׿ò“¸ó÷ÃUWýkœ|ô¬w79¼çÿŸðU¯É£^ú WýÿpÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêªÿóò‡pë­·òZ¯õZüöoÿ6ÿ[Hâùy¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGßÁzw“Ã{Nðïñ _õš<ê¥ÏpÕÿ÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ú?í}Þç}øîïþn¾ë»¾‹÷~ï÷æ Iá«^“G½ô®úÿáž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUÿ§üõ_ÿ5ïó>ïÃñãǹõÖ[¹õÖ[x©—z)þú¯ÿšÿM$ñü¼ßkÿ8W]õ/‰Ú¸þ•ŸÄ¿ÿ®ºê_ãä£ï`½»Éá='ø÷ø„¯zMõÒg¸êÿ‡{ŸþéŸæƒ?øƒ¹êªøà•^é•xøÃÎUW½¨žò”§ðÇüǼû»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUWýŸ#‰:vì¿ýÛ¿ÍK¿ôKó¿‰$žŸ÷{í窫þ%Q׿ò“¸ó÷ÃUWýkœ|ô¬w79¼çÿŸðU¯É£^ú WýÿpÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêªÿs^ú¥_š¿ù›¿à­Þê­øìÏþl^ú¥_šÿm$ñü¼ßkÿ8W]õ/‰Ú¸þ•ŸÄ¿ÿ®ºê_ãä£ï`½»Éá='ø÷ø„¯zMõÒg¸êÿ‡{ŸþéŸæƒ?øƒ¹êªøà•^é•xøÃÎUW½¨žò”§ðÇüǼû»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žŸ÷{í窫þ%Q׿ò“¸ó÷ÃUWýkœ|ô¬w79¼çÿŸðU¯É£^ú WýÿpÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\uÕ¿$jãúW~wþþc¸êª“¾ƒõî&‡÷œàßã¾ê5yÔKŸáªÿî¹ç~ú§šþà檫þ5~à~€Wz¥Wâá8W]õ¢zÊSžÂÿñóîïþî\uÕ¿Æ·|Ë·ðVoõV\wÝu\uÕ‹ê¯ÿú¯¹õÖ[yë·~k®ºêª«þA¶ÍUW]uÕÿP’x~Þﵜ«®ú—Dm\ÿÊOâÎß W]õ¯qòÑw°ÞÝäðžü{|ÂW½&zé3\õÿÃ=÷ÜÃOÿôOóÁüÁ\uÕ¿ÆüÀðJ¯ôJ<üá窫^TOyÊSøã?þcÞýÝß«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏÏû½ösÕUÿ’¨ë_ùIÜùû᪫þ5N>úÖ»›Þs‚Oøª×äQ/}†«þ¸çž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâùy¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGßÁzw“Ã{Nðïñ _õš<ê¥ÏpÕÿ÷Üs?ýÓ?ÍðsÕUÿ?ð?À+½Ò+ñð‡?œ«®zQ=å)Oáÿøy÷ww®ºê_ã[¾å[x«·z+®»î:®ºêEõ×ý×Üzë­¼õ[¿5W]uÕUÿ‹ Ûæª«®ºê(Iù“?™«®ú×ø™Ÿùô ñÒ/ýÒ\uÕ‹êž{îá§ú§ùàþ`®ºê_ã~àx¥Wz%þð‡sÕU/ª§<å)üñÿ1ïþîïÎUWýk|Ë·| oõVoÅu×]ÇUW½¨þú¯ÿš[o½•·~ë·æª«®ºêdÛ\uÕUWý%‰çÇ6W]õ/Y­V|õW5ŸüÉŸÌUWýküÌÏü zЃxé—~i®ºêEuÏ=÷ðÓ?ýÓ|ð0W]õ¯ñ?ð¼Ò+½øÃ¹êªÕSžòþøÿ˜w÷w窫þ5¾å[¾…·z«·âºë®ãª«^Tý×Í­·ÞÊ[¿õ[sÕUW]õ¿²m®ºêª«þ‡’Äóc›«®ú—¬V+¾ú«¿šOþäO檫þ5~æg~†=èA¼ôK¿4W]õ¢ºçž{øéŸþi>øƒ?˜«®ú×øø^é•^‰‡?üá\uÕ‹ê)Oy üÇÌ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žÛ\uÕ¿dµZñÕ_ýÕ|ò'2W]õ¯ñ3?ó3ÜrË-¼Ì˼ W]õ¢º÷Þ{ù©Ÿú)>øƒ?˜«®ú×øø^ñ_‘G<â\uÕ‹ê©O}*ôGÄ»¿û»sÕUÿßò-ßÂ[½Õ[qÝu×qÕU/ª¿þë¿æÖ[oå­ßú­¹êª«®ú_Ù6W]uÕUÿCIâù±ÍUWýKV«_ýÕ_Í'ò'sÕUÿ?ó3?Ã-·ÜÂ˼ÌËpÕU/ª{ï½—Ÿú©Ÿâƒ?øƒ¹êªøà_ñyÄ#ÁUW½¨žúÔ§òGôG¼û»¿;W]õ¯ñ-ßò-¼Õ[½×]wW]õ¢úë¿þkn½õVÞú­ßš«®ºêªÿEmsÕUW]õ?”$žŸ÷{í窫þ%Q׿ò“¸ó÷ÃUWýkœ|ô¬w78¼çW]õ¢ê¶VœzôÜóç窫þ5μä3Ø¿ã$« Û\uÕ‹j~ò€í›ÎqöoÌUWýk\÷òOáÂnb8˜sÕU/ªÍë.2;~È…'ÜÀ·ÿÖÛqÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\uÕ¿$jãúW~wþþc¸êª“¾“õî‡÷œàª«^TÝÖŠS¾ƒ{þüá\uÕ¿Æ™—|ûwœdua›«®zQÍO°}Ó9Îþ탹êªë^þ)\xÂM s®ºêEµyÝEfÇ¹ð„›øößz;®ºêª«þ@¶ÍUW]uÕÿP’x~Þﵜ«®ú—Dm\ÿÊOâÎß W]õ¯qòÑw²ÞÝàðž\uÕ‹ªÛZqêÑwpÏŸ?œ«®ú×8ó’Ï`ÿŽ“¬.lsÕU/ªùɶo:ÇÙ¿}0W]õ¯qÝË?… O¸‰á`ÎUW½¨6¯»Èìø!žpßþ[oÇUW]uÕÿȶ¹êª«®úJÏÏû½ösÕUÿ’¨ë_ùIÜùû᪫þ5N>úNÖ»Þs‚«®zQu[+N=úîùó‡sÕUÿg^òìßq’Õ…m®ºêE5?yÀöMç8û·檫þ5®{ù§pá 71̹êªÕæu™?äÂnàÛëí¸êª«®ú_Ù6W]uÕUÿCIâùy¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGßÉzwƒÃ{NpÕU/ªnkÅ©GßÁ=þp®ºê_ãÌK>ƒý;N²º°ÍUW½¨æ'ؾégÿöÁ\uÕ¿Æu/ÿ.<á&†ƒ9W]õ¢Ú¼î"³ã‡\xÂM|ûo½W]uÕUÿ Ûæª«þ›}Îç|zЃxï÷~o®ºê~’x~Þﵜ«®ú—Dm\ÿÊOâÎß W]õ¯qòÑw²ÞÝàðž\uÕ‹ªÛZqêÑwpÏŸ?œ«®ú×8ó’Ï`ÿŽ“¬.lsÕU/ªùɶo:ÇÙ¿}0W]õ¯qÝË?… O¸‰á`ÎUW½¨6¯»Èìø!žpßþ[oÇUW]uÕÿȶ¹êªÿf’x­×z-~û·›«®ºŸ$žŸ÷{í窫þ%Q׿ò“¸ó÷ÃUWýkœ|ô¬w78¼çW]õ¢ê¶VœzôÜóç窫þ5μä3Ø¿ã$« Û\uÕ‹j~ò€í›ÎqöoÌUWýk\÷òOáÂnb8˜sÕU/ªÍë.2;~È…'ÜÀ·ÿÖÛqÕUW]õ¿²m®ºê¿™$^ëµ^‹ßþíßæª«î'‰ççý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'}'ëÝ ï9ÁUW½¨º­§}÷üùùêª3/ù öï8ÉêÂ6W]õ¢šŸ<`û¦sœýÛsÕUÿ×½üS¸ð„›æ\uÕ‹jóº‹ÌŽrá 7ðí¿õv\uÕUWý/€l›«®úoöÚ¯ýÚ¼ôK¿4_ýÕ_ÍUWÝOÏÏû½ösÕUÿ’¨ë_ùIÜùû᪫þ5N>úNÖ»Þs‚«®zQu[+N=úîùó‡sÕUÿg^òìßq’Õ…m®ºêE5?yÀöMç8û·檫þ5®{ù§pá 71̹êªÕæu™?äÂnàÛëí¸êª«®ú_Ù6W]uÕUÿCIâùy¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGßÉzwƒÃ{NpÕU/ªnkÅ©GßÁ=þp®ºê_ãÌK>ƒý;N²º°ÍUW½¨æ'ؾégÿöÁ\uÕ¿Æu/ÿ.<á&†ƒ9W]õ¢Ú¼î"³ã‡\xÂM|ûo½W]uÕUÿ Ûæª«®ºê(Ië³x ?øÁ<ãÏàéO:~ðƒyAvww9qâoõVoÅOÿôOðÙŸýÙ|Îç|¶ùœÏù>û³?›ççÁ~0?õS?ÅK¿ôKó‚üôOÿ4ó1í·ÞÊóóÒ/ýÒ|×w}/ýÒ/ÍÿD’x~Þﵜ«®ú—Dm\ÿÊOâÎß W]õ¯qòQw²ÞÛàðî\uÕ‹ªÛ\qê±wpÏŸ=œ«®ú×8óÏ`ÿ®“¬ÎosÕU/ªù‰¶o9ÇÙ¿y0W]õ¯qÝË?… O¸‰á`ÎUW½¨6¯»Èìø!žpßþ[oÇUW]uÕÿȶ¹êª#I<øÁfww—ÝÝ]Ž;Æ{¿÷{süøq~ú§š¿ù›¿á~ßõ]ßÅ{¿÷{óÜ$ðZ¯õZüöoÿ6$ €·~ë·æ·û·ÙÝÝåAzoýÖoÍñãÇùéŸþiþæoþ€ãÇóS?õS¼ök¿6ÿV?ýÓ?ÍÛ¼ÍÛp¿—z©—âøñãìîîò7ó7Üï³>ë³øìÏþlî÷ÙŸýÙ|Îç|ŸõYŸÅgögó‚|õW5ó1Àw}×wñÞïýÞ|ög6Ÿó9ŸÀ{¿÷{óÝßýÝÜïµ^뵸õÖ[yÆ3žÁýŽ?ÎÓŸþtŽ?Îsûê¯þj>æc>†û=èAâÁ~0ý×Í¥K—8~ü8¿õ[¿ÅK¿ôKó?$žŸ÷{í窫þ%Q׿ò“¸ó÷ÃUWýkœ|Ô¬÷68¼ûW]õ¢ê6WœzìÜóg窫þ5μÄ3Ø¿ë$«óÛ\uÕ‹j~â€í[ÎqöoÌUWýk\÷òOáÂnb8˜sÕU/ªÍë.2;~È…'ÜÀ·ÿÖÛqÕUW]õ¿²m®ºêßHô^ïõ^|õW5Çç~ßýÝßÍû¼Ïûpüøqžþô§süøqH¯õZ¯Åoÿöoó@’x ÷z¯÷⻿û»y ¯þê¯æc>æc8~ü8OúÓ9~ü8ÿyÈC¸õÖ[9vì?ýÓ?Ík¿ökó@?ýÓ?ÍÛ¼ÍÛpüøqžþô§süøqn½õVò‡ðà?˜§?ýé¼ /ó2/Ã_ÿõ_sìØ1vww¹ßgögó9Ÿó9ÜïAz_ýÕ_Í[¿õ[s¿ßþíßæ­ßú­¹té_õU_ÅGôGó@¿ýÛ¿Íë¼Îëp¿¯úª¯â£?ú£¹ßîî.ýÑÍ÷|Ï÷ðÒ/ýÒüÕ_ýÿÓHâùy¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGÝÉzoƒÃ»OpÕU/ªnsÅ©ÇÞÁ=öp®ºê_ãÌK<ƒý»N²:¿ÍUW½¨æ'ؾågÿæÁ\uÕ¿Æu/ÿ.<á&†ƒ9W]õ¢Ú¼î"³ã‡\xÂM|ûo½W]uÕUÿ Ûæª«þ$q¿—z©—â¯ÿú¯y~>û³?›ÏùœÏà½Þë½øîïþnH¯õZ¯Åoÿöoó@’¸ßk½ÖkñÛ¿ýÛû³?›ÏùœÏàØ±cüõ_ÿ5~ðƒynßýÝßÍû¼ÏûðZ¯õZüöoÿ6ô:¯ó:üöoÿ6?õS?Å[¿õ[óü¼ök¿6¿ó;¿À_ýÕ_ñÒ/ýÒüO"‰ççý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'u'ë½ ï>ÁUW½¨ºÍ§{÷üÙùêª3/ñ öï:Éêü6W]õ¢šŸ8`û–sœý›sÕUÿ×½üS¸ð„›æ\uÕ‹jóº‹ÌŽrá 7ðí¿õv\uÕUWý/€l›«®ú7’Äý~ë·~‹×~í׿9~ü8—.]âøñã\¼x‘’Àk½ÖkñÛ¿ýÛ<$î÷ô§??øÁçs>€÷z¯÷⻿û»yA$ðZ¯õZüöoÿ6÷»õÖ[yÈCÀK½ÔKñ×ý×¼ ßýÝßÍgögóà?˜þèæ­ßú­ùŸDÏÏû½ösÕUÿ’¨ë_ùIÜùû᪫þ5N>êNÖ{Þ}‚«®zQu›+N=öîù³‡sÕUÿg^âìßu’Õùm®ºêE5?qÀö-ç8û7檫þ5®{ù§pá 71̹êªÕæu™?äÂnàÛëí¸êª«®ú_Ù6W]õo$ €cÇŽ±»»Ë óÖoýÖüÌÏü õWÅK¿ôKs?I¼Ök½¿ýÛ¿ÍIà¥^ê¥øë¿þk^˜×~í׿w~çwxúӟ΃ü`þµŽ?Î¥K—xí×~m>ú£?š·z«·â_ãøñã\ºt‰ãÇsñâEžÛCòn½õVô që­·ò@ŸýÙŸÍç|Îçð]ßõ]¼÷{¿7/ˆ$îg›û}÷w7ïó>ïÀg}ÖgñÙŸýÙüO±»»Ëç|ÎçðÝßýÝìîîòoõ~¯ýã\uÕ¿$jãúW~wþþc¸êª“º“õÞ‡wŸàª«^TÝæŠS½ƒ{þìá\uÕ¿Æ™—xûwdu~›«®zQÍO°}Ë9Îþ̓¹êªë^þ)\xÂM s®ºêEµyÝEfÇ¹ð„›øößz;®ºêª«þ@¶ÍUWýIàµ^ëµøíßþm^˜ÏþìÏæs>çsø­ßú-^ûµ_›ûIàµ^ëµøíßþmHïõ^ïÅw÷wóÂ|ôG4_ó5_ÀoýÖoñÚ¯ýÚÜzë­|Ï÷|/Ì{½×{ñà?€¯þê¯æc>æcxn¯ýÚ¯Í[¿õ[óZ¯õZ¼ôK¿4/Ì{¿÷{ó=ßó=üÔOýoýÖoÍý~ú§š·y›·à³>ë³øìÏþlè³?û³ùœÏù~ë·~‹×~í׿‘Äýls¿ÏþìÏæs>çsø©Ÿú)Þú­ßšÿ)>ú£?š¯ùš¯áßëý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'u'ë½ ï>ÁUW½¨ºÍ§{÷üÙùêª3/ñ öï:Éêü6W]õ¢šŸ8`û–sœý›sÕUÿ×½üS¸ð„›æ\uÕ‹jóº‹ÌŽrá 7ðí¿õv\uÕUWý/€l›«®ú7’À[½Õ[ñÓ?ýÓ¼0ŸýÙŸÍç|ÎçðYŸõY|ög6÷“Àk½ÖkñÛ¿ýÛ<$>ë³>‹ÏþìÏæ…ùìÏþl>çs>€ßú­ßâµ_ûµùíßþm^çu^‡æ·~ë·xí×~mî÷ÙŸýÙ|õW5—.]âùyðƒÌ{¿÷{óYŸõYû³?›ÏùœÏà·~ë·xí×~m^IÜÏ6÷ûìÏþl>çs>€ßú­ßâµ_ûµùŸâĉìîîòïõ~¯ýã\uÕ¿$jãúW~wþþc¸êª“º“õÞ‡wŸàª«^TÝæŠS½ƒ{þìá\uÕ¿Æ™—xûwdu~›«®zQÍO°}Ë9Îþ̓¹êªë^þ)\xÂM s®ºêEµyÝEfÇ¹ð„›øößz;®ºêª«þ@¶ÍUWýIà½Þë½øîïþn^˜ÏþìÏæs>çsøª¯ú*>ú£?šûIàµ^ëµøíßþmHŸõYŸÅgögóÂ|ög6Ÿó9ŸÀoýÖoñÚ¯ýÚüöoÿ6¯ó:¯Ã ó[¿õ[¼ök¿6´»»ËOÿôOóÓ?ýÓüÌÏü ÏÏK¿ôKó[¿õ[?~œçöà?˜g<ã\¼x‘ãdz»»ËCòvwwy­×z-~û·›çöÙŸýÙ|Îç|¿õ[¿Åk¿ökó‚Hâ~¶¹ßGôGó5_ó5üÖoý¯ýÚ¯ÍÿÇçÒ¥Kü{½ßkÿ8W]õ/‰Ú¸þ•ŸÄ¿ÿ®ºê_ãä£îd½·ÁáÝ'¸êªU·¹âÔcïàž?{8W]õ¯qæ%žÁþ]'Yßæª«^Tólßr޳ó`®ºê_㺗 žpÃÁœ«®zQm^w‘ÙñC.<á&¾ý·ÞŽ«®ºêªÿmsÕUÿF’x­×z-~û·›æ³?û³ùœÏù~ë·~‹×~í׿~’x­×z-~û·›’ÀG}ÔGñÕ_ýÕ¼0ïýÞïÍ÷|Ï÷ð[¿õ[¼ök¿6·Þz+ßýÝßÍ óÞïýÞ<øÁæ…ùíßþm~ú§šŸþéŸæÏx÷{¯÷z/¾û»¿›çöÕ_ýÕ|ÌÇ| ßõ]ßÅ{¿÷{óÝßýݼÏû¼ßõ]ßÅ{¿÷{óÜ>û³?›ÏùœÏà·~ë·xí×~m^IÜÏ6÷ûìÏþl>çs>€Ÿú©Ÿâ­ßú­ùŸâ£?ú£ùš¯ùþ½Þﵜ«®ú—Dm\ÿÊOâÎß W]õ¯qòQw²ÞÛàðî\uÕ‹ªÛ\qê±wpÏŸ=œ«®ú×8óÏ`ÿ®“¬ÎosÕU/ªù‰¶o9ÇÙ¿y0W]õ¯qÝË?… O¸‰á`ÎUW½¨6¯»Èìø!žpßþ[oÇUW]uÕÿȶ¹êª#I<øÁæéO:/Ìk¿ökó;¿ó;üÕ_ý/ýÒ/Íý$ðZ¯õZüöoÿ6$ €×z­×â·û·ya^æe^†¿þë¿àâÅ‹?~œÿ ßýÝßÍû¼Ïûp?Û<·[o½•‡<ä!¼Õ[½?ýÓ?Í[¿õ[ó3?ó3;vŒ[o½•ãÇóÜ>û³?›ÏùœÏà·~ë·xí×~m^IÜÏ6÷ûê¯þj>æc>€Ïú¬Ïâ³?û³yAvwwy›·y^ú¥_š—z©—â½ßû½ùÏöÑýÑ|÷w7—.]âßêý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'u'ë½ ï>ÁUW½¨ºÍ§{÷üÙùêª3/ñ öï:Éêü6W]õ¢šŸ8`û–sœý›sÕUÿ×½üS¸ð„›æ\uÕ‹jóº‹ÌŽrá 7ðí¿õv\uÕUWý/€l›«®ú7’Äýžþô§óà?˜çgww—'Nð =ˆ[o½•’Àk½ÖkñÛ¿ýÛ<$îwñâEŽ?Îósë­·ò‡<€—z©—â¯ÿú¯ù×úìÏþl~æg~†¿þë¿æ·~ë·xí×~m^?øÁ<ãÏÀ6ÏÏ[¿õ[ó3?ó3<ýéOç!yïõ^ïÅw÷wóü|ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ¼ ’¸Ÿmî÷×ý×¼Ì˼ /ýÒ/Í_ýÕ_ñ‚üôOÿ4oó6oÀ{½×{ñÝßýÝüO"‰ççý^ûǹêªIÔÆõ¯ü$îüýÇpÕUÿ'u'ë½ ï>ÁUW½¨ºÍ§{÷üÙùêª3/ñ öï:Éêü6W]õ¢šŸ8`û–sœý›sÕUÿ×½üS¸ð„›æ\uÕ‹jóº‹ÌŽrá 7ðí¿õv\uÕUWý/€l›«®ú7’ÄýÞú­ßšŸú©Ÿâùùèþh¾æk¾€Ïú¬Ïâ³?û³y I¼Ök½¿ýÛ¿ÍIâ~õQÅWõWóü¼ÍÛ¼ ?ýÓ? Àw}×wñÞïýÞük}÷w7ïó>ïÀ[¿õ[óS?õSû³?€=èAüõ_ÿ5Çç$ðZ¯õZüöoÿ6$‰úìÏþl>ë³>‹ûíîîò1ó1|÷w7/õR/Å_ÿõ_óo±»»Ëƒü`.]ºÀgögóQõQ?~œûíîîò>ïó>üôOÿ4ŸõYŸÅgögó‚?~œK—.q¿=èAÜzë­¼ ŸýÙŸÍç|Îçð[¿õ[¼ök¿6/ˆ$îg›úíßþm^çu^€ãÇó]ßõ]¼õ[¿5÷ÛÝÝåc>æcøîïþn^ëµ^‹ßþíßæIÁUW½¨ºÍ§{÷üÙùêª3/ñ öï:Éêü6W]õ¢šŸ8`û–sœý›sÕUÿ×½üS¸ð„›æ\uÕ‹jóº‹ÌŽrá 7ðí¿õv\uÕUWý/€l›«®ú7’ÄýlóŸA¯õZ¯Åoÿöoó¿Íë¼ÎëðÛ¿ýÛ;vŒÝÝ]®úבÄóó~¯ýã\uÕ¿$jãúW~wþþc¸êª“º“õÞ‡wŸàª«^TÝæŠS½ƒ{þìá\uÕ¿Æ™—xûwdu~›«®zQÍO°}Ë9Îþ̓¹êªë^þ)\xÂM s®ºêEµyÝEfÇ¹ð„›øößz;®ºêª«þ@¶ÍUWýIâ~¶ùÏ €×z­×â·û·ùß仿û»yŸ÷y>ë³>‹ÏþìÏæªIêNÖ{Þ}‚«®zQu›+N=öîù³‡sÕUÿg^âìßu’Õùm®ºêE5?qÀö-ç8û7檫þ5®{ù§pá 71̹êªÕæu™?äÂnàÛëí¸êª«®ú_Ù6W]uÕUÿCIâùy¿×þq®ºê_µqý+?‰;ÿ1\uÕ¿ÆÉGÝÉzoƒÃ»OpÕU/ªnsÅ©ÇÞÁ=öp®ºê_ãÌK<ƒý»N²:¿ÍUW½¨æ'ؾågÿæÁ\uÕ¿Æu/ÿ.<á&†ƒ9W]õ¢Ú¼î"³ã‡\xÂM|ûo½W]uÕUÿ Ûæª«®ºê(IÁUW½¨ºÍ§{÷üÙùêª3/ñ öï:Éêü6W]õ¢šŸ8`û–sœý›sÕUÿ×½üS¸ð„›æ\uÕ‹jóº‹ÌŽrá 7ðí¿õv\uÕUWý/€l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýÙŸåæ›oæe^æe¸êªÕ½÷ÞËOþäOò!ò!\uÕ¿Æþàò ¯ð <âફ^TO}êSùÃ?üCÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYn¾ùf^æe^†«®zQÝ{ï½üäOþ$ò!ÂUWýküàþ ¯ð ¯À#ñ®ºêEõÔ§>•?üÃ?ä=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÏm®ºê_²Z­øê¯þj>ù“?™«®ú×øÙŸýYnºé&^öe_–«®zQÝwß}üÄOüò!ÂUWýküàþ /ÿò/Ï#ùH®ºêEõ´§=?øƒ?à=Þã=¸êªoù–oá­Þê­¸îºë¸êªÕ_ÿõ_së­·òÖoýÖ\uÕUWý/‚l›«®ºêªÿ¡$ñüØæª«þ%«ÕНþê¯æ“?ù“¹êªŸýٟ妛nâe_öe¹êªÕ}÷ÝÇOüÄOð!ò!\uÕ¿Æþàòò/ÿò<ò‘䪫^TO{ÚÓøƒ?øÞã=Þƒ«®ú×ø–oùÞê­ÞŠë®»Ž«®zQýõ_ÿ5·Þz+oýÖoÍUW]uÕÿ"ȶ¹êª«®úJÿÞû½ß›¯úª¯âøñãüK~û·›ù˜á¯ÿú¯ù·zðƒÌW}ÕWñÖoýÖüKvwwùœÏù¾û»¿›ÝÝ]þ­Þû½ß›¯úª¯âøñãüK~û·›ù˜á¯ÿú¯ù·zé—~i>ë³>‹·~ë·æ_²»»Ëç|ÎçðÝßýÝìîîòoqüøqÞú­ßš¯úª¯âøñãüK¾ú«¿šOû´Oãè舫—~é—æ³>ë³xë·~kþ%»»»|Îç|ßýÝßÍîî.ÿÇç­ßú­ùª¯ú*Ž?οä·û·ù˜ùþú¯ÿš«—~é—æ³>ë³xë·~kþ%»»»|Îç|ßýÝßÍîî.ÿÇç­ßú­ùª¯ú*Ž?οä·û·ù˜ùþú¯ÿš«—~é—æ³>ë³xë·~kþ%ÏxÆ3x›·yþáþaø·8~ü8oýÖoÍW}ÕWqüøqþ%¿ýÛ¿ÍÇ|ÌÇð×ý×ü[½ôK¿4ŸõYŸÅ[¿õ[ó/ÙÝÝåc>æcøéŸþivwwù·8~ü8ïýÞïÍg}Ögqüøqþ%?ýÓ?Íç|Îçð×ý×ü[½ôK¿4_õU_Åk¿ökó/ÙÝÝåc>æcøéŸþivwwù·8~ü8ïýÞïÍg}Ögqüøqþ%Ÿò)ŸÂ×ý×sppÀ¿ÕK¿ôKóU_õU¼ök¿6ÿ’ÝÝ]>æc>†ŸþéŸfww—‹ãÇóÞïýÞ|Ög}Çç_òÓ?ýÓ|Îç|ý×Í¿ÕK¿ôKóU_õU¼ök¿6ÿ’ÝÝ]>æc>†ŸþéŸfww—‹ãÇóÞïýÞ|Ög}Çç_òÓ?ýÓ|Îç|ý×Í¿ÕK¿ôKóU_õU¼ök¿6ÿ’g<ã¼þë¿>·ÝvÃ0ðoqüøqÞû½ß›Ïú¬ÏâøñãüK~ú§šÏùœÏá¯ÿú¯ù·zé—~i¾ê«¾Š×~í׿_²»»ËÇ|ÌÇðÝßýÝü[?~œ÷~ï÷æ³>ë³8~ü8ÿ’ŸþéŸæc>æc¸õÖ[ù·zé—~i¾ê«¾Š×~í׿_²»»Ëû¼ÏûðÓ?ýÓü[?~œ÷~ï÷櫾ê«xQ¼íÛ¾-¿ôK¿Äjµâßêµ_ûµùª¯ú*^ú¥_šÉ­·ÞÊÇ|ÌÇðÓ?ýÓü[?~œþèæ³>ë³xQ|Í×| _ýÕ_Í­·ÞÊ¿Õk¿ökóU_õU¼ôK¿4ÿ’[o½•ù˜á§ú§ù·zðƒÌ{¿÷{óYŸõY¼(¾æk¾†¯þê¯æÖ[oåßê­ßú­ù¬Ïú,^ú¥_šÉïÿþïóŽïøŽÜ}÷Ýü[=øÁæ½ßû½ù¬Ïú,^_ó5_ÃWõWsë­·òoõÖoýÖ|Ög}/ýÒ/Í¿äÖ[oåc>æcøéŸþiþ­üàóÞïýÞ|Ög}/Нùš¯á«¿ú«¹õÖ[ù·zë·~k>ë³>‹—~é—æ_rë­·ò1ó1üôOÿ4ÿV~ðƒyï÷~o>ë³>‹ÅÛ¾íÛòË¿üË,—KîwüøqÞû½ß›¯úª¯âª«®ºê0dÛ\uÕUWý%‰ÿ(¯ýÚ¯ÍoýÖoñÂÜzë­¼Ì˼ »»»üGø­ßú-^ûµ_›æ£?ú£ùš¯ùþ#¼ök¿6¿õ[¿Å së­·ò2/ó2ìîîòá·~ë·xí×~m^˜þèæk¾ækøðÚ¯ýÚüÖoý/Ìïÿþïóš¯ùšØæ?ÂoýÖoñÚ¯ýÚ¼0ýÑÍ×|Í×ðáµ_ûµù­ßú-^˜[o½•—y™—aww—ÿ¿õ[¿Åk¿ökóÂ|ôG4_ó5_Ä·~ë·æ§~ê§xaþú¯ÿš—y™—á?Ê_ýÕ_ñÒ/ýÒ¼07Üpwß}7ÿÞú­ßšŸú©Ÿâ…ùë¿þk^æe^†ÿ(õWÅK¿ôKó¼ÍÛ¼ ?ýÓ?Í„·~ë·æ§~ê§xaþú¯ÿš—y™—á?Ê_ýÕ_ñÒ/ýÒ¼0oó6oÃOÿôOóá­ßú­ù©Ÿú)^˜oÿöoç>àøòWõW¼ôK¿4/Ìë¼ÎëðÛ¿ýÛüGø¨ú(¾ú«¿šæ·û·y×yþ#?~œ¿ú«¿âÁ~0/Ìë¼ÎëðÛ¿ýÛüGø¨ú(¾ú«¿šæ·û·y×yþ#?~œ¿ú«¿âÁ~0/̉'ØÝÝå?ÂG}ÔGñÕ_ýÕ¼0¿ýÛ¿Íë¼ÎëðáøñãüÕ_ý~ðƒya^çu^‡ßþíßæ?ÂG}ÔGñÕ_ýÕ¼0¿ýÛ¿Íë¼ÎëðáøñãüÕ_ý~ðƒya^æe^†¿þë¿æ?ÂG}ÔGñÕ_ýÕ¼0Ÿò)ŸÂñóáøñã<ýéOçøñã¼0/ó2/Ã_ÿõ_óá³>ë³øìÏþl^˜ŸþéŸæmÞæmøpüøqžþô§süøq^˜—y™—á¯ÿú¯ùðYŸõY|ög6/ÌOÿôOó6oó6üG8~ü8OúÓ9~ü8/ÌÆÆËå’ÿŸõYŸÅgögóÂüôOÿ4oó6oÄãÇóô§?ãÇó¼Ì˼ ý×Í„Ïú¬Ïâ³?û³ya¾û»¿›÷yŸ÷á?ƒü`þê¯þŠãÇó‚ìîîò2/ó2Üzë­üGøª¯ú*>ú£?šæmÞæmøéŸþi^ú¨â«¿ú«¹êª«®ú Ù6W]uÕUÿCIâ?ÒÓŸþtüàó‚|õW5ó1Ô÷z¯÷⻿û»yaNœ8Áîî.ÿQžþô§óà?˜ä«¿ú«ù˜ùþ£¼×{½ßýÝßÍ #‰ÿHOúÓyðƒÌ ò:¯ó:üöoÿ6ÿQ>ê£>Нþê¯æ…‘Ĥ‹/rüøq^ÏþìÏæs>çsøòQõQ|õW5/Œ$þ#]¼x‘ãÇó‚|ög6Ÿó9ŸÃ”ú¨â«¿ú«ya$ñÉ6/Ìgögó9Ÿó9üGù¬Ïú,>û³?›Fÿ‘lóÂ|ög6Ÿó9ŸÃ”Ïú¬Ïâ³?û³ya$ñÉ6/Ì˾ìËòWõWüGù¬Ïú,>û³?›dww—'NðÉ6/ÌGôGó5_ó5üGù¬Ïú,>û³?›dww—'NðÉ6/Ì{¿÷{ó=ßó=üGù¬Ïú,>û³?›ä÷ÿ÷y×x þ£?~œ‹/ò¼÷{¿7ßó=ßÔ¯úª¯â£?ú£yAn½õVò‡ðåøñã\¼x‘æ½ßû½ùžïùþ£|ÕW}ýÑÍ ò×ý×¼Ì˼ ÿQŽ?ÎÅ‹yan¸áî¾ûnþ£|×w}ïýÞïÍ ò×ý×¼Ì˼ ÿQüàóô§?æ­ßú­ù™Ÿùþ£|×w}ïýÞïÍ ò×ý×¼Ì˼ ÿQüàóô§?æµ_ûµùßùþ£üÔOýoýÖoÍ òÕ_ýÕ|ÌÇ| ÿQ^ú¥_š¿ú«¿â…yí×~m~çw~‡ÿ(?õS?Å[¿õ[ó‚üöoÿ6¯ó:¯Ã”—~é—æ¯þê¯xa^ûµ_›ßùßá?ÊOýÔOñÖoýÖ¼ ¿ýÛ¿Íë¼Îëðåµ^ëµøíßþm^˜'N°»»Ë rüøq.^¼ÈUW]uÕÿPȶ¹êª«®úJÿ‘žþô§óà?˜ä«¿ú«ù˜ùþ£¼×{½ßýÝßÍ süøq.]ºÄ”§?ýé<øÁæùê¯þj>æc>†ÿ(ïõ^ïÅw÷wóÂHâ?ÒÓŸþtüàó‚¼Î뼿ýÛ¿Í”ú¨â«¿ú«ya$ñéâÅ‹?~œä³?û³ùœÏùþ£|ÔG}_ýÕ_Í #‰ÿH/^äøñã¼ ŸýÙŸÍç|Îçðå£>ê£øê¯þj^IüG²Í óÙŸýÙ|Îç|ÿQ>ë³>‹ÏþìÏæ…‘Ä$Û¼0ŸýÙŸÍç|Îçðå³>ë³øìÏþl^IüG²Í sóÍ7sÇwðå³>ë³øìÏþl^ÝÝ]Nœ8Á$Û¼0ýÑÍ×|Í×ðå³>ë³øìÏþl^ÝÝ]Nœ8Á$Û¼0ýÑÍ×|Í×ðå³>ë³øìÏþl^þáæ]Þå]ørìØ1vwwyaÞû½ß›ïùžïá?ÊW}ÕWñÑýѼ ·Þz+yÈCørìØ1vwwyaÞû½ß›ïùžïá?ÊW}ÕWñÑýѼ ý×Í˼ÌËð娱cìîîòœ9s†sçÎñ廾ë»xï÷~o^¿þë¿æe^æeøò =ˆ[o½•æ­ßú­ù™Ÿùþ£|×w}ïýÞïÍ ò×ý×¼Ì˼ ÿQô që­·ò¼ök¿6¿ó;¿Ã”Ÿú©Ÿâ­ßú­yA¾ú«¿šù˜á?ÊK½ÔKñ×ý×¼0¯ýÚ¯ÍïüÎïðå§~ê§xë·~k^ßþíßæu^çuøòR/õRüõ_ÿ5/Ìk¿ökó;¿ó;üGù©Ÿú)Þú­ßšä·û·y×yþ£¼Ök½¿ýÛ¿Í sâÄ vwwyAŽ;Æîî.W]uÕUÿC!Ûæª«®ºê(IüGy­×z-~û·›æÖ[oå¥_ú¥¹téÿ~ë·~‹×~í׿…ùèþh¾æk¾†ÿ¯õZ¯ÅoÿöoóÂÜzë­¼ôK¿4—.]â?ÂoýÖoñÚ¯ýÚ¼0ýÑÍ×|Í×ðáµ^ëµøíßþm^˜þèæk¾ækøò[¿õ[¼ök¿6/ÌGôGó5_ó5üGx­×z-~û·›æÖ[oå¥_ú¥¹téÿþê¯þŠ—~é—æ…ùèþh¾æk¾†ÿoõVoÅOÿôOóÂüõ_ÿ5/ó2/Ô¿ú«¿â¥_ú¥ya$ñå­Þê­øéŸþi^˜¿þë¿æe^æeøòWõW¼ôK¿4/Ì[¿õ[ó3?ó3üGx«·z+~ú§šæ¯ÿú¯y™—yþ£üÕ_ý/ýÒ/Í óÖoýÖüÌÏü ÿÞê­ÞŠŸþéŸæ…yù—yþâ/þ‚ÿ(OúÓyðƒÌ óÖoýÖüÌÏü ÿ>ê£>Нþê¯æ…ùíßþm^çu^‡ÿÇŽã¯ÿú¯yðƒÌ óÚ¯ýÚüÎïüÿ>ê£>Нþê¯æ…ùíßþm^çu^‡ÿÇŽã¯ÿú¯yðƒÌ òÛ¿ýÛ¼Îë¼ÿQ>ê£>Нþê¯æ…ùíßþm^çu^‡ÿÇŽã¯ÿú¯yðƒÌ óÚ¯ýÚüÎïüÿ>ê£>Нþê¯æ…ùíßþm^çu^‡ÿÇŽã¯ÿú¯yðƒÌ óÚ¯ýÚüÎïüÿ>ë³>‹ÏþìÏæ…y‰—x þþïÿžÿÇŽãÖ[oåøñã¼0/ýÒ/ÍßüÍßðá³>ë³øìÏþl^˜ŸþéŸæmÞæmøpìØ1n½õVŽ?Î óÒ/ýÒüÍßü ÿ>ë³>‹ÏþìÏæ…ùéŸþiÞæmÞ†ÿÇŽãÖ[oåøñã¼ ¿ýÛ¿Íë¼Îëðå³>ë³øìÏþl^˜ŸþéŸæmÞæmøpìØ1n½õVŽ?Î óÒ/ýÒüÍßü ÿ¾ê«¾Šþèæ…ùîïþnÞç}Þ‡ÿzЃøë¿þkŽ?Î ²»»ËK¿ôKóŒg<ƒÿ_õU_ÅGôGóÂ<âà)Oy /Èg}ÖgñÙŸýÙ\uÕUWý…l›«®ºêªÿ¡$ñá½Þë½øê¯þjŽ?οä·û·ùèþhþæoþ†«=èA|õW5oýÖoÍ¿dww—ÏþìÏæ»¿û»¹téÿÇŽã­ßú­ùê¯þjŽ?οä·û·ùèþhþæoþ†«—z©—â³?û³yë·~kþ%»»»|ög6ßýÝßÍ¥K—ø·8vìoýÖoÍWõWsüøq^˜ÏþìÏæs>çsø÷z©—z)>û³?›·~ë·æ_²»»ËgögóÝßýÝ\ºt‰‹cÇŽñÖoýÖ|õW5Çç_òÛ¿ýÛ|ôG4ó7ÿÕK½ÔKñÙŸýÙ¼õ[¿5ÿ’ÝÝ]>û³?›ïþîïæÒ¥Kü[;vŒ·~ë·æ«¿ú«9~ü8ÿ’ßþíßæ£?ú£ù›¿ùþ­^ê¥^ŠÏþìÏæ­ßú­ù—HâßëØ±c¼õ[¿5_ýÕ_ÍñãÇù—üöoÿ6ýÑÍßüÍßðoõR/õR|ög6oýÖoÍ¿dww—ÏþìÏæ»¿û»¹téÿÇŽã½ßû½ùìÏþlŽ?οä§ú§ùìÏþlþæoþ†«—z©—â«¿ú«yí×~mþ%»»»|ôG4?ýÓ?Í¥K—ø·8vìïýÞïÍgögsüøq^˜×~í׿w~çwø÷z©—z)¾ú«¿š×~í׿_²»»ËGôGóÓ?ýÓ\ºt‰‹cÇŽñÞïýÞ|ög6Çç_òÓ?ýÓ|ög6ó7ÿÕK½ÔKñÕ_ýÕ¼ök¿6ÿ’ÝÝ]>ú£?šŸþéŸæÒ¥Kü[;vŒ÷~ï÷æ³?û³9~ü8ÿ’ŸþéŸæ³?û³ù›¿ùþ­^ê¥^Нþê¯æµ_ûµya~û·›×y×áßëØ±c¼÷{¿7ŸýÙŸÍñãÇù—üôOÿ4ŸýÙŸÍßüÍßðoõR/õR|õW5¯ýگͿdww—þèæ§ú§¹téÿÇŽã½ßû½ùìÏþlŽ?οä§ú§ùèþhžñŒgðoõR/õR|õW5¯ýگͿdww—÷~ï÷æg~ægø·:vìýÑÍgögó/yí×~m~çw~‡¯×z­×â«¿ú«yé—~iþ%·Þz+ýÑÍÏüÌÏðouìØ1>ú£?šÏþìÏæEñÕ_ýÕ|õW5ÏxÆ3ø·z«·z+>û³?›—~é—æ_rë­·òÑýÑüÌÏü ÿVzЃxï÷~o>û³?›ÅWõWóÕ_ýÕ<ãÏàßê­Þê­øìÏþl^ú¥_šæ·û·y×yþ½ô ñÞïýÞ|ög6/Нþê¯æ«¿ú«yÆ3žÁ¿Õ[½Õ[ñÙŸýÙ¼ôK¿4ÿ’[o½•þèæg~ægø·zЃÄ{¿÷{óÙŸýÙ¼(¾ú«¿š¯þê¯æÏxÿVoõVoÅgögóÒ/ýÒüKn½õV>ú£?šŸù™ŸáßêAzïýÞïÍgögó/yí×~m~çw~‡ç¶µµÅÇ}ÜÇñÙŸýÙ\uÕUWý†l›«®ºêªÿ¡$ñüØæª«^ÏþìÏæs>çsxnŸõYŸÅgögsÕU/ˆ$žÛ\uÕ òÚ¯ýÚüÎïüÏí·~ë·xí×~m®ºêùùíßþm^çu^‡çöZ¯õZüöoÿ6W]õ‚¼ök¿6¿ó;¿Ãsû­ßú-^ûµ_›«®z~~û·›×y×á¹½Ök½¿ýÛ¿ÍUW½ ¯ýÚ¯ÍïüÎïðÜ~ë·~‹×~í׿ª«®ºê8dÛ\uÕUWý%‰çÇ6W]õ‚|ög6Ÿó9ŸÃsû¬Ïú,>û³?›«®zA$ñüØæª«^×~í׿w~çwxn¿õ[¿Åk¿öksÕUÏÏoÿöoó:¯ó:<·×z­×â·û·¹êªäµ_ûµùßùžÛoýÖoñÚ¯ýÚ\uÕóóÛ¿ýÛ¼Îë¼Ïíµ^ëµøíßþm®ºêyí×~m~çw~‡çö[¿õ[¼ök¿6W]uÕUÿÃ!Ûæª«®ºê(Ië³>‹ÏþìÏæª«^Içs>‡çöYŸõY|ög6W]õ‚Hâù±ÍUW½ ¯ýÚ¯ÍïüÎïðÜ~ë·~‹×~í׿ª«žŸßþíßæu^çuxn¯õZ¯ÅoÿöosÕU/Èk¿ökó;¿ó;<·ßú­ßâµ_ûµ¹êªçç·û·y×yžÛk½ÖkñÛ¿ýÛ\uÕ òÚ¯ýÚüÎïüÏí·~ë·xí×~m®ºêª«þ‡C¶ÍUW]uÕÿP’x~lsÕU/Ègögó9Ÿó9<·Ïú¬Ïâ³?û³¹êªDÏm®ºêyí×~m~çw~‡çö[¿õ[¼ök¿6W]õüüöoÿ6¯ó:¯Ãs{­×z-~û·›«®zA^ûµ_›ßùßá¹ýÖoý¯ýÚ¯ÍUW=?¿ýÛ¿Íë¼ÎëðÜ^ëµ^‹ßþíßæª«^×~í׿w~çwxn¿õ[¿Åk¿öksÕUW]õ?²m®ºêª«þ‡’Äóc›«®zA>û³?›ÏùœÏá¹}Ög}ŸýÙŸÍUW½ ’x~lsÕU/Èk¿ökó;¿ó;<·ßú­ßâµ_ûµ¹êªçç·û·y×yžÛk½ÖkñÛ¿ýÛ\uÕ òÚ¯ýÚüÎïüÏí·~ë·xí×~m®ºêùùíßþm^çu^‡çöZ¯õZüöoÿ6W]õ‚¼ök¿6¿ó;¿Ãsû­ßú-^ûµ_›«®ºêªÿámsÕUW]õ?ÔñãǹtétìØ1vww¹êªä³?û³ùœÏùžÛOýÔOñÖoýÖ\uÕ "‰çöZ¯õZüöoÿ6W]õ‚|ôG4_ó5_Ãsû­ßú-^ûµ_›«®z~n½õVò‡ðÜÞê­ÞŠŸþéŸæª«^·~ë·æg~ægxnOúÓyðƒÌUW=?¿ýÛ¿Íë¼ÎëðÜ>ê£>Нþê¯æª«^×~í׿w~çwxn¶¹êª«®ú_Ù6W]uÕUÿC}ôG4_ó5_Ã}Ög}ŸýÙŸÍUW½ ·Þz+/ýÒ/Í¥K—¸ß±cǸõÖ[9~ü8W]õ‚¼õ[¿5?ó3?Ã}ÕW}ýÑÍUW½ ý×Í˼ÌËð@/õR/Å_ÿõ_sÕU/Ìk¿ökó;¿ó;<ÐOýÔOñÖoýÖ\uÕ òÓ?ýÓ¼ÍÛ¼ ôZ¯õZüöoÿ6W]õ¼ôK¿4ó7ÃýÖoý¯ýÚ¯ÍUW½ _ýÕ_ÍÇ|ÌÇð@oõVoÅOÿôOsÕUW]õ¿²m®ºêª«þûèþh¾û»¿€þèæ³?û³¹êªÉoÿöoóÑýÑüÍßü oõVoÅgögóÒ/ýÒ\uÕ ³»»ËgögóÝßýÝ?~œ÷~ï÷æ³?û³¹êªÉOÿôOóÙŸýÙüÍßü oõVoÅWõWóà?˜«®zavwwùèþh~ú§šãÇóÑýÑ|ôG4W]õ/ùê¯þj¾ú«¿šg<ã¼×{½_ýÕ_ÍñãǹêªæÖ[oå£?ú£ù™Ÿù^ê¥^ŠÏþìÏæ­ßú­¹êªÉWõWóÕ_ýÕìîîòÖoýÖ|õW5Ç窫®ºêdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUWýð=ßó=|÷w7¿ýÛ¿ ÀñãÇyí×~m>ê£>Š×~í׿ª«^ý×Í˼ÌË`›«®z~n½õV¾æk¾†ßþíßæ¯ÿú¯¹ßk¿ökóÖoýÖ¼×{½Ç窫žÛ÷|Ï÷ðÝßýÝüöoÿ6Ççµ_ûµyë·~kÞë½Þ‹«®zQíîîò‡<„ÝÝ]>ë³>‹ÏþìÏæª«î·»»Ë×|Í×ð¢z¯÷z/üàsÕU÷ûéŸþi~æg~†ßþíßæÖ[oà¥_ú¥yë·~k>ê£>ŠãÇsÕUÏíÖ[oå{¾ç{ø·x­×z-^ûµ_›«®ºêªÿamsÕUW]õßhww—·y›·á·û·yAÞú­ßšïú®ïâøñã\uÕ ²»»Ëë¼Îëð×ý×Øæª«žÛ×|Í×ðÑýѼ0Ç绾ë»xë·~k®º `ww—×y×á¯ÿú¯yA^ú¥_šïú®ïâ¥_ú¥¹êªÉÛ¼ÍÛðÓ?ýÓ|Ög}ŸýÙŸÍUWÝï·û·y×y^T¿õ[¿Åk¿öksÕU»»»¼ÍÛ¼ ¿ýÛ¿Í rüøq~ë·~‹—~é—æª«è·û·y×yþ->ë³>‹ÏþìÏæª«®ºêdÛ\uÕUWý7z›·y~ú§€=èA¼÷{¿7/ýÒ/ÍoÿöoóÝßýÝ\ºt €÷~ï÷滾뻸êªäu^çuøíßþmîg›«®z ïþîïæ}Þç}¸ßk½ÖkñÚ¯ýÚ<øÁæÖ[oå§ú§ù›¿ùî÷S?õS¼õ[¿5Wýÿ¶»»Ëë¼Îëð×ý×<èAâ½ßû½yé—~iþú¯ÿšŸþéŸæoþæoxðƒÌ_ýÕ_qüøq®ºêùîïþnÞç}Þ‡û}Ög}ŸýÙŸÍUWÝï³?û³ùœÏù^T¿õ[¿Åk¿öksÕÿo»»»¼Îë¼ý× Àƒô Þû½ß›—~é—æ¯ÿú¯ùéŸþiþæoþ€ãÇóWõW<øÁ檫î÷Û¿ýÛ¼Îë¼ÿ_õU_ÅGôGsÕUW]õ? ²m®ºêª«þ›|÷w7ïó>ïÀk½ÖkñÓ?ýÓ?~œûíîîòÚ¯ýÚüÍßü ¿õ[¿Åk¿öksÕU´»»Ëë¼Îëð×ý×û³?›ÏùœÏàÁ~0õWÅñãǹêÿ¯ÏþìÏæs>çsx­×z-~ú§šãÇó@ïýÞïÍ÷|Ï÷ð^ïõ^|÷w7W]õüÜzë­¼Ì˼ »»»Üï³>ë³øìÏþl®ºê~ïýÞïÍ÷|Ï÷ðô§??øÁ\uÕ¿ä³?û³ùœÏù^ê¥^ŠßþíßæøñãÜoww—þèæ{¾ç{x¯÷z/¾û»¿›«®ú·øîïþnÞç}Þ€—z©—â·û·9~ü8W]uÕUÿÃ Ûæª«®ºê¿ÉCòn½õVŽ;Æ­·ÞÊñãÇyn·Þz+yÈCxé—~iþê¯þŠ«®ºßOÿôOó>ïó>ìîîòÜlsÕU÷ûê¯þj>æc>€ú¨â«¿ú«yA^ûµ_›ßùß໾ë»xï÷~o®úÿëĉìîîrìØ1n½õVŽ?ÎsÛÝÝåÁ~0—.]âøñã\¼x‘«®z~^æe^†¿þë¿æØ±c\ºt €Ïú¬Ïâ³?û³¹êªû½Ì˼ ý×ͱcÇØÝÝ媫þ%·Þz+yÈCxЃÄ_ÿõ_süøqžÛîî.~ðƒ¹t鶹ꪭ¿þë¿æu^çuØÝÝ娱cüõ_ÿ5~ðƒ¹êª«®úÙ6W]uÕUÿ ~ú§š·y›·à½Þë½øîïþn^·~ë·æg~ægxúӟ΃ü`®úÿí¯ÿú¯ù˜ù~û·›û½×{½ý×ÍßüÍß`›«®ºßk¿ökó;¿ó;üÕ_ý/ýÒ/Í òÕ_ýÕ|ÌÇ| ŸõYŸÅgögsÕÿO¿ýÛ¿Íë¼ÎëðVoõVüôOÿ4/Èk¿ökó;¿ó;üÕ_ý/ýÒ/ÍUW=Ðgögó9Ÿó9|ÕW}ó1Àg}ÖgñÙŸýÙ\uÕý$ðZ¯õZüöoÿ6W]õ/ùê¯þj>æc>€ïú®ïâ½ßû½yAÞû½ß›[o½€¯þê¯æ¥_ú¥¹êªÕîî.¯ó:¯Ã_ÿõ_ð[¿õ[¼ök¿6W]uÕUÿC!Ûæª«®ºê¿Ágögó9Ÿó9|×w}ïýÞïÍ òÕ_ýÕ|ÌÇ| _õU_ÅGôGsÕÿoŸýÙŸÍç|ÎçpìØ1>û³?›þèæµ_ûµùßùlsÕU÷{ë·~kvww¹õÖ[¹õÖ[ya~û·›×y×à£>ê£øê¯þj®úÿkww—¿þë¿àµ_ûµyA^ûµ_›ßùßàéO:~ðƒ¹êªûýõ_ÿ5/ó2/Àg}ÖgñÚ¯ýÚ¼Î뼟õYŸÅgögsÕU¿ýÛ¿Íë¼ÎëðYŸõY|ög6W]õ/y™—yþú¯ÿšcÇŽ±»»ËUWýgùìÏþl>çs>€·z«·â§ú§¹êª«®ú Ù6W]uÕUÿ ^ûµ_›ßùßà¯þê¯xé—~i^ßþíßæu^çuø¨ú(¾ú«¿š«þûìÏþl>çs>‡ú¨â³?û³9~ü8¯ýÚ¯ÍïüÎï`›«®ú·øìÏþl>çs>€Ïú¬Ïâ³?û³¹êªfww—'NpìØ1vww¹êªûíîîò2/ó2Üzë­¼ÔK½ý×Íoÿöoó:¯ó:|Ög}ŸýÙŸÍUW|÷w7ïó>ïÀOýÔOñÖoýÖÜïw~çwx­×z-®ºê¹Ià­Þê­øéŸþi®ºê?í·ÞÊCòŽ;Æ­·ÞÊñãǹꪫ®ú Ù6W]uÕUÿ ^æe^†¿þë¿À6/Ì­·ÞÊCò^ëµ^‹ßþíßæªÿßþú¯ÿš?øÁ?~œzí×~m~çw~Û\uÕ¿ÅCòn½õV~ë·~‹×~í׿ª«^ÝÝ]^çu^‡¿þë¿à³>ë³øìÏþl®ºê~ýÑÍ×|Í×pìØ1þú¯ÿš?øÁüöoÿ6¯ó:¯Àg}ÖgñÙŸýÙ\uÀGôGó5_ó5üÕ_ý?ó3?ÃWõW³»»Ëý^ú¥_š÷~ï÷æ£>꣸꪿þë¿æe^æeø¬Ïú,>û³?€ïùžïá§ú§ÙÝÝàÁ~0¯ýÚ¯Í{½×{qÕUÿ¯ó:¯ÃoÿöoðU_õU|ôG4W]uÕUÿÃ!Ûæª«®ºê¿$îg›‰$^ëµ^‹ßþíßæª«žŸ×~í׿w~çw°ÍUWýk}ög6Ÿó9ŸÀK½ÔKñ×ý×\uÕsûë¿þk~çw~‡¿þë¿æ§ú§ÙÝÝà½Þë½øîïþn®ºê~?ýÓ?ÍÛ¼ÍÛðU_õU|ôG4¿ýÛ¿Íë¼ÎëðYŸõY|ög6W]ðÚ¯ýÚüÎïüÇgww—ä¥_ú¥ù­ßú-Ž?ÎUÿýöoÿ6¯ó:¯Àg}ÖgñÖoýÖ¼Ïû¼ý×ÍóóÚ¯ýÚüÔOýÇ窫^T¿ýÛ¿Íë¼Îëð =ˆ[o½•«®ºêªÿmsÕUW]õß@÷³Í¿D/ýÒ/Í_ýÕ_qÕUÏÏk¿ökó;¿ó;Øæª«þ5~û·›×y×á~¿õ[¿Åk¿öksÕUÏíĉìîîò@õQÅWõWsÕU÷ÛÝÝå!y»»»¼Õ[½?ýÓ?Íý~û·›×y×à³>ë³øìÏþl®º @ôZ¯õZ¼õ[¿5/ýÒ/Í­·ÞÊoÿöoó=ßó=Üï¥_ú¥ù­ßú-Ž?ÎUÿ?ýöoÿ6¯ó:¯ÀG}ÔGñ=ßó=ìîîðZ¯õZìîîò7ó7Üï¥_ú¥ù­ßú-Ž?ÎUW½(^çu^‡ßþíß໾ë»xï÷~o®ºêª«þ@¶ÍUW]uÕI?~œ‹/ò/‘ÄýlsÕUÏÏk¿ökó;¿ó;Øæª«^Tý×Íë¼Îë°»» Àg}ÖgñÙŸýÙ\uÕó#‰=èA<ãÏàüàóS?õS¼ôK¿4W]õ:¯ó:üöoÿ6ÇŽãÖ[oåøñãÜï·û·y×y>ë³>‹ÏþìÏæª«þú¯ÿš—y™—á~?õS?Å[¿õ[óÜþú¯ÿš×~í×æÒ¥K|Ög}ŸýÙŸÍUÿ?}ög6Ÿó9ŸÀñãÇÙÝÝåµ^ëµøéŸþiŽ?Îýþú¯ÿš÷~ï÷æoþæox¯÷z/¾û»¿›«®ú—üõ_ÿ5/ó2/Àƒô n½õV®ºêª«þ—@¶ÍUW]uÕIÜÏ6ÿIÜÏ6W]õü¼ök¿6¿ó;¿€m®ºêEñÝßýÝ|ÌÇ| »»»¼×{½ßýÝßÍUW½(n½õVÞû½ß›ßùßàøñãüÖoý/ýÒ/ÍUÿ}õW5ó1ÀOýÔOñÖoýÖ<Ðoÿöoó:¯ó:|Ög}ŸýÙŸÍUWíîîò×ý×üõ_ÿ5~ðƒyë·~k^ïþîïæ}Þç}8~ü8/^äªÿŸ>û³?›ÏùœÏá~oõVoÅOÿôOóüÜzë­¼ôK¿4—.]àéO:~ðƒ¹êªæ½ßû½ùžïù¾ë»¾‹÷~ï÷檫®ºê dÛ\uÕUWý7Äýló/‘ÀK½ÔKñ×ý×\uÕóóÚ¯ýÚüÎïü¶¹êªÉw÷wó>ïó>Üï½Þë½øîïþn®ºê_ë­ßú­ù™Ÿù^ûµ_›ßú­ßâªÿŸþú¯ÿš×y×aww—÷z¯÷⻿û»yn¿ýÛ¿Íë¼ÎëðYŸõY|ög6W]õ¯õà?˜g<ãüÖoý¯ýÚ¯ÍUÿÿ|õW5ó1Ãýžþô§óà?˜ä£?ú£ùš¯ù¾ê«¾Šþèæª«^ÝÝ]Nœ8Áý.^¼Èñãǹꪫ®ú_Ù6W]uÕUÿ Ž?Î¥K—°Í¿D¯õZ¯ÅoÿöosÕUÏÏk¿ökó;¿ó;Øæª«^˜÷yŸ÷ỿû»¹ßW}ÕWñÑýÑ\uÕ¿Åîî.'Nœà~/^äøñã\õÿÏ˼ÌËð×ý×<èAâ¯ÿú¯9~ü8Ïí·û·y×y>ë³>‹ÏþìÏæª«þµÞú­ßšŸù™Ÿà§~ê§xë·~k®úÿç·û·y×yô që­·òÂüôOÿ4oó6oÀG}ÔGñÕ_ýÕ\uÕ òÝßýݼÏû¼oõVoÅOÿôOsÕUW]õ¿²m®ºêª«þ¼ök¿6¿ó;¿ÀÅ‹9~ü8/Èoÿöoó:¯ó:¼Õ[½?ýÓ?ÍUW=?¯ýÚ¯ÍïüÎï`›«®z~vwwù˜ù¾û»¿›û}×w}ïýÞïÍUWý{¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ\õÿËoÿöoó:¯ó:ü[½Ök½¿ýÛ¿ÍUW½(>û³?›ÏùœÏà«¾ê«øèþh®úÿç·û·y×y^ëµ^‹ßþíßæ…ùíßþm^çu^€×z­×â·û·¹êªä­ßú­ù™Ÿù¾ë»¾‹÷~ï÷檫®ºêdÛ\uÕUWý7øèþh¾æk¾€ßú­ßâµ_ûµyA~ú§š·y›·à³>ë³øìÏþl®ºêùyí×~m~çw~Û\uÕsÛÝÝåu^çuøë¿þkŽ;ÆoÿöoóÒ/ýÒ\uÕóó;¿ó;ìîîòR/õR<øÁæ…yí×~m~çw~€ßú­ßâµ_ûµ¹êÿ—ßþíßæu^çuø·z­×z-~û·›«þÿÚÝÝåoþæox©—z)Ž?Î óÖoýÖüÌÏü ¿õ[¿Åk¿öksÕÿO’8~ü8/^ä…ùíßþm^çu^€Ïú¬Ïâ³?û³¹êªäĉìîîpñâEŽ?ÎUW]uÕÿ"ȶ¹êª«®úoðÝßýݼÏû¼ŸõYŸÅgögó‚|ôG4_ó5_ÀOýÔOñÖoýÖ\uÕóóÚ¯ýÚüÎïü¶¹êªÚÝÝåu^çuøë¿þk^ê¥^Šïþîïæ¥_ú¥¹êªçç£?ú£ùš¯ù¾ë»¾‹÷~ï÷æ…y™—yþú¯ÿ€ßú­ßâµ_ûµ¹êÿ—[o½•ïþîïæ_rë­·ò=ßó=¼Ök½¯ýÚ¯ Àƒü`Þû½ß›«þzë·~k~æg~€ïú®ïâ½ßû½yaò‡pë­·ðô§??øÁ\õÿÓƒü`žñŒgðô§??øÁ¼ _ýÕ_ÍÇ|ÌÇðU_õU|ôG4W]õüüõ_ÿ5/ó2/Àk½ÖkñÛ¿ýÛ\uÕUWý/ƒl›«®ºêªÿ»»»œ8q€?øÁ<ýéOçùÙÝÝå!y»»»;vŒÝÝ]®ºêyí×~m~çw~Û\uÕ½ÍÛ¼ ?ýÓ? ÀK½ÔKñÛ¿ýÛ?~œ«®zA¾û»¿›÷yŸ÷àµ_ûµù­ßú-^[o½•‡<ä!;vŒÝÝ]®ºêùíßþm^çu^€Ïú¬Ïâ³?û³¹êª¯þê¯æc>æcxí×~m~ë·~‹ä·û·y×y^ëµ^‹ßþíßæªÿ¿¾ú«¿šù˜à³>ë³øìÏþl^×y×á·û·xúӟ΃ü`®ºêùùê¯þj>æc>€Ïú¬Ïâ³?û³¹êª«®ú_Ù6W]uÕUÿMÞû½ß›ïùžïà«¾ê«øèþhžÛGôGó5_ó5|Ög}ŸýÙŸÍUW½ ¯ýÚ¯ÍïüÎï`›«®ºßOÿôOó6oó6;vŒ[o½•ãÇsÕUÿ’ãÇséÒ%¾ë»¾‹÷~ï÷æ¹íîîò:¯ó:üõ_ÿ5ŸõYŸÅgögsÕU/Èoÿöoó:¯ó:|Ög}ŸýÙŸÍUWÝzë­<ä!á~?õS?Å[¿õ[óÜvwwy™—yn½õV~ê§~Š·~ë·æªÿ¿n½õV^ú¥_šK—.ðWõW¼ôK¿4Ïí»¿û»yŸ÷y^ëµ^‹ßþíßæª«^÷~ï÷æ{¾ç{ø­ßú-^ûµ_›«®ºêªÿemsÕUW]õßäÖ[oå¥_ú¥¹téŸýÙŸÍG}ÔGqüøqn½õV>çs>‡ïþîïàAzý×Íñãǹêªäµ_ûµùßùlsÕU÷{ÈC­·Þ Àƒü`üàó¢x©—z)¾ú«¿š«þÿúéŸþiÞæmÞ†û}ög6ïõ^ïŃü`~æg~†þèæÖ[oà¥^ê¥øíßþmŽ?ÎUW½ ¿ýÛ¿Íë¼ÎëðYŸõY|ög6W]ðÙŸýÙ|Îç|÷ûìÏþlÞë½Þ‹?øÁ|Ï÷|ýÑÍîî.ïõ^ïÅw÷wsÕU_ýÕ_ÍÇ|ÌÇpüøq>û³?›÷z¯÷âøñãìîîò5_ó5|ög6ÇŽã·û·yé—~i®ºêyí×~m~çw~€¿ú«¿â¥_ú¥¹êª«®ú_Ù6W]uÕUÿ¾û»¿›÷yŸ÷á…9vì¿ýÛ¿ÍK¿ôKsÕU/Ìk¿ökó;¿ó;Øæª«~ú§š·y›·áßâµ^ëµøíßþm®úÿí»¿û»yŸ÷yþ%/õR/Åoÿöosüøq®ºê…ùíßþm^çu^€Ïú¬Ïâ³?û³¹êªû½÷{¿7ßó=ßÿä½Þë½øîïþn®ºê~ŸýÙŸÍç|ÎçðÂ;vŒ¯þê¯æ½ßû½¹êªæÄ‰ìîî`›«®ºêªÿ…msÕUW]õßì¯ÿú¯ùèþh~çw~‡çöZ¯õZ|÷w7~ðƒ¹êªÉk¿ökó;¿ó;Øæª«>û³?›ÏùœÏáßâµ^ëµøíßþm®ºê·û·ùìÏþl~çw~‡çvìØ1>ú£?šÏþìÏæª«^¿ýÛ¿Íë¼ÎëðYŸõY|ög6W]õ@?ýÓ?ÍGôGóŒg<ƒçö =ˆÏþìÏæ½ßû½¹êªçöÛ¿ýÛ|ôG4ó7Ãs{«·z+>û³?›—~é—æª«þ%’8vì»»»\uÕUWý/„l›«®ºêªÿ!n½õVn½õVn½õVüàóà?˜?øÁ\uÕUW]uÕÿ·Þz+·Þz+ý×ÍK¿ôKsüøq^ú¥_š«®ºêªÿ ·Þz+·Þz+»»»?~œãÇóÒ/ýÒ\uÕ¿äÖ[oåÖ[oåÖ[oåÁ~0~ðƒyðƒÌUW]uÕUWý?‚l›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®z–ïþîïæÏxïõ^ïŃü`þ+}õW5—.]à£>ê£8~ü8W]uÕUW]uÕUW]uÕ¿ ²m®ºêª«®ºêª«®ºêªçð:¯ó:üWùª¯ú*^ú¥_š¿þë¿æc>æc¸ßK½ÔKñÕ_ýÕ\õ_ë§ú§y›·y^ê¥^Š¿þë¿æ¿ÚWõWó1ó1¼õ[¿5?õS?ÅUW]uÕUW]uÕUW]õ¯‚l›«®ºêª«®ºêª«®ºê9Hâ¿ÊoýÖoñÚ¯ýÚüöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæªÿ:»»»<ä!aww€ßú­ßâµ_ûµùïðà?˜g<ã|ÕW}ýÑÍUW]uÕUW]uÕUW]õ"C¶ÍUW]uÕUW]uÕUW]õ$ñ_å·~ë·xí×~m~û·›×y×á~¯õZ¯ÅoÿöosÕ×y×á·û·x­×z-~û·›ÿ.?ýÓ?ÍÛ¼ÍÛpüøqþê¯þŠ?øÁ\uÕUW]uÕUW]uÕU/dÛ\uÕUW]uÕUW]uÕUÏAÿU~ë·~‹×~í׿·û·y×yî÷Z¯õZüöoÿ6Wý×øéŸþiÞæmÞ†û=ýéOçÁ~0ÿ^ûµ_›ßùßà­ßú­ù©Ÿú)®ºêª«®ºêª«®ºêª ²m®ºêª«®ºêª«®ºêªçðÛ¿ýÛ¼¨>ú£?š¿ù›¿á~ë·~‹ÕK¿ôKsüøq~û·›×y×á~¯õZ¯ÅoÿöosÕ¾ÝÝ]^æe^†[o½€÷z¯÷⻿û»ùïöÛ¿ýÛ¼Îë¼÷û­ßú-^ûµ_›«®ºêª«®ºêª«®ºê_„l›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6WýïñÙŸýÙ|Îç|÷{úӟ΃ü`þ'xí×~m~çw~€?øÁ<ýéO窫®ºêª«®ºêª«®ú!Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²ÍUÿ;ìîîò‡<„ÝÝ]Þë½Þ‹ïþîïæŠßþíßæu^çu¸ßw}×wñÞïýÞ\uÕUW]uÕUW]uÕU/²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Û\õ¿Ãgögó9Ÿó9Üï·~ë·xí×~mþ'yðƒÌ3žñ üàóô§?«®ºêª«®ºêª«®ºê…B¶ÍUW]uÕUW]uÕUW]õoöÚ¯ýÚüÎïüd›«þçÛÝÝå!y»»»<èAâÖ[o嚯þê¯æc>æc¸ßw}×wñÞïýÞ\uÕUW]uÕUW]uÕU/²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx ÛükÝzë­|Ï÷|÷{ЃÄ{¿÷{óü|÷w7ÏxÆ3¸ßg}Ögq¿ÝÝ]~æg~†ïþîïà·û·yé—~iŽ?Îk¿ökó^ïõ^<øÁæùÙÝÝå{¾ç{øéŸþivwwùë¿þküàóÒ/ýÒ¼ök¿6ïõ^ïÅñãÇù·ú™Ÿù~û·›¿þë¿æÖ[oåÖ[oå¥_ú¥9~ü8/ýÒ/Í[¿õ[óZ¯õZüWùîïþnÞç}Þ‡û}ÕW}ýÑÍ¿Ö÷|Ï÷ðÛ¿ýÛÜzë­üõ_ÿ5»»»<øÁæÁ~0Çç­ßú­y­×z-üàóoqë­·ò‡<„û½ôK¿4õWÅUW]uÕUW]uÕUW]õ!Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸Ÿm¾û»¿›ù˜aww—滾ë»xï÷~oès>çsøìÏþl^˜ãÇó]ßõ]¼õ[¿5ÿßó=ßÃgögsë­·ò/yðƒÌw}×wñÚ¯ýÚüg{™—yþú¯ÿšû=ýéOçÁ~0/ª¯ùš¯á³?û³ÙÝÝåEñÞïýÞ|Ög}~ðƒù×zé—~iþæoþ†ûýÕ_ý/ýÒ/ÍUW]uÕUW]uÕUW]õ|!Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ŸmÞç}Þ‡ïþîïæEõQõQ|õW5¯ó:¯Ãoÿöoó¢ú©Ÿú)Þú­ßšÉîî.ïó>ïÃOÿôOó¯õÞïýÞ|×w}ÿYþú¯ÿš—y™—á~zЃ¸õÖ[yQ½Ïû¼ßýÝßÍ¿ÖñãÇù­ßú-^ú¥_šÏþìÏæs>çs¸ß{½×{ñÝßýÝ\uÕUW]uÕUW]uÕUϲm®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}ÕW}ó1Ã=èAâÁ~0¿ó;¿Ãóóô§?ÏùœÏỿû»y ×z­×`ww—¿ù›¿á¹?~œ§?ýé?~œdww—×y×á¯ÿú¯yn/õR/Åk¿öksüøqn½õVþú¯ÿš¿ù›¿á¹½÷{¿7ßõ]ßņþèæk¾æk¸ß{½×{ñÝßýݼ(>û³?›ÏùœÏá¹½Ök½¯ýÚ¯ Àîî.ý×ÍïüÎïðÜŽ?ÎÓŸþtŽ?΋ê¯ÿú¯y™—yî÷à?˜§?ýé\uÕUW]uÕUW]uÕUϲm®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡çç½Þë½øìÏþlüàs¿ÝÝ]>ú£?šïùžïáüàsë­·pìØ1¾ú«¿š÷~ï÷æþú¯ÿš÷~ï÷æoþæox ïú®ïâ½ßû½yAÞç}Þ‡ïþîïæ^ëµ^‹¯þê¯æ¥_ú¥yný×ÍGôGó;¿ó;<ÐW}ÕWñÑýÑüG{™—yþú¯ÿšûýÔOýoýÖoÍ¿äÖ[oå!yô^ïõ^|õW5Çç¹Ýzë­|ôG4?ó3?Ã}ÔG}_ýÕ_Í¿†$è¯þê¯xé—~i®ºêª«®ºêª«®ºêªçl›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6ÿZ¿ýÛ¿Íë¼Îëp¿×z­×â·û·y~^ûµ_›ßùßá¹}×w}ïýÞïÍ òÚ¯ýÚüÎïüÏíØ±cüõ_ÿ5~ðƒy~vwwyðƒÌ¥K—¸ß[½Õ[ñÓ?ýÓæc¸ßk½ÖkñÛ¿ýÛüKÞû½ß›ïùžïá~ÇçâÅ‹ük¼ök¿6¿ó;¿Ãý>ë³>‹ÏþìÏæª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùè½Þë½øîïþn^˜ŸþéŸæmÞæmxn¿õ[¿Åk¿ökóÂ|ôG4_ó5_Ãýüàóô§?ççe^æeøë¿þkî÷Z¯õZüöoÿ6/ŠÝÝ]^ú¥_šg<ãÜï³>ë³øìÏþlþ£üöoÿ6¯ó:¯ÃÙæEñÞïýÞ|Ï÷|÷û¬Ïú,>û³?›É­·ÞÊCòè¯þê¯xé—~i^TïýÞïÍ÷|Ï÷p¿·z«·â§ú§¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡zúӟ΃ü`^˜ÝÝ]Nœ8Á½ÔK½ý×Í¿ä§ú§y›·yÈ6Ïí·û·y×yè·~ë·xí×~m^TßýÝßÍû¼Ïûp¿ãÇsñâEþ£|ög6Ÿó9ŸÃý^ê¥^Š¿þë¿æEñÚ¯ýÚüÎïü÷û¨ú(¾ú«¿šÅgögsüøq^ú¥_šãÇóÒ/ýÒük|õW5ó1Ãýüàóô§?«®ºêª«®ºêª«®ºêy Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßK½ÔKñ×ý×¼($ñ@õQÅWõWó/ùíßþm^çu^‡²ÍsûìÏþl>çs>‡û=èAâÖ[oå_cww—'Nð@¿õ[¿Åk¿ökóá½ßû½ùžïùî÷^ïõ^|÷w7/ŠÏþìÏæs>çs¸ßñãÇù©Ÿú)^ûµ_›ÿl¿ýÛ¿Íë¼Îëð@¶¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û½Ök½¿ýÛ¿Í‹Bô]ßõ]¼÷{¿7ÿ’ßþíßæu^çux Û<·×~í׿w~çw¸ßG}ÔGñÕ_ýÕük½ôK¿4ó7Ãý¾ê«¾Šþèæ?Âk¿ökó;¿ó;Üï³>ë³øìÏþl^¿ýÛ¿Íë¼ÎëðÜÞú­ßš·~ë·æ­Þê­8~ü8ÿ~û·›×y×áþê¯þŠ—~é—æª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóÜ$ñ@ïýÞïÍ{¿÷{ó¯õÑýÑüõ_ÿ5÷{¯÷z/¾û»¿›ÿ/ó2/Ã_ÿõ_s¿Ïú¬Ïâ³?û³yQ½ök¿6¿ó;¿Ã òÒ/ýÒ¼õ[¿5¯ýÚ¯Ík½ÖkñåÖ[oå!yô[¿õ[¼ök¿6W]uÕUW]uÕUW]uÕs@¶ÍUW]uÕUW]uÕUW]õoöÚ¯ýÚüÎïüd›­ßþíßæu^çu¸ßk½ÖkñÛ¿ýÛû³?›ÝÝ]Þû½ß›Ÿù™ŸáEuüøqÞú­ßš¯úª¯âøñãü[Hâ~ë·~‹×~í׿ª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×áló@ý×Í˼ÌËðŸáµ^ëµøíßþmþ#Hâ¾ê«¾ŠþèæßâÖ[oå«¿ú«ùéŸþižñŒgð¢8~ü8¿õ[¿ÅK¿ôKó¯%‰ú­ßú-^ûµ_›«®ºêª«®ºêª«®ºê9 Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼($ñ@¿õ[¿Åk¿ökó/ùíßþm^çu^‡²Ís“Ä}ÕW}/ýÒ/Í¿×ñãÇyé—~iþ#Hâ>ë³>‹ÏþìÏæßë¯ÿú¯ùíßþm~ú§šßùßá…9~ü8¿õ[¿ÅK¿ôKó¯!‰ú­ßú-^ûµ_›«®ºêª«®ºêª«®ºê9 Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼($ñ@¿õ[¿Åk¿ökó/ùíßþm^çu^‡²Ís“Ä}×w}ïýÞïÍÿ$¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³ùöÛ¿ýÛüôOÿ4?ýÓ?Í3žñ žÛ{½×{ñÝßýݼ¨~û·›×y×á~ë·~‹×~í׿ª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóÜüàóŒg<ƒû}ÔG}_ýÕ_Íÿ$¯ýÚ¯ÍïüÎïp¿÷z¯÷⻿û»ùÏôÛ¿ýÛ¼õ[¿5—.]âló¢úíßþm^çu^‡ú«¿ú+^ú¥_š«®ºêª«®ºêª«®ºê9 Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼($ñ@¿õ[¿Åk¿ökó/ùíßþm^çu^‡²Ís{ï÷~o¾ç{¾‡û½ôK¿4õWÅ¿Öw÷w#‰?øÁ<èAâÁ~0ÿQÞû½ß›ïùžïá~¯õZ¯Åoÿöoó¢ØÝÝåoþæo¸õÖ[±Í{¿÷{ó¢úîïþnÞç}Þ‡ú«¿ú+^ú¥_šÅoÿöoó:¯ó:æc>†û=èAâÖ[o媫®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóÜvwwyðƒÌ¥K—¸ßK¿ôKóWõW¼¨ÞæmÞ†ŸþéŸæžþô§óà?˜ÿ?ýÓ?ÍÛ¼ÍÛð@¶ù—ìîîrâÄ 軾ë»xï÷~o^¿ýÛ¿Íë¼Îëð@¶yQ½÷{¿7ßó=ßÃýÞê­ÞŠŸþéŸæª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóü|ög6Ÿó9ŸÃ½÷{¿7ßõ]ßſ仿û»yŸ÷yè½Þë½øîïþnþ£ìîîrâÄ è·~ë·xí×~mþ%oýÖoÍÏüÌÏp¿ãÇóô§?ãÇó/y×y~û·›û=èAâÖ[oåEõ2/ó2üõ_ÿ5÷ûª¯ú*>ú£?š«®ºêª«®ºêª«®ºêy Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼($ñ@¿õ[¿Åk¿ökó/ùíßþm^çu^‡²Íó³»»Ëk¿ökó7ó7<Ðk¿ökó]ßõ]<øÁæ¹íîîò5_ó5|ög6tìØ1þú¯ÿš?øÁüGzé—~iþæoþ†û}ÕW}ýÑÍ¿ä·û·y×yèÁ~0ßõ]ßÅk¿ökóüìîîò1ó1|÷w7ô]ßõ]¼÷{¿7/ŠÝÝ]Nœ8ÁýÕ_ý/ýÒ/ÍUW]uÕUW]uÕUW]õë³øìÏþl^’x ßú­ßâµ_ûµù—üöoÿ6¯ó:¯ÃÙæùë¿þk^ûµ_›K—.ñÜ^ú¥_š·~ë·æ~ý×Íoÿöo³»»Ësû®ïú.Þû½ß›ÿhýÑÍ×|Í×p¿·z«·â§ú§yQ|ôG4_ó5_Ãs{é—~i^ûµ_›ãÇs¿¿þë¿æ·û·ÙÝÝåÞë½Þ‹ïþîïæEõÓ?ýÓ¼ÍÛ¼ ÷;vì»»»\uÕUW]uÕUW]uÕUϲm®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍ‹Bô[¿õ[¼ök¿6ÿ’ßþíßæu^çux Û¼0ý×Í{¿÷{ó7ó7ü[|×w}ïýÞï͆¿þë¿æe^æe¸ßñãǹxñ"/ª÷~ï÷æ{¾ç{ø·x©—z)~û·›ãÇó¢úìÏþl>çs>‡û}ÔG}_ýÕ_ÍUW]uÕUW]uÕUW]õ|!Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼($ñ@¿õ[¿Åk¿ökó/ùíßþm^çu^‡²Í¿dww—¯þê¯æ«¿ú«¹té/Š×z­×â«¿ú«yé—~iþ3=øÁæÏx÷û«¿ú+^ú¥_šÕgögóÕ_ýÕ\ºt‰űcÇøìÏþl>ú£?š­—y™—á¯ÿú¯¹ß_ýÕ_ñÒ/ýÒ\uÕUW]uÕUW]uÕUϲm®ºêª«®ºêª«®ºêª³ïþîïæÖ[oå>û³?›­[o½•ïþîïæ~~ðƒyï÷~ožŸïþîïæÖ[oå~¯ýÚ¯Ík¿ökó¢øìÏþlè½ßû½yðƒÌ¿äÖ[o廿û»y ÏþìÏæEµ»»ËOÿôOóÓ?ýÓüõ_ÿ5ÏxÆ3x ×z­×â¥_ú¥yï÷~o^ú¥_šÿ _ýÕ_ÍÇ|ÌÇp¿ú¨â«¿ú«ùרÝÝå§ú§ùéŸþin½õVþæoþ†z©—z)^ú¥_š×~í׿­ßú­9~ü8ÿZý×Í˼ÌËp¿=èAÜzë­\uÕUW]uÕUW]uÕU/²m®ºêª«®ºêª«®ºêª«þÚÝÝåÁ~0—.]àøñã\¼x‘ÿi>ú£?š¯ùš¯á~ßõ]ßÅ{¿÷{sÕUW]uÕUW]uÕUW½@ȶ¹êª«®ºêª«®ºêª«®úê½ßû½ùžïùî÷]ßõ]¼÷{¿7ÿ“œ8q‚ÝÝ]Ž;Æîî.W]uÕUW]uÕUW]uÕ …l›«®ºêª«®ºêª«®ºêªÿ§n½õVò‡p¿×~í׿·~ë·øŸâ»¿û»yŸ÷yî÷YŸõY|ög6W]uÕUW]uÕUW]uÕ …l›«®ºêª«®ºêª«®ºêªÿÇ>ú£?š¯ùš¯á~OúÓyðƒÌÿ¯ó:¯Ãoÿöoð =ˆ¿þë¿æøñã\uÕUW]uÕUW]uÕU/²m®ºêª«®ºêª«®ºêª«þÛÝÝåÁ~0—.]à½Þë½øîïþnþ»ýöoÿ6¯ó:¯Ãý¾ë»¾‹÷~ï÷檫®ºêª«®ºêª«®ú!Ûæª«®ºêª«®ºêª«®ºêÿ¹¯þê¯æc>æc¸ßÓŸþtüàóßéu^çuøíßþm^ê¥^Š¿þë¿æª«®ºêª«®ºêª«®z‘ Ûæª«®ºêª«®ºêª«®ºê*^ú¥_š¿ù›¿à­ßú­ù©Ÿú)þ»üöoÿ6¯ó:¯Ãý~ë·~‹×~í׿ª«®ºêª«®ºêª«®z‘ Ûæª«®ºêª«®ºêª«®ºê*þú¯ÿš—y™—á~¿õ[¿Åk¿ökóßá!y·Þz+õQÅWõWsÕUW]uÕUW]uÕUW½ÈmsÕUW]uÕUW]uÕUW]uÕe_ýÕ_ÍÇ|ÌÇðÚ¯ýÚüÖoýÿÕ¾û»¿›÷yŸ÷à¥^ê¥øë¿þk®ºêª«®ºêª«®ºêªdÛ\uÕUW]uÕUW]uÕUW]õ,_ýÕ_Íîî.ýÑÍñãÇù¯ôÝßýÝÜzë­¼õ[¿5/ýÒ/ÍUW]uÕUW]uÕUW]õ¯‚l›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þïàÜu@ –ôIEND®B`‚uv-0.9.17+ds1/assets/png/install-warm.png000066400000000000000000005352521520155276700201610ustar00rootroot00000000000000‰PNG  IHDR@è†{2ƒºqIDATxíà$I’$I‹ª™»GDDfffVUUUUwwwww÷ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌtwwwwWWUUUUffFFD„»›™ ÏLfWwuwwOÏÌÌÌÌL¢l›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«þÇøë¿þk.]ºÄýŽ;ÆK¿ôKóoqë­·òŒg<ƒçöZ¯õZü[ìîîò7ó7<ÐK½ÔKqüøq®úïwë­·òŒg<ƒz­×z-®úï÷×ý×\ºt‰û=èAâÁ~0Ïmww—¿ù›¿á^ëµ^‹ÿvwwù›¿ùîwìØ1^ú¥_š«®ºêª«®ºêª«®ú/‚l›«®ºêª«®ºêª«þÇxë·~k~æg~†û?~œ‹/òoñÖoýÖüÌÏü Ïí·~ë·xí×~mþµ>û³?›ÏùœÏá.^¼Èñãǹê¿ßgögó9Ÿó9ê£øê¯þjþµ^ûµ_›ßùßá~¯õZ¯ÅoÿöosÕÿ ŸýÙŸÍç|Îçð@¶ùÿdww—ÏùœÏá«¿ú«ù­ßú-^ûµ_›ÿ ^ûµ_›ßùßá~ŸõYŸÅgögóÜ~û·›×y×áló?Åîî._ó5_Ãgögó[¿õ[¼ök¿6/Èoÿöoó:¯ó:Üïµ^ëµøíßþm®ºêª«®ºêª«®ºê¿²m®ºêª«®ºêª«®úã¯ÿú¯y™—yè«¾ê«øèþhþ5~ú§š·y›·áùyé—~iþê¯þŠ-I<Ðg}ÖgñÙŸýÙ\õ?Ãgögó9Ÿó9ú£?šÝÝ]~ë·~‹×~í׿‚×~í׿w~çw¸ßg}ÖgñÙŸýÙ<·ßþíßæu^çux ÛüOðÓ?ýÓ|ÌÇ| ·Þz+¿õ[¿Åk¿ökó‚üöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæª«®ºêª«®ºêª«þ‹ Ûæª«®ºêª«®ºêªÿQüàóŒg<ƒû½×{½ßýÝßÍ¿ÆGôGó5_ó5ÜïØ±c\ºt‰û]¼x‘ãÇó¢úíßþm^çu^‡ú«¿ú+^ú¥_š«þgøìÏþl>çs>‡²Íÿ’x ßú­ßâµ_ûµùŸàµ_ûµùßùî÷YŸõY|ög6Ïí·û·y×yÈ6ÿHâ~ë·~‹×~í׿ùíßþm^çu^‡û½Ök½¿ýÛ¿ÍUW]uÕUW]uÕUWýA¶ÍUW]uÕUW]uÕUÿ£¼÷{¿7ßó=ßÃýüàóô§?‡<ä!Üzë­Üï­Þê­ø™Ÿùî÷]ßõ]¼÷{¿7/ªÏþìÏæs>çs¸ß±cÇØÝÝåªÿ9>û³?›ÏùœÏálóÿ…$è·~ë·xí×~mþ'xí×~m~çw~‡û}Ög}ŸýÙŸÍsûíßþm^çu^‡²Íÿ’x ßú­ßâµ_ûµyAn½õV¾û»¿›û=øÁæ½ßû½¹êª«®ºêª«®ºêªÿ"ȶ¹êª«®ºêª«®ºê”ïþîïæ}Þç}x §?ýé<øÁæEqë­·ò‡<„û½Ök½oýÖoÍÇ|ÌÇp¿÷z¯÷⻿û»yQ½ök¿6¿ó;¿ÃýÞë½Þ‹ïþîïæªÿ9>û³?›ÏùœÏálóÿ…$è·~ë·xí×~mþ'xí×~m~çw~‡û}Ög}ŸýÙŸÍsûíßþm^çu^‡²Íÿ’x ßú­ßâµ_ûµ¹êª«®ºêª«®ºêªÿ¡msÕUW]uÕUW]uÕÿ(·Þz+yÈCx Ÿú©Ÿâ­ßú­yQ|õW5ó1Ãý>ë³>‹÷~ï÷æ!y÷;~ü8/^äE%‰ú®ïú.Þû½ß›«þçøìÏþl>çs>‡²Íÿ’x ßú­ßâµ_ûµùŸàµ_ûµùßùî÷YŸõY|ög6Ïí·û·y×yÈ6ÿHâ~ë·~‹×~í׿ª«®ºêª«®ºêª«þ‡B¶ÍUW]uÕUW]uÕUÿã¼ôK¿4ó7Ãý>ê£>Нþê¯æEñÖoýÖüÌÏü ÷û«¿ú+^ú¥_š?øÁ<ãÏà~õWÅK¿ôKó/ùíßþm^çu^‡zúӟ΃ü`®úŸã³?û³ùœÏùÈ6ÿ_Hâ~ë·~‹×~í׿‚×~í׿w~çw¸ßg}ÖgñÙŸýÙ<·ßþíßæu^çux ÛüO ‰ú­ßú-^ûµ_›«®ºêª«®ºêª«®ú Ù6W]uÕUW]uÕUWýóÑýÑ|Í×| ÷{é—~iþê¯þŠ…$îwìØ1vwwxï÷~o¾ç{¾‡û}Ög}ŸýÙŸÍ¿ä³?û³ùœÏùî÷R/õRüõ_ÿ5/ª¿þë¿æÒ¥Kìîîò×ý×¼ök¿6/õR/ÅñãÇùò×ý×üÍßü ~ðƒy­×z-þµvwwù›¿ùn½õVn½õV^ûµ_›cÇŽñÒ/ýÒüKn½õVžñŒgð×ý×ìîîòÚ¯ýÚ<èAâÁ~0ÿÙ>û³?›ÏùœÏálóïqë­·òŒg<ƒ[o½•[o½•×~í×æØ±c¼ôK¿4ÿ^·Þz+ÏxÆ3øë¿þkvwwyðƒ̃ü`ô ñà?˜-I<ÐoýÖoñÚ¯ýÚü[Üzë­<ãÏà·û·xé—~iŽ?΃ô üàó¯ñÚ¯ýÚüÎïü÷û¬Ïú,>û³?›çöÛ¿ýÛ¼Îë¼d›­ÝÝ]þæoþ€ßþíßà¥_ú¥9~ü8zЃxðƒÌ¿–$è·~ë·xí×~mþ³Ýzë­<ãÏà¯ÿú¯ÙÝÝå¥_ú¥9~ü8/õR/ÅñãÇùÏð;¿ó;üöoÿ6~ðƒyðƒÌK½ÔKqüøq®ºêª«®ºêª«®ú_Ù6W]uÕUW]uÕUWýóÓ?ýÓ¼ÍÛ¼ d›Éoÿöoó:¯ó:Üï­Þê­øéŸþi~ú§š·y›·á~/ýÒ/Í_ýÕ_ñ/yë·~k~æg~†û}ÔG}_ýÕ_Í óÛ¿ýÛ|Í×| ¿ýÛ¿Íîî./ÌK¿ôKóÑýѼ×{½ÿ’ßþíßæu^çu¸ßk½ÖkñÛ¿ýÛìîîò6oó6üöoÿ6tüøq>ú£?šú¨âøñãüöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßà¯ÿú¯ùš¯ù¾û»¿›ççÁ~0ŸýÙŸÍ{½×{ñÜ~û·›¯ùš¯á§ú§y~^ûµ_›Ïú¬Ïâµ_ûµùÏòÙŸýÙ|Îç|d›ä³?û³ùœÏùî÷[¿õ[¼ök¿6?ýÓ?Í÷|Ï÷ðÓ?ýÓû³?›äÖ[oåk¾ækøíßþmþú¯ÿšæÁ~0¯ýÚ¯Íg}Ögñà?˜Ék¿ökó;¿ó;Üï³>ë³øìÏþlžÛoÿöoó:¯ó:ë³>‹?øÁ¼ ’xQ}Ög}ŸýÙŸÍý~û·›×y×á~¯õZ¯Åoÿöoó¢ØÝÝåk¾ækøîïþnn½õV^×~í׿½ßû½y¯÷z/^T’¸ßk½ÖkñÛ¿ýÛìîîò9Ÿó9|÷w7»»»û³?›Åw}×wñÞïýÞügøìÏþl>çs>‡²Í òÙŸýÙ|Îç|÷û­ßú-^ú¥_šÏùœÏá«¿ú«yQ?~œßú­ßâ¥_ú¥ù—üõ_ÿ5¯ó:¯Ãîî.ÿýÑÍW}ÕWñüHâEõYŸõY|ög6Ïmww—ÏùœÏá«¿ú«ù·ø®ïú.Þû½ß›æµ_ûµùßùî÷YŸõY|ög6Ïí·û·y×yÈ6ÿ’ù˜á«¿ú«ù·ø®ïú.Þû½ß›çG/ªÏú¬Ïâ³?û³¹ßoÿöoó:¯ó:Üïµ^ëµøíßþmþ%ßýÝßÍÇ|Ìǰ»»Ë‹êÁ~0?õS?ÅK¿ôKó/‘Äý^ëµ^‹ßþíßæ§ú§yŸ÷yvwwyQ¼÷{¿7ßõ]ßÅUW]uÕUW]uÕUÿ£!Ûæª«®ºêª«®ºêªÿ‘^ûµ_›ßùßá~ŸõYŸÅgögó¼Ì˼ ý×Íýžþô§óà?˜û½ök¿6¿ó;¿Ãý¾ë»¾‹÷~ï÷æùë¿þk^æe^†²Íóó×ý×¼Îë¼»»»ü[?~œ§?ýé?~œçç·û·y×yî÷Z¯õZ¼ôK¿4_ó5_à òR/õRüõ_ÿ5¿ýÛ¿Íë¼Îëp¿×z­×â­ßú­ù˜ùþ5>ë³>‹ÏþìÏæ}Þç}øîïþnþ5~ë·~‹×~í׿?Úgögó9Ÿó9çs¸ßoýÖoñ5_ó5üôOÿ4ÿÇç·~ë·xé—~i^ßþíßæmÞæmØÝÝåßâ½ßû½ù®ïú.ž›$^TŸõYŸÅgögó@»»»¼Îë¼ý×Í¿ÇOýÔOñÖoýÖ¼ ¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³yn¿ýÛ¿Íë¼Îëð@¶yAvwwy×yþú¯ÿš¯úª¯â£?ú£yn’xQ}Ög}ŸýÙŸÍý~û·›×y×á~¯õZ¯Åoÿöoó¼Ïû¼ßýÝßÍ¿ÅñãÇùª¯ú*Þû½ß›F÷{­×z->û³?›×y×á_ë£>ê£øê¯þj®ºêª«®ºêª«®ú Ù6W]uÕUW]uÕUWýôÙŸýÙ|Îç|÷{­×z-~û·›äÖ[oå!y÷{©—z)þú¯ÿšúìÏþl>çs>‡û½×{½ßýÝßÍ òÕ_ýÕ|ÌÇ| ÷{­×z-~û·›ç¶»»ËCòvwwy ÷z¯÷â­ßú­yé—~iüàpë­·ò×ý×üôOÿ4ßó=ßÃs{¯÷z/¾û»¿›çç·û·y×yî÷ =ˆg<ã¼0ßõ]ßÅ{¿÷{ðÛ¿ýÛ¼Îë¼÷;~ü8»»»ÜïAzïýÞïÍk¿ök°»»Ëw÷wó3?ó3<ÐñãÇù¨ú(>çs>‡û½×{½¯ýگ̓ü`þú¯ÿš¯þê¯æÏxôÒ/ýÒüÕ_ýÿÑ>û³?›ÏùœÏáló‚|ög6Ÿó9ŸÃý^ú¥_š¿þë¿æ~¯õZ¯Å[¿õ[óÒ/ýÒìîîòÓ?ýÓ|Ï÷|Ïíµ_ûµù­ßú-^‡<ä!Üzë­ÜïØ±c|ôG4¯ýÚ¯Ík¿ökpë­·rë­·òÝßýÝ|Ï÷|Ïí·~ë·xí×~mè³?û³¹ßç|Îçð@ïõ^ïŃü`î÷Ú¯ýÚ¼ök¿6ôÞïýÞ|Ï÷|ô =ˆþèæ¥_ú¥yí×~mî÷Û¿ýÛüöoÿ6ßýÝßÍ3žñ èøñã\¼x‘äµ_ûµùßùî÷YŸõY|ög6Ïí·û·y×yÈ6/È{¿÷{ó=ßó=<Ѓô >ú£?š—~é—æµ_ûµ¹ßoÿöoóÛ¿ýÛ|÷w7ÏxÆ3x ãÇóô§?ãÇó@ŸýÙŸÍý>çs>‡z¯÷z/üàs¿×~í׿µ_ûµ¹ßoÿöoó:¯ó:Üïµ^ëµøíßþm^þèæk¾ækxnïõ^ïÅ[¿õ[óÒ/ýÒ<øÁæ¯ÿú¯ùë¿þk¾û»¿›ßùßá¹ýÕ_ý/ýÒ/Í "‰û=øÁfww—ÝÝ]Ž;Æ{¿÷{óÚ¯ýÚ?~€¿þë¿æ«¿ú«yÆ3žÁsû­ßú-^ûµ_›«®ºêª«®ºêª«þGB¶ÍUW]uÕUW]uÕUÿ#ýöoÿ6¯ó:¯ÃÙæùîïþnÞç}Þ‡û}ÔG}_ýÕ_Íýõ_ÿ5/ó2/ÃýŽ?ÎÅ‹yAÞú­ßšŸù™Ÿá~_õU_ÅGôGóÜ>ú£?š¯ùš¯á¾ë»¾‹÷~ï÷æ…ùë¿þk^ûµ_›K—.q¿ãÇsñâEžŸßþíßæu^çux~Þê­ÞНþê¯æÁ~0ý×ÍWõWóÕ_ýÕ?~€ßþíßæu^çux~Þë½Þ‹ïþîïæùùê¯þj>æc>†ççØ±cüôOÿ4¯ýÚ¯ÍsÛÝÝåµ_ûµù›¿ùè¯þê¯xé—~iþ#}ög6Ÿó9ŸÃÙæùìÏþl>çs>‡çvìØ1¾û»¿›·~ë·æùùë¿þk^ûµ_›K—.ñ@õWÅK¿ôKóܾû»¿›÷yŸ÷á~ÇŽã·û·yé—~i^¿þë¿æµ_ûµ¹té÷{«·z+~ú§šDô[¿õ[¼ök¿6/Èoÿöoó:¯ó:<Ð{½×{ñÝßýݼ0»»»¼÷{¿7?ó3?ÃýÔOýoýÖoÍóóÚ¯ýÚüÎïü÷û¬Ïú,>û³?›çöÛ¿ýÛ¼Îë¼d›çç¯ÿú¯y™—yè½Þë½øîïþn^˜ÝÝ]>ú£?šïùžïá¾ë»¾‹÷~ï÷æ‘ÄýÖoý¯ýÚ¯Í òÛ¿ýÛ¼Îë¼÷{­×z-~û·›çç·û·y×yèØ±cüôOÿ4¯ýÚ¯Í òÕ_ýÕ|ÌÇ| tüøqžþô§süøqžIû³?›Åw÷wó>ïó><ÐoýÖoñÚ¯ýÚ<·ßþíßæu^çuxnïõ^ïÅw÷wó/ùíßþm^çu^‡çöYŸõY|ög6/̃ü`žñŒgðÜþê¯þŠ—~é—æ¹õÖ[yÈCÂ}×w}ïýÞïͤÏþìÏæs>çsx Û¼ ŸýÙŸÍç|ÎçðÜþê¯þŠ—~é—æ…ùíßþm^çu^‡úª¯ú*>ú£?šçöÞïýÞ|Ï÷|÷û¬Ïú,>û³?›ÉWõWó1ó1ë³øìÏþlžI<·÷z¯÷⻿û»ù—¼ôK¿4ó7ÃýŽ?ÎÅ‹¹êª«®ºêª«®ºê$dÛ\uÕUW]uÕUW]õ?Ö[¿õ[ó3?ó3Üï«¾ê«øèþhžŸ'N°»»Ëýlóü¼õ[¿5?ó3?Ãý>ë³>‹ÏþìÏæ¹ýõ_ÿ5/ó2/ÃýŽ;Æîî.Ïí§ú§y›·yèâÅ‹?~œÅîî.'Nœà~ë·~‹×~í׿¹ýöoÿ6¯ó:¯Ã;vŒ[o½•ãÇó/ùíßþm^çu^‡:vì·Þz+Çç…ùèþh¾æk¾†z¯÷z/¾û»¿›ÉK¿ôKó7ó7Üï³>ë³øìÏþlþ#}ög6Ÿó9ŸÃÙæùìÏþl>çs>‡z¯÷z/¾û»¿›Ńü`žñŒgp¿÷z¯÷⻿û»yn¯ýÚ¯ÍïüÎïp¿ïú®ïâ½ßû½ù—ìîîrâÄ Ž;ÆK¿ôKóà?˜ÏþìÏæÁ~0Ï$è·~ë·xí×~m^'N°»»Ëý¾ë»¾‹÷~ï÷æEõÖoýÖüÌÏü ÷{«·z+~ú§šççµ_ûµùßùî÷YŸõY|ög6Ïí·û·y×yÈ6ÏÏCòn½õVî÷]ßõ]¼÷{¿7/ª·~ë·æg~æg¸ßk½ÖkñÛ¿ýÛ¼ ’x ßú­ßâµ_ûµyA~û·›×y×á~¯õZ¯ÅoÿöoóÜ~ú§š·y›·á¾ê«¾ŠþèæEõÖoýÖüÌÏü ÷;~ü8OúÓ9~ü8ÏMÏíâÅ‹?~œÉw÷wó>ïó>û³?›ÏùœÏáló‚|ög6Ÿó9ŸÃýÖoý¯ýگ͋â½ßû½ùžïùî÷Z¯õZüöoÿ6Ïí­ßú­ù™Ÿùî÷Ú¯ýÚüÖoýÿÑ$ñ@¿õ[¿Åk¿ökó‚üöoÿ6¿ýÛ¿ ÀoÿöoóÓ?ýÓ?~œÕgögó9Ÿó9Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ<·ßþíßæu^çux Ûú£?šççÖ[oå!ytñâEŽ?νõ[¿5?ó3?Ãý¾ë»¾‹÷~ï÷æ?ƒ$è·~ë·xí×~mžÛoÿöoó:¯ó:<ÐW}ÕWñÑýѼ(~û·›×y×á¾ë»¾‹÷~ï÷æ_òÓ?ýÓ¼ÍÛ¼ d›Åk¿ökó;¿ó;Üï³>ë³øìÏþlþ#}ög6Ÿó9ŸÃÙæùìÏþl>çs>‡²Í‹ê³?û³ùœÏùî÷Z¯õZüöoÿ6Ïí«¿ú«ù˜ùè­ßú­ùª¯ú*üàóEô[¿õ[¼ök¿6ÿY>û³?›ÏùœÏá~¯õZ¯Åoÿöoóü¼ök¿6¿ó;¿Ãý>ë³>‹ÏþìÏæ¹ýöoÿ6¯ó:¯ÃÙæ?Ãgögó9Ÿó9Üïµ^ëµøíßþm^I<ÐoýÖoñÚ¯ýÚ¼ ¿ýÛ¿Íë¼Îëp¿×z­×â·û·ynyÈC¸õÖ[¹ßG}ÔGñÕ_ýÕük½ôK¿4ó7ÃýÞë½Þ‹ïþîïæ¹Iâ¾ê«¾ŠþèæE%‰ú­ßú-^ûµ_›«®ºêª«®ºêª«þÇA¶ÍUW]uÕUW]uÕUÿ£?~œK—.q¿§?ýé<øÁæ^æe^†¿þë¿æ~OúÓyðƒÌ òÒ/ýÒüÍßü ÷û©Ÿú)Þú­ßš:qâ»»»ÜïéO:~ðƒùrë­·ò7ó7üôOÿ4ßýÝßÍýÖoý¯ýÚ¯Ísûíßþm^çu^‡ú­ßú-^ûµ_›Åoÿöoó:¯ó:<ÐoýÖoñÚ¯ýÚüK~û·›×y×áló¢øìÏþl>çs>‡û}Ög}ŸýٟͤÏþìÏæs>çsx Û¼ ŸýÙŸÍç|Îçp¿—z©—â¯ÿú¯yQ}ög6Ÿó9ŸÃý^ëµ^‹ßþíßæ¹Ýzë­<ä!áùyðƒÌ[¿õ[óÚ¯ýÚ¼Õ[½ÿ’x ßú­ßâµ_ûµù´»»ËßüÍßðÓ?ýÓüôOÿ4·Þz+÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6Ïí·û·y×yÈ6ÿ‘~çw~‡ßþíßæ»¿û»¹õÖ[¹ßk½ÖkñÛ¿ýÛ¼ ’x ßú­ßâµ_ûµyA~û·›×y×á~¯õZ¯ÅoÿöoóÜ$ñ@?õS?Å[¿õ[ó¯õÞïýÞ|Ï÷|÷{­×z-~û·›ç&‰ú­ßú-^ûµ_›•$è·~ë·xí×~m®ºêª«®ºêª«®úÙ6W]uÕUW]uÕUWýöÞïýÞ|Ï÷|÷û®ïú.Þû½ß›ûíîîrâÄ î÷ =ˆ[o½•æ£?ú£ùš¯ùî÷QõQ|õW5÷»õÖ[yÈCÂý^ê¥^Š¿þë¿æßâw~çwØÝÝå¯ÿú¯ÙÝÝå¯ÿú¯ùë¿þkvwwyA~ë·~‹×~í׿¹ýöoÿ6¯ó:¯ÃýÖoý¯ýگ͋â·û·y×yèéO:~ðƒù—üöoÿ6¯ó:¯ÃÙæEñÙŸýÙ|Îç|÷û¬Ïú,>û³?›ÿHŸýÙŸÍç|Îçð@¶yA>û³?›ÏùœÏá~¯õZ¯Åoÿöoó¢úìÏþl>çs>‡û½Ök½¿ýÛ¿ÍóóÕ_ýÕ|ÌÇ| ÿ’·~ë·æµ_ûµy«·z+üàó¯!‰ú­ßú-^ûµ_›‹¿þë¿æÒ¥Küõ_ÿ5»»»üöoÿ6·Þz+·Þz+/Èk½ÖkñÛ¿ýÛïó>Üï£>ê£øê¯þjî÷ÝßýݼÏû¼÷{¯÷z/¾û»¿›æ§ú§y›·yî÷Ò/ýÒüÕ_ý÷ûîïþnÞç}Þ‡û}ÔG}_ýÕ_Í‹â·û·ùžïù~û·›[o½•‹ßú­ßâµ_ûµyn¿ýÛ¿Íë¼Îëð@¶yQýöoÿ6¯ó:¯ÃÙæEñÛ¿ýÛ¼Îë¼d›Ågögó9Ÿó9Üï³>ë³øìÏþlþ#}ög6Ÿó9ŸÃÙæùìÏþl>çs>‡û½Ök½¿ýÛ¿Í‹ê³?û³ùœÏùî÷Z¯õZüöoÿ6/Ègögó9Ÿó9¼¨^ú¥_š÷~ï÷æ­Þê­xðƒÌ¿Dô[¿õ[¼ök¿6/Š[o½•Ÿù™Ÿá§ú§ùíßþmþ-^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍsûíßþm^çu^‡²Í¿äÖ[oåg~ægøéŸþi~û·›‹×z­×â·û·yA$ñ@¿õ[¿Åk¿ökó‚üöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæ~û·›×y×álóoñÝßýݼÏû¼d›ç&‰ú­ßú-^ûµ_›•$è·~ë·xí×~m®ºêª«®ºêª«®úÙ6W]uÕUW]uÕUWývë­·ò‡<„û½ôK¿4õWÅýÞû½ß›ïùžïá~?õS?Å[¿õ[ó/‘Ä]¼x‘ãÇðÞïýÞ|Ï÷|÷û­ßú-^ûµ_›æ¯ÿú¯ù˜ù~û·›·z«·âg~ægx ßú­ßâµ_ûµyn¿ýÛ¿Íë¼Îëð@¶yQýöoÿ6¯ó:¯ÃÙæEñÛ¿ýÛ¼Îë¼d›Ågögó9Ÿó9Üï³>ë³øìÏþlþ#}ög6Ÿó9ŸÃÙæùìÏþl>çs>‡û½Ök½¿ýÛ¿Í‹ê³?û³ùœÏùî÷Z¯õZüöoÿ6/Ì_ÿõ_óÙŸýÙüÌÏü /ªãÇóÑýÑ|Ög}/Œ$è·~ë·xí×~mþ%Ÿó9ŸÃgögó¯ñ =ˆ?øÁüÎïü÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6Ïí·û·y×yÈ6/Ìç|ÎçðÕ_ýÕìîîò¢zЃÄñãÇù›¿ùî÷Z¯õZüöoÿ6/ˆ$è·~ë·xí×~m^ßþíßæu^çu¸ßk½ÖkñÛ¿ýÛ<Ðoÿöoó:¯ó:çs¸ßk½ÖkñÛ¿ýÛ¼(vwwùéŸþi~ú§šßþíßæÒ¥KüKÞû½ß›ïú®ïâ‘ÄýÖoý¯ýÚ¯Í ²»»Ëë¼Îëð×ý×üK^ëµ^‹—~é—æ¥_ú¥yí×~müàóÙŸýÙ|Îç|÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6Ïí·û·y×yÈ6ÏÏîî.oó6oÃoÿöoó/y­×z-^ú¥_š—~é—æµ_ûµyðƒÌgögó9Ÿó9Üïµ^ëµøíßþm^I<ÐoýÖoñÚ¯ýÚ¼ ¿ýÛ¿Íë¼Îëp¿×z­×â·û·y ßþíßæu^çux Ûü[|÷w7ïó>ïÃÙæ¹Iâ~ë·~‹×~í׿E%‰ú­ßú-^ûµ_›«®ºêª«®ºêª«þÇA¶ÍUW]uÕUW]uÕUÿã}ôG4_ó5_Ãý~ë·~‹×~í׿¯ÿú¯y™—yî÷Z¯õZüöoÿ6/Нþê¯æc>æc¸ßg}ÖgñÙŸýÙÜzë­<ä!á~oõVoÅOÿôOó‚ìîîò‡<„ÝÝ]èØ±c¼õ[¿5/ýÒ/ÍK¿ôKóÚ¯ýÚ¼ ’x ßú­ßâµ_ûµyn¿ýÛ¿Íë¼Îëð@¶yQýöoÿ6¯ó:¯ÃÙæEñÛ¿ýÛ¼Îë¼d›Ågögó9Ÿó9Üï³>ë³øìÏþlþ#}ög6Ÿó9ŸÃÙæùìÏþl>çs>‡û½Ök½¿ýÛ¿Í‹ê³?û³ùœÏùî÷Z¯õZüöoÿ6ÿ¿ýÛ¿ÍoÿöoóÛ¿ýÛüÎïü/Èw}×wñÞïýÞû³?›ÏùœÏá~¯õZ¯Åoÿöoó‚Hâ~ë·~‹×~í׿ùíßþm^çu^‡û½Ök½¿ýÛ¿Íýöoÿ6¯ó:¯ÃýÖoý¯ýگͿÖgögó9Ÿó9ÜïØ±cìîîòÜ$ñ@¿õ[¿Åk¿ökó¢’ÄýÖoý¯ýÚ¯ÍUW]uÕUW]uÕUÿã Ûæª«®ºêª«®ºêªÿñ~ú§š·y›·á~ŸõYŸÅgögóÕ_ýÕ|ÌÇ| ÷û¬Ïú,>û³?›Å_ÿõ_ó2/ó2Üïµ^ëµøíßþm¾û»¿›÷yŸ÷á~_õU_ÅGôGó‚|ôG4_ó5_ý×{½_ýÕ_ÍñãÇyQHâ~ë·~‹×~í׿¹ýöoÿ6¯ó:¯ÃÙæEõÛ¿ýÛ¼Îë¼d›Åoÿöoó:¯ó:çs¸ßk½ÖkñÛ¿ýÛü{íîîòÓ?ýÓ|ög6ÏxÆ3x ?øÁ<ýéOçù‘ÄýÖoý¯ýÚ¯Íóó×ý×¼Ì˼ ôR/õR|÷w7/ýÒ/Í‹â½ßû½ùžïùî÷Z¯õZüöoÿ6ÏÏk¿ökó;¿ó;Üï³>ë³øìÏþlžÛoÿöoó:¯ó:û³?›ÿHŸýÙŸÍç|Îçð@¶yA>û³?›ÏùœÏá~¯õZ¯Åoÿöoó¢úìÏþl>çs>‡û½Ök½¿ýÛ¿Í”ÝÝ]^ûµ_›¿ù›¿álóüHâ~ë·~‹×~í׿ùyï÷~o¾ç{¾‡ú«¿ú+^ú¥_šÕk¿ökó;¿ó;Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ<·ßþíßæu^çux Û<·þèæk¾ækx ¿ú«¿â¥_ú¥yQ½ök¿6¿ó;¿Ãý^ëµ^‹ßþíßæ‘ÄýÖoý¯ýÚ¯Í òÛ¿ýÛ¼Îë¼÷{­×z-~û·›çöà?˜g<ãÜï³>ë³øìÏþlþµ^æe^†¿þë¿æ~oõVoÅOÿôOóÜ$ñ@¿õ[¿Åk¿ökó¢’ÄýÖoý¯ýÚ¯ÍUW]uÕUW]uÕUÿã Ûæª«®ºêª«®ºêªÿ^ú¥_š¿ù›¿àøñã\¼x‘'N°»» À±cÇØÝÝå_ã½ßû½ùžïùîwñâE^çu^‡¿þë¿àAz·Þz+/È­·ÞÊCò軾ë»xï÷~o^T?ýÓ?ÍÛ¼ÍÛð@¿õ[¿Åk¿ökóÜ~û·›×y×áló¢úíßþm^çu^‡²Í‹â·û·y×yÈ6/ŠÏþìÏæs>çs¸ßg}ÖgñÙŸýÙüGúìÏþl>çs>‡²Í òÙŸýÙ|Îç|÷{­×z-~û·›Õgögó9Ÿó9Üïµ^ëµøíßþmè·û·ù™Ÿùþú¯ÿš¿þë¿æÁ~0õWÅ‹ê§ú§y›·yè·~ë·xí×~mž›$è·~ë·xí×~mžŸ×~í׿w~çw¸ßk½ÖkñÛ¿ýÛükœ8q‚ÝÝ]î÷Z¯õZüöoÿ6ÏÏk¿ökó;¿ó;Üï³>ë³øìÏþlžÛoÿöoó:¯ó:çs¸ßg}ÖgñÙŸýÙüGúìÏþl>çs>‡²Í òÙŸýÙ|Îç|÷{­×z-~û·›Õgögó9Ÿó9Üïµ^ëµøíßþmè·û·y×yèéO:~ðƒyQüöoÿ6¯ó:¯Ã]¼x‘ãÇóÜ$ñ@¿õ[¿Åk¿ökóü¼ök¿6¿ó;¿Ãý^ëµ^‹ßþíßæEõÕ_ýÕ|ÌÇ| ôZ¯õZüöoÿ6ÏÏk¿ökó;¿ó;Üï³>ë³øìÏþlžÛoÿöoó:¯ó:û³?›ÏùœÏá~ŸõYŸÅgögóé³?û³ùœÏùÈ6/Ègögó9Ÿó9Üïµ^ëµøíßþm^TŸýÙŸÍç|Îçp¿×z­×â·û·ynÇçÒ¥KÜï£>ê£øê¯þj^ŸýÙŸÍç|Îçp¿cÇŽ±»»Ëó#‰ú­ßú-^ûµ_›ççµ_ûµùßùî÷à?˜§?ýé¼(n½õV^æe^†ÝÝ]èµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ<·ßþíßæu^çux Û<·×~í׿w~çw¸ßƒü`žþô§ó¢¸õÖ[y™—yvwwy ×z­×â·û·yA$ñ@?õS?Å[¿õ[ó‚üöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyðƒÌ3žñ îwüøqžþô§süøqþ%¿ýÛ¿Íë¼Îëð@oõVoÅOÿôOóüHâ~ë·~‹×~í׿E%‰ú­ßú-^ûµ_›«®ºêª«®ºêª«þÇA¶ÍUW]uÕUW]uÕUÿkHâyúӟ΃ü`þµ^ú¥_š¿ù›¿áù¹xñ"Çç…9~ü8—.]â~Çç¯þê¯xðƒÌ ó9Ÿó9|ög6ÏÏg}ÖgñÙŸýÙ<·ßþíßæu^çux Û¼¨~û·›×y×áló¢øíßþm^çu^‡²Í‹â³?û³ùœÏùî÷YŸõY|ög6ÿ‘>û³?›ÏùœÏáló‚|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæEõÙŸýÙ|Îç|÷{­×z-~û·›çöÑýÑ|Í×| ô]ßõ]¼÷{¿7/Ì_ÿõ_ó2/ó2<Ðg}ÖgñÙŸýÙú£?š¯ùš¯á¾ê«¾Šþèæ…ùë¿þk^çu^‡ÝÝ]žÛk½ÖkñÛ¿ýÛçsxn/õR/Å_ÿõ_ó/ùèþh¾æk¾†zðƒÌw}×wñÚ¯ýÚ<Ðîî.?ó3?Ãw÷wóÛ¿ýÛ¼ ŸõYŸÅgögóÜ~û·›×y×áló¢úíßþm^çu^‡²Í‹â·û·y×yÈ6/ŠÏþìÏæs>çs¸ßg}ÖgñÙŸýÙüGúìÏþl>çs>‡²Í òÙŸýÙ|Îç|÷{­×z-~û·›Õgögó9Ÿó9Üïµ^ëµøíßþmžÛîî.~ðƒ¹téôÚ¯ýÚ|ôG4¯õZ¯Åñãǹß_ÿõ_ó3?ó3|õW5»»»ÜïØ±cÜzë­?~œççµ_ûµùßùè¥_ú¥yðƒÌîî.ïõ^ïÅ{¿÷{ð×ý×¼Ì˼ Ïí«¿ú«y¯÷z/Ž?Îýöoÿ6ßó=ßÃw÷wóÂØæùyí×~m~çw~‡û}Ög}ŸýÙŸÍsûíßþm^çu^‡²Ís»õÖ[yÈCÂsûìÏþl>ê£>ŠãÇó@ý×Í×|Í×ðÝßýݼ0¶yA^ûµ_›ßùßá^ú¥_š—~é—æÖ[oå½Þë½xï÷~oî÷Û¿ýÛ¼Îë¼÷{­×z-~û·›ä½ßû½ùžïùèøñã¼÷{¿7ïõ^ïÅK¿ôKs¿Ÿù™Ÿá»¿û»ùéŸþižÛw}×wñÞïýÞ¼ ’x ßú­ßâµ_ûµyQIâ~ë·~‹×~í׿ª«®ºêª«®ºêªÿqmsÕUW]uÕUW]uÕÿ_ýÕ_ÍÇ|ÌÇðÜ>ê£>Нþê¯æßâ·û·y×yžÛG}ÔGñÕ_ýÕüKvwwyðƒÌ¥K—xnÇç¥_ú¥¸õÖ[¹õÖ[ynïõ^ïÅñãÇùš¯ùî÷Z¯õZüöoÿ6Ïí·û·y×yÈ6/ªßþíßæu^çux Û¼(~û·›×y×áló¢øìÏþl>çs>‡û}Ög}ŸýٟͤÏþìÏæs>çsx Û¼ ŸýÙŸÍç|Îçp¿×z­×â·û·yQ}ög6Ÿó9ŸÃý^ëµ^‹ßþíßæùùéŸþiÞæmÞ†«cÇŽñÛ¿ýÛ¼ôK¿4/Ègögó9Ÿó9¼ ïõ^ïÅw÷ws¿þèæk¾ækx~^ú¥_šãdz»»Ë_ÿõ_óÜ^ê¥^Нþê¯æu^çux §?ýé<øÁæ¹½ök¿6¿ó;¿Ãý>ë³>‹ÏþìÏæ¹ýöoÿ6¯ó:¯ÃÙæùùèþh¾æk¾†çç¥_ú¥9~ü8»»»üõ_ÿ5ÏíAzßýÝßÍë¼Îëð@OúÓyðƒÌóóÕ_ýÕ|ÌÇ| /Èk½ÖkñÛ¿ýÛÜï·û·y×yî÷Z¯õZüöoÿ6/Èîî.ïýÞïÍÏüÌÏðoõU_õU|ôG4/Œ$è·~ë·xí×~m^T’x ßú­ßâµ_ûµ¹êª«®ºêª«®ºêdÛ\uÕUW]uÕUW]õ¿Æ_ÿõ_ó2/ó2<·ßú­ßâµ_ûµù·:~ü8—.]â~ë·~‹×~í׿Eñ×ý×¼õ[¿5ÏxÆ3xQ;vŒ¯þê¯æ½ßû½ùíßþm^çu^‡ºxñ"Çç~û·›×y×áló¢úíßþm^çu^‡²Í‹â·û·y×yÈ6/ŠÏþìÏæs>çs¸ßg}ÖgñÙŸýÙüGúìÏþl>çs>‡²Í òÙŸýÙ|Îç|÷{­×z-~û·›Õgögó9Ÿó9Üïµ^ëµøíßþm^ŸþéŸæ½ßû½¹téÿzЃøîïþn^ûµ_›fww—?øÁ\ºt‰ççµ^ëµøíßþmè½ßû½ùžïùþ5>ê£>ŠÏþìÏæøñã?~œK—.q¿ïú®ïâ½ßû½yn¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³yn¿ýÛ¿Íë¼Îëð@¶yAÞû½ß›ïùžïá_ã£>ê£øìÏþlŽ?΃ü`žñŒgp¿ïú®ïâ½ßû½y~vwwyí×~mþæoþ†ççÁ~0OúÓ¹ßoÿöoó:¯ó:Üïµ^ëµøíßþmþ%ýÑÍ×|Í×ð¯qìØ1¾û»¿›·~ë·æ_"‰ú­ßú-^ûµ_›•$è·~ë·xí×~m®ºêª«®ºêª«®úÙ6W]uÕUW]uÕUWý¯rüøq.]ºÄÙæßã­ßú­ù™ŸùÈ6ÿ»»»|ög6ßýÝßÍ¥K—xAô ñÞïýÞ|ôG4Çç~~ðƒyÆ3žÁý¾ê«¾Šþèæ~û·›×y×áló¢úíßþm^çu^‡²Í‹â·û·y×yÈ6/ŠÏþìÏæs>çs¸ßg}ÖgñÙŸýÙüGúìÏþl>çs>‡²Í òÙŸýÙ|Îç|÷{­×z-~û·›Õgögó9Ÿó9Üïµ^ëµøíßþm^˜ÝÝ]>û³?›ŸþéŸæÏx/ÌK½ÔKñÖoýÖ|ög6/ªÝÝ]Þû½ß›Ÿù™Ÿáù±Ísûîïþn>û³?›g<ã¼ ÇŽã­ßú­ùìÏþlüàs¿þèæk¾æk¸ßK¿ôKóWõW<·×~í׿w~çw¸ßg}ÖgñÙŸýÙ<·ßþíßæu^çux Û¼0?ýÓ?ÍGôGóŒg<ƒ䨱c¼õ[¿5Ÿýٟ̓ü`î÷ÑýÑ|Í×| ÷{ðƒÌÓŸþt^ÝÝ]>ú£?šïùžïáù±Íý~û·›×y×á~¯õZ¯Åoÿöoó¢¸õÖ[ùìÏþl~ú§šK—.ñ‚<èAâ½ßû½ùèþhŽ?΋Bô[¿õ[¼ök¿6/*I<ÐoýÖoñÚ¯ýÚ\uÕUW]uÕUW]õ?²m®ºêª«®ºêª«®ºê?ØoÿöoóÛ¿ýÛ<Ѓü`^ûµ_›?øÁ\uÕ_ÿõ_ó×ý×Üzë­<ÐK¿ôKóÒ/ýÒ<øÁæßjww—¿þë¿æ^ûµ_›æ¯ÿú¯ùë¿þkn½õVîwüøq^ú¥_š×~í׿º¿þë¿æ¯ÿú¯¹õÖ[¹ßñãÇyé—~i^ûµ_›ÿH»»»üõ_ÿ5ôÚ¯ýÚügøíßþmþú¯ÿšÝÝ]î÷à?˜—~é—æ¥_ú¥¹êª«®ºêª«®ºêªÙ6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºê¨ÝÝ]>çs>‡ïþîïà½ßû½ùª¯ú*®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«^dÛ\uÕUWýõÑýÑ|Í×| ôQõQ|õW5W]uÕUW]uÕUW]uÕUW]uÕUW]uÕU/²m®ºêª«þ‡:qâ»»»<Ðñãǹxñ"WýÏuÇwPkåºë®ãªÿùžñŒg°±±Á™3g¸ê¾[o½•­­-NŸ>ÍUÿó=õ©OåäÉ“œ8q‚«þç{ÊSžÂ™3g8vìWýÏ÷Ä'>‘n¸íím®úŸï Ox7ß|3›››\õ?ß?üÃ?ðЇ>”ÅbÁUÿóýÝßý|ä#™Íf\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(Iê£øê¯þjþ+Iâùy¿×þqþ%où^á-ßû±\õ_ïWõWÙÞÞæU^åU¸ê¾_ø…_àÚk¯åå_þå¹ê¾Ÿù™ŸáAz/ýÒ/ÍUÿóýèþ(/ñ/Ácó®úŸïðy…WxñˆGpÕÿ|ßó=ßÃk½Ökñà?˜«þçûŽïøÞèÞˆ›nº‰«þçûÆoüFÞáÞ3gÎpÕÿ|_ó5_Ã{½×{qüøq®ºêª«®zȶ¹êª«þWyí×~m~çw~Û¼0’x­×z-~û·›ûÝzë­<ä!à­Þê­øéŸþižŸ÷~ï÷æ{¾ç{ø«¿ú+^ú¥_šÿJ’x~ÞﵜÉ[¾×cxË÷~,Wý×ûÕ_ýU¶··y•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?ÊK¼ÄKð˜Ç<†«þçûÁüA^á^G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘wx‡wàÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêªÿU^ûµ_›ßùßÀ6/Œ$^ëµ^‹ßþíßæ^ú¥_š¿ù›¿àéO:~ðƒy ÝÝ]Nœ8ÀK½ÔKñ×ý×üW“Äóó~¯ýãüKÞò½Ã[¾÷c¹ê¿Þ¯þ꯲½½Í«¼Ê«pÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^â%^‚Ç<æ1\õ?ßþàò ¯ð <âàªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼Ã;¼gΜáªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUWý¯òÚ¯ýÚüÎïü¶ya$ðZ¯õZüöoÿ6ôÝßýݼÏû¼_õU_ÅGôGó@ßýÝßÍû¼ÏûðU_õU|ôG4ÿ^»»»|Îç|ßýÝßÍîî.ÿVï÷Ú?οä-ßë1¼å{?–«þëýê¯þ*ÛÛۼʫ¼ WýÏ÷ ¿ð \{íµ¼üË¿æc>€Ÿú©Ÿâ­ßú­ùpüøq.]ºÄ¿×û½öó/yË÷z oùÞåªÿz¿ú«¿Êöö6¯ò*¯ÂUÿóýÂ/ü×^{-/ÿò/ÏUÿóýÌÏü zЃxé—~i®úŸïGôGy‰—x ó˜ÇpÕÿ|?øƒ?È+¼Â+ðˆG<‚«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òïðœ9s†«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]õ¿Êk¿ökó;¿ó;Øæùíßþm^çu^€×z­×â·û·y~üàóŒg<ƒ?øÁ<ýéOà!y·Þz+zЃ¸õÖ[ùòÑýÑ|Í×| ÿ^ï÷Ú?οä-ßë1¼å{?–«þëýê¯þ*ÛÛۼʫ¼ WýÏ÷ ¿ð \{íµ¼üË¿çs>€¿ú«¿àe^æeø¨ú(¾ú«¿šÿHýÑÍw÷wséÒ%þ­ÞﵜÉ[¾×cxË÷~,Wý×ûÕ_ýU¶··y•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?ÊK¼ÄKð˜Ç<†«þçûÁüA^á^G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘wx‡wàÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêªÿU^ûµ_›ßùßà·~ë·xí×~mžŸÏþìÏæs>çsx­×z-~û·›ççÖ[oå!yõQÀ×|Í×ðô§??øÁüw‘Äóó~¯ýãüKÞò½Ã[¾÷c¹ê¿Þ¯þ꯲½½Í«¼Ê«pÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^â%^‚Ç<æ1\õ?ßþàò ¯ð <âàªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼Ã;¼gΜáªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUWý¯òÑýÑ|Í×| ŸõYŸÅgögóÜvwwyÈCÂîî.¯õZ¯Åoÿöoó‚¼õ[¿5?ó3?Ãü`n½õV^ê¥^Š¿þë¿æ¿“$žŸ÷{íç_ò–ïõÞò½ËUÿõ~õW•íím^åU^…«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèò/ñ<æ1áªÿù~ðWx…WàxWýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäÞá8sæ WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºê•ŸþéŸæmÞæm¸ßWõWóQõQÜï·û·ù˜ùþú¯ÿšû½Ök½¿ýÛ¿Í òÝßýݼÏû¼ô]ßõ]¼÷{¿7ÿ$ñü¼ßkÿ8ÿ’·|¯Çð–ïýX®ú¯÷«¿ú«looó*¯ò*\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”—x‰—à1y WýÏ÷ƒ?øƒ¼Â+¼xÄ#¸ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#ïðïÀ™3g¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUÿë¼ôK¿4ó7Ãýüàóà?˜[o½•[o½€÷z¯÷âÖ[oåw~çwx­×z-~û·›æøñã\ºt‰û]¼x‘ãÇóßIÏÏû½öó/yË÷z oùÞåªÿz¿ú«¿Êöö6¯ò*¯ÂUÿóýÂ/ü×^{-/ÿò/ÏUÿóýÌÏü zЃxé—~i®úŸïGôGy‰—x ó˜ÇpÕÿ|?øƒ?È+¼Â+ðˆG<‚«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òïðœ9s†«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]õ¿Îîî.oýÖoÍïüÎïðü|ÔG}_ýÕ_Ík¿ökó;¿ó;¼Ök½¿ýÛ¿Í óÞïýÞ|Ï÷|ïõ^ïÅw÷wóßMÏÏû½öó/yË÷z oùÞåªÿz¿ú«¿Êöö6¯ò*¯ÂUÿóýÂ/ü×^{-/ÿò/ÏUÿóýÌÏü zЃxé—~i®úŸïGôGy‰—x ó˜ÇpÕÿ|?øƒ?È+¼Â+ðˆG<‚«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òïðœ9s†«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]õ¿Ö_ÿõ_óÓ?ýÓÜzë­?~œ?øÁ¼õ[¿5~ðƒøë¿þkvww9~ü8/ýÒ/Í ³»»Ë_ÿõ_ðà?˜?øÁüw“Äóó~¯ýãüKÞò½Ã[¾÷c¹ê¿Þ¯þ꯲½½Í«¼Ê«pÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^â%^‚Ç<æ1\õ?ßþàò ¯ð <âàªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼Ã;¼gΜáªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žŸ÷{íç_ò–ïõÞò½ËUÿõ~õW•íím^åU^…«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèò/ñ<æ1áªÿù~ðWx…WàxWýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäÞá8sæ WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñü¼ßkÿ8ÿ’·|¯Çð–ïýX®ú¯÷«¿ú«looó*¯ò*\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”—x‰—à1y WýÏ÷ƒ?øƒ¼Â+¼xÄ#¸ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#ïðïÀ™3g¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰ççý^ûÇù—¼å{=†·|ïÇrÕ½_ýÕ_e{{›Wy•Wáªÿù~á~k¯½–—ù—çªÿù~æg~†=èA¼ôK¿4WýÏ÷£?ú£¼ÄK¼yÌc¸ê¾üÁä^áxÄ#ÁUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¿ñy‡wxΜ9ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(Iú£?šÅWõWó1ó1<Ðk½ÖkñÛ¿ýÛüO$‰ççý^ûǹê®ã¿›¶êØ¿ã4WýÏwâ‘w1Ì9¸ë$WýÏwòÑw°ÞÝäðž\õç-ßë1¼å{?–ÿh?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêÿ©ÏþìÏæs>çsø¬Ïú,>û³?›É_ÿõ_óÚ¯ýÚ\ºt €¯úª¯â£?ú£¹ßoÿöoó:¯ó:¼Ök½¿ýÛ¿Í¿äÖ[oå­ßú­ù›¿ùüàóô§?çöÛ¿ýÛ¼Îë¼/õR/ÅßüÍßðÒ/ýÒüÕ_ý/Š—y™—á¯ÿú¯yЃÄ3žñ ^ëµ^‹ßþíßæ"Iû³?›ÏùœÏàµ^ëµøíßþmî÷Û¿ýÛ¼Î뼯õZ¯Åoÿöoó¢øë¿þk^ûµ_›K—.ðYŸõY|ög6ôÛ¿ýÛ¼Î뼯õZ¯ÅñãÇù™Ÿùžþô§óà?˜æÖ[oå!yõQÅ×|Í×ðZ¯õZüöoÿ6ÿIâùy¿×þq®úŸëøÃ玲:öï8ÍUÿóxä]Œsî:ÉUÿó|ô¬w79¼çWýÇyË÷z oùÞå?ÚüÈð’/ù’<æ1áªÿù~à~€W|ÅWäxWýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàßñ9sæ WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®úê³?û³ùœÏù>ë³>‹ÏþìÏæEñÛ¿ýÛ¼Îë¼ÇçâÅ‹Üï·û·y×y^ëµ^‹ßþíßæEõÝßýݼÏû¼ÇçâÅ‹<Ðoÿöoó:¯ó:¼Ök½ïýÞïÍû¼ÏûðU_õU|ôG4/ÌWõWó1ó1;vŒŸþéŸæu^çux­×z-~û·›ÿ‰$ñü¼ßkÿ8WýÏuüáwÓVûwœæªÿùN<ò.ƃ9wäªÿùN>úÖ»›Þs‚«þã¼å{=†·|ïÇòíG~äGxÉ—|Ió˜ÇpÕÿ|?ð?À+¾â+òˆG<‚«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7|Ã7ðŽïøŽœ9s†«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUWý?õÙŸýÙ|Îç|ŸõYŸÅgögó¢øíßþm^çu^‡ûÙæ~¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿÇçÒ¥KüÕ_ý/ýÒ/Íý~û·›×y×àµ^ëµøíßþmŽ?Î¥K—xé—~iþê¯þŠæ!y·Þz+ïõ^ïÅ{¿÷{ó:¯ó:¼Ök½¿ýÛ¿ÍÿD’x~Þﵜ«þç:þð»i«Žý;NsÕÿ|'yãÁœƒ»NrÕÿ|'}ëÝMï9ÁUÿqÞò½Ã[¾÷cùö#?ò#¼äK¾$yÌc¸ê¾øà_ñyÄ#ÁUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áxÇw|GΜ9ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«þŸúìÏþl>çs>€Ïú¬Ïâ³?û³yQ|÷w7ïó>ïÀk½ÖkñÛ¿ýÛÜï·û·y×y^ëµ^‹ßþíßæ_ã­ßú­ù™Ÿù¾ê«¾Šþèæ~¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ïýÞïÍ÷|Ï÷ðô§??øÁçs>€Ïú¬Ïâ³?û³ù—ìîîò2/ó2Üzë­|ÔG}_ýÕ_Íý~û·›×y×àµ^ëµøíßþmþ5>û³?›ÏùœÏà³>ë³øìÏþlî÷Û¿ýÛ¼Î뼯õZ¯ÅoÿöoóÓ?ýÓ¼ÍÛ¼ _õU_ÅGôGóü|ôG4_ó5_Ãô n½õV~û·›×y×àµ^ëµøíßþmþ'’Äóó~¯ýã\õ?×ñ‡ßM[uìßqš«þç;ñÈ»æÜu’«þç;ùè;XïnrxÏ ®úó–ïõÞò½Ë´ù‘á%_ò%yÌcÃUÿóýÀü¯øŠ¯È#ñ®úŸï{¾ç{x­×z-üàsÕÿ|ßñßÁ½ÑqÓM7qÕÿ|ßð ßÀ;¾ã;ræÌ®úŸïk¾ækx¯÷z/Ž?ÎUW]uÕUÏÙ6W]õÿÔgögó9Ÿó9|Ög}ŸýÙŸÍ ó×ý×|ÌÇ| ¿ýÛ¿Íýžþô§óà?˜ûýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛük|ög6Ÿó9ŸÀk½ÖkñÛ¿ýÛÜï·û·y×y^ëµ^‹ßþíßàøñã\ºt‰—~é—æ¯þê¯x~ò‡pë­·òQõQ|õW5¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿIâùy¿×þq®úŸëøÃ玲:öï8ÍUÿóxä]Œsî:ÉUÿó|ô¬w79¼çWýÇyË÷z oùÞå?ÚüÈð’/ù’<æ1áªÿù~à~€W|ÅWäxWýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàßñ9sæ WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®úê³?û³ùœÏùüàóà?˜ä¯ÿú¯ÙÝÝå¾ê«¾Šþèæ~û·›×y×àµ^ëµøíßþmþ5>û³?›ÏùœÏàµ^ëµøíßþmî÷Û¿ýÛ¼Î뼯õZ¯ÅoÿöoðÞïýÞ|Ï÷|OúÓyðƒÌýõ_ÿ5/ó2/À_ýÕ_ñÒ/ýÒüöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛüO$‰ççý^ûǹê®ã¿›¶êØ¿ã4WýÏwâ‘w1Ì9¸ë$WýÏwòÑw°ÞÝäðž\õç-ßë1¼å{?–ÿh?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêÿ©ÏþìÏæs>çsø·øª¯ú*>ú£?šçöÛ¿ýÛ¼Î뼯õZ¯Åoÿöoó¯ñÙŸýÙ|Îç|¯õZ¯Åoÿöos¿ßþíßæu^çux­×z-~û·€ŸþéŸæmÞæmøª¯ú*>ú£?šúèþh¾æk¾†=èAÜzë­üöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛüWØÝÝås>çsøîïþnvwwù·z¿×þq®úŸëøÃ玲:öï8ÍUÿóxä]Œsî:ÉUÿó|ô¬w79¼çWýÇyË÷z oùÞå?ÚüÈð’/ù’<æ1áªÿù~à~€W|ÅWäxWýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàßñ9sæ WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®úê³?û³ùœÏù^/õR/Ńü`^ûµ_›÷~ï÷æøñãú£?€÷~ï÷æ{¾ç{x©—z)þú¯ÿšûýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛüW8~ü8—.]âßëý^ûǹê®ã¿›¶êØ¿ã4WýÏwâ‘w1Ì9¸ë$WýÏwòÑw°ÞÝäðž\õç-ßë1¼å{?–ÿh?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêÿ©ÏþìÏæs>çsø¬Ïú,>û³?›¯ßþíßæu^çux­×z-~û·›Õ­·ÞÊCòî÷ô§??øÁÜï·û·y×y^ëµ^‹ßþíßæŽ?Î¥K—xé—~iþê¯þ €'N°»»ËW}ÕWñÑýÑÜï·û·y×y^ëµ^‹ßþíßæ¿ÂGôGó5_ó5ü{½ßkÿ8WýÏuüáwÓVûwœæªÿùN<ò.ƃ9wäªÿùN>úÖ»›Þs‚«þã¼å{=†·|ïÇòíG~äGxÉ—|Ió˜ÇpÕÿ|?ð?À+¾â+òˆG<‚«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7|Ã7ðŽïøŽœ9s†«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUWý?õÙŸýÙ|Îç|ŸõYŸÅgögóïõÛ¿ýÛ¼Î뼯õZ¯Åoÿöoó¢zï÷~o¾ç{¾€·z«·â§ú§y ßþíßæu^çux­×z-~û·›zï÷~o¾ç{¾€§?ýéüõ_ÿ5oó6oÀÓŸþtüàs¿ßþíßæu^çux­×z-~û·›ÿ*ýÑÍw÷wséÒ%þ­Þﵜ«þç:þð»i«Žý;NsÕÿ|'yãÁœƒ»NrÕÿ|'}ëÝMï9ÁUÿqÞò½Ã[¾÷cùö#?ò#¼äK¾$yÌc¸ê¾øà_ñyÄ#ÁUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áxÇw|GΜ9ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«þŸúìÏþl>çs>€Ïú¬Ïâ³?û³ù÷úíßþm^çu^€×z­×â·û·yQüôOÿ4oó6oÃý~ë·~‹×~í׿~û·›×y×àµ^ëµøíßþmè§ú§y›·y¾ê«¾Š¿þë¿æ{¾ç{x©—z)þú¯ÿšúíßþm^çu^€×z­×â·û·ùŸHÏÏû½ösÕÿ\Ç~7mÕ±Çi®úŸïÄ#ïb<˜sp×I®úŸïä£ï`½»Éá='¸ê?Î[¾×cxË÷~,ÿÑ~äG~„—|É—ä1y WýÏ÷?ð¼â+¾"xÄ#¸ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïøŽïÈ™3g¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕÿSŸýÙŸÍç|ÎçðYŸõY|ög6ÿ^¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿ’¯ùš¯á³?û³ÙÝÝà­Þê­øéŸþižÛoÿöoó:¯ó:¼Ök½¿ýÛ¿Ís;~ü8—.]âµ^ëµø›¿ùvwwùª¯ú*>ú£?šúíßþm^çu^€×z­×â·û·ùŸHÏÏû½ösÕÿ\Ç~7mÕ±Çi®úŸïÄ#ïb<˜sp×I®úŸïä£ï`½»Éá='¸ê?Î[¾×cxË÷~,ÿÑ~äG~„—|É—ä1y WýÏ÷?ð¼â+¾"xÄ#¸ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïøŽïÈ™3g¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕÿSŸýÙŸÍç|ÎçðYŸõY|ög6ÿ^¿ýÛ¿Íë¼Îëðà?˜÷~ï÷æùë¿þkþú¯ÿš[o½•û½ÔK½¿ýÛ¿ÍñãÇyn¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6Ïí£?ú£ùš¯ùèâÅ‹?~œúíßþm^çu^€×z­×â·û·ùŸHÏÏû½ösÕÿ\Ç~7mÕ±Çi®úŸïÄ#ïb<˜sp×I®úŸïä£ï`½»Éá='¸ê?Î[¾×cxË÷~,ÿÑ~äG~„—|É—ä1y WýÏ÷?ð¼â+¾"xÄ#¸ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïøŽïÈ™3g¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕÿSŸýÙŸÍç|ÎçðYŸõY|ög6ÿ^¿ýÛ¿Íë¼ÎëðoñZ¯õZüôOÿ4Ççùùíßþm^çu^€×z­×â·û·yný×Í˼ÌËp¿·z«·â§ú§yn¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿIâùy¿×þq®úŸëøÃ玲:öï8ÍUÿóxä]Œsî:ÉUÿó|ô¬w79¼çWýÇyË÷z oùÞå?ÚüÈð’/ù’<æ1áªÿù~à~€W|ÅWäxWýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàßñ9sæ WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®úê³?û³ùœÏù>ë³>‹ÏþìÏæßë·û·y×y^TÇŽã­ßú­yï÷~o^ûµ_›æ·û·y×y^ëµ^‹ßþíßæùyðƒÌ3žñ ¾ë»¾‹÷~ï÷æ¹ýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛüO$‰ççý^ûǹê®ã¿›¶êØ¿ã4WýÏwâ‘w1Ì9¸ë$WýÏwòÑw°ÞÝäðž\õç-ßë1¼å{?–ÿh?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêÿ©[o½•[o½€?øÁ<øÁæßkww—¿þë¿æEñà?˜?øÁ¼¨vwwùë¿þkŽ?ÎK¿ôKóüüõ_ÿ5»»»¼ôK¿4Çç¹íîîò×ý×?~œ—~é—æ"IúÖ»›Þs‚«þã¼å{=†·|ïÇòíG~äGxÉ—|Ió˜ÇpÕÿ|?ð?À+¾â+òˆG<‚«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7|Ã7ðŽïøŽœ9s†«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]uÕÿP’x~Þﵜ«þç:þð»i«Žý;NsÕÿ|'yãÁœƒ»NrÕÿ|'}ëÝMï9ÁUÿqÞò½Ã[¾÷cùö#?ò#¼äK¾$yÌc¸ê¾øà_ñyÄ#ÁUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áxÇw|GΜ9ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(IÍUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]uÕÿP’x~lsÕÿ\¿ò+¿Â±cÇxåW~e®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùŽ;Æ+¿ò+sÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯüʯpìØ1^ù•_™«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßüÈðR/õR<úÑæªÿù~à~€Wz¥Wâá8WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàÞé8}ú4WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~åW~…cÇŽñʯüÊ\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ+¿ò+;vŒW~åWæªÿù~á~k¯½–—ù—çªÿù~æg~†=èA¼ôK¿4WýÏ÷#?ò#¼ÔK½~ô£¹ê¾øà•^é•xøÃÎUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áx§wz'NŸ>ÍUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]uÕÿP’x~lsÕÿ\¿ò+¿Â±cÇxåW~e®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùŽ;Æ+¿ò+sÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯüʯpìØ1^ù•_™«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßüÈðR/õR<úÑæªÿù~à~€Wz¥Wâá8WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàÞé8}ú4WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~åW~…cÇŽñʯüÊ\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ+¿ò+;vŒW~åWæªÿù~á~k¯½–—ù—çªÿù~æg~†=èA¼ôK¿4WýÏ÷#?ò#¼ÔK½~ô£¹ê¾øà•^é•xøÃÎUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áx§wz'NŸ>ÍUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]uÕÿP’x~lsÕÿ\¿ò+¿Â±cÇxåW~e®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùŽ;Æ+¿ò+sÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯüʯpìØ1^ù•_™«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßüÈðR/õR<úÑæªÿù~à~€Wz¥Wâá8WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàÞé8}ú4WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~åW~…cÇŽñʯüÊ\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ+¿ò+;vŒW~åWæªÿù~á~k¯½–—ù—çªÿù~æg~†=èA¼ôK¿4WýÏ÷#?ò#¼ÔK½~ô£¹ê¾øà•^é•xøÃÎUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áx§wz'NŸ>ÍUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]uÕÿP’x~lsÕÿ\¿ò+¿Â±cÇxåW~e®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùŽ;Æ+¿ò+sÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯüʯpìØ1^ù•_™«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßüÈðR/õR<úÑæªÿù~à~€Wz¥Wâá8WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàÞé8}ú4WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~åW~…cÇŽñʯüÊ\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ+¿ò+;vŒW~åWæªÿù~á~k¯½–—ù—çªÿù~æg~†=èA¼ôK¿4WýÏ÷#?ò#¼ÔK½~ô£¹ê¾øà•^é•xøÃÎUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áx§wz'NŸ>ÍUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]uÕÿP’x~lsÕÿ\¿ú«¿ÊÎίüʯÌUÿóýâ/þ"×\s /ÿò/ÏUÿóýìÏþ,·Ür /ýÒ/ÍUÿóýèþ(/ù’/É£ýh®úŸïðyÅW|Eþð‡sÕÿ|ßû½ßËk¼Ækð‡<„«þçûÎïüNÞð ß›nº‰«þçûÆoüFÞñߑӧOsÕÿ|_÷u_Ç{¼Ç{püøq®ºêª«®zȶ¹êª«®úJÏm®úŸëW~åW8vì¯üʯÌUÿóýÂ/ü×^{-/ÿò/ÏUÿóýÌÏü zЃxé—~i®úŸïG~äGx©—z)ýèGsÕÿ|?ð?À+½Ò+ñð‡?œ«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7|Ã7ðNïôNœ>}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]uÕÿP’x~lsÕÿ\¿ò+¿Â±cÇxåW~e®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùŽ;Æ+¿ò+sÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯüʯpìØ1^ù•_™«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßüÈðR/õR<úÑæªÿù~à~€Wz¥Wâá8WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàÞé8}ú4WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~åW~…cÇŽñʯüÊ\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ+¿ò+;vŒW~åWæªÿù~á~k¯½–—ù—çªÿù~æg~†=èA¼ôK¿4WýÏ÷#?ò#¼ÔK½~ô£¹ê¾øà•^é•xøÃÎUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áx§wz'NŸ>ÍUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]uÕÿP’x~lsÕÿ\¿ò+¿Â±cÇxåW~e®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùŽ;Æ+¿ò+sÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯüʯpìØ1^ù•_™«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßüÈðR/õR<úÑæªÿù~à~€Wz¥Wâá8WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàÞé8}ú4WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~åW~…cÇŽñʯüÊ\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ+¿ò+;vŒW~åWæªÿù~á~k¯½–—ù—çªÿù~æg~†=èA¼ôK¿4WýÏ÷#?ò#¼ÔK½~ô£¹ê¾øà•^é•xøÃÎUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áx§wz'NŸ>ÍUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]uÕÿP’x~lsÕÿ\¿ò+¿Â±cÇxåW~e®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùŽ;Æ+¿ò+sÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯüʯpìØ1^ù•_™«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßüÈðR/õR<úÑæªÿù~à~€Wz¥Wâá8WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàÞé8}ú4WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~åW~…cÇŽñʯüÊ\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ+¿ò+;vŒW~åWæªÿù~á~k¯½–—ù—çªÿù~æg~†=èA¼ôK¿4WýÏ÷#?ò#¼ÔK½~ô£¹ê¾øà•^é•xøÃÎUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áx§wz'NŸ>ÍUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]uÕÿP’x~lsÕÿ\¿ò+¿Â±cÇxåW~e®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùŽ;Æ+¿ò+sÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯüʯpìØ1^ù•_™«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßüÈðR/õR<úÑæªÿù~à~€Wz¥Wâá8WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàÞé8}ú4WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~åW~…cÇŽñʯüÊ\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ+¿ò+;vŒW~åWæªÿù~á~k¯½–—ù—çªÿù~æg~†=èA¼ôK¿4WýÏ÷#?ò#¼ÔK½~ô£¹ê¾øà•^é•xøÃÎUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áx§wz'NŸ>ÍUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]uÕÿP’x~lsÕÿ\¿ò+¿Â±cÇxåW~e®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùŽ;Æ+¿ò+sÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯüʯpìØ1^ù•_™«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßüÈðR/õR<úÑæªÿù~à~€Wz¥Wâá8WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàÞé8}ú4WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~åW~…cÇŽñʯüÊ\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ+¿ò+;vŒW~åWæªÿù~á~k¯½–—ù—çªÿù~æg~†=èA¼ôK¿4WýÏ÷#?ò#¼ÔK½~ô£¹ê¾øà•^é•xøÃÎUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áx§wz'NŸ>ÍUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(Iá«^“G½ô~á~k¯½–—ù—çªÿù~æg~†=èA¼ôK¿4WýÏ÷#?ò#¼ÔK½~ô£¹ê¾øà•^é•xøÃÎUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áx§wz'NŸ>ÍUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUWý/ ‰‹÷~ï÷櫾ê«8~ü8ÿüõ_ÿ5ïó>ïÃ_ýÕ_qHâùy¿×þq®úŸëøÃ玲zöï8ÅUÿs}ÂW½&zé3üÂ/ü×^{-/ÿò/ÏUÿóýÌÏü zЃxé—~i®úŸïG~äGx©—z)ýèGsÕÿ|?ð?À+½Ò+ñð‡?œ«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7|Ã7ðNïôNœ>}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUWý/ ‰«—~é—æ·~ë·8~ü8ÿ“}ög6Ÿó9Ÿ€m®I}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUWý/ ‰ûýÖoý¯ýگ͋âÁ~0ÏxÆ3øª¯ú*>ú£?šÿ©^ûµ_›ßùßÀ6W$žŸ÷{íçªÿ¹Ž?ünÚªgÿŽS\õ?×'|Õkò¨—>Ã/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕÿ’¸ßoýÖoñÚ¯ýÚ¼(Þû½ß›ïùžïà³>ë³øìÏþlžŸÝÝ]~æg~†[o½•¿þë¿æÁ~0Ççµ_ûµy­×z-^T»»»üÌÏü ·Þz+ý×̓ü`üàóZ¯õZ¼ôK¿4ÏÏ_ÿõ_séÒ%>ú£?š¿þë¿à·û·8vì/ýÒ/ À­·ÞÊ3žñ ^ëµ^ €ïùžïá§ú§yé—~i^ëµ^‹—~é—æoþæo8vì/ýÒ/Í¿äw~çw8vì/ýÒ/Íÿ$’x~Þﵜ«þç:þð»i«žý;NqÕÿ\ŸðU¯É£^ú ¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUÿ Hâ~¿õ[¿Åk¿ökó¢øìÏþl>çs>€Ïú¬Ïâ³?û³ynŸó9ŸÃWõW³»»ËóóÚ¯ýÚ|ÕW}/ýÒ/Í ó9Ÿó9|õW5»»»çs>Û¼Ì˼ ý×Í}ÕW}ó1Àñãǹxñ"/ÌOÿôOó6oó6|ÔG}_ýÕ_Íÿ$’x~Þﵜ«þç:þð»i«žý;NqÕÿ\ŸðU¯É£^ú ¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUÿ Hâ~¿õ[¿Åk¿ökó¢xí×~m~çw~€¯úª¯â£?ú£y ÷yŸ÷ỿû»¹ßƒô üàð×ý×\ºt €ãÇóU_õU¼÷{¿7ÏÏÛ¼ÍÛðÓ?ýÓÜïAz~ðƒøë¿þk.]ºÀK¿ôKóS?õS<øÁæ~¯ýÚ¯ÍïüÎïðü¼Ök½¿ýÛ¿ Àgögó9Ÿó9|Ög}Ÿó9ŸÃs»xñ"ïýÞïÍÏüÌÏðS?õS¼õ[¿5/È{¿÷{ó=ßó=<ýéOçÁ~0ÿ“Hâùy¿×þq®úŸëøÃ玲zöï8ÅUÿs}ÂW½&zé3üÂ/ü×^{-/ÿò/ÏUÿóýÌÏü zЃxé—~i®úŸïG~äGx©—z)ýèGsÕÿ|?ð?À+½Ò+ñð‡?œ«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7|Ã7ðNïôNœ>}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUWý/ ‰ûýÖoý¯ýگͿä·û·y×yî÷WõW¼ôK¿4÷ûèþh¾æk¾€cÇŽñÓ?ýÓ¼ök¿6÷ÛÝÝå«¿ú«ùœÏùŽ?ÎoýÖoñÒ/ýÒ<ÐGôGó5_ó5<èA⻿û»yí×~mî·»»ËGôGó=ßó=¼ôK¿4õWÅs{í×~m~çw~Û<·ÏþìÏæs>çs¸ß±cÇøèþh^ûµ_›[o½•¿þë¿æ«¿ú«ùéŸþiÞæmÞ€÷z¯÷⻿û»y~vww9qâ/õR/Å_ÿõ_ó?$žŸ÷{íçªÿ¹Ž?ünÚªgÿŽS\õ?×'|Õkò¨—>Ã/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕÿ’¸ßoýÖoñÚ¯ýÚ¼0ßó=ßÃGôG³»» Àk½ÖkñÛ¿ýÛÜïÖ[oå!yÇŽã·û·yé—~ižŸÏþìÏæs>çsx¯÷z/¾û»¿›ûÝzë­<ä!àØ±cüõ_ÿ5~ðƒy~Þú­ßšŸù™Ÿ໾ë»xï÷~oèµ_ûµùßùlóÜ>û³?›ÏùœÏá~¿õ[¿Åk¿ökóü?~œK—.pñâEŽ?ÎsûîïþnÞç}Þ€ïú®ïâ½ßû½ùŸFÏÏû½ösÕÿ\Ç~7mÕ³Ç)®úŸë¾ê5yÔKŸá~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêIÜï¥_ú¥9~ü8/Èoÿöoó@ÇŽã¯ÿú¯yðƒÌý¾ú«¿šù˜à£>ê£øê¯þj^˜ãÇséÒ%.^¼ÈñãÇøèþh¾æk¾€Ïú¬Ïâ³?û³yAn½õVò‡ðVoõVüôOÿ4ôÚ¯ýÚüÎïü¶ynŸýÙŸÍç|ÎçðR/õRüõ_ÿ5/ÈGôGó5_ó5|×w}ïýÞïÍs{×y~û·€‹/rüøqþ§‘Äóó~¯ýã\õ?×ñ‡ßM[õìßqŠ«þçú„¯zMõÒgø…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßüÈðR/õR<úÑæªÿù~à~€Wz¥Wâá8WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàÞé8}ú4WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ú_@ÿ/õR/Åw÷wóÒ/ýÒ<Ðk¿ökó;¿ó;üÖoý¯ýÚ¯Í óÞïýÞ|Ï÷|?õS?Å[¿õ[ðÚ¯ýÚüÎïüõWÅK¿ôKóÂ?~œK—.`›zí×~m~çw~Û<·ÏþìÏæs>çsø¨ú(¾ú«¿šä¯ÿú¯y™—yÞê­ÞŠŸþéŸæn½õVò‡ð^ïõ^|÷w7ÿvwwùœÏù¾û»¿›ÝÝ]þ­Þﵜ«þç:þð»i«žý;NqÕÿ\ŸðU¯É£^ú ¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUÿ HâEõZ¯õZ¼ôK¿4¯ýÚ¯Í[¿õ[óü¼Ì˼ ý× €mþ%ŸýÙŸÍç|ÎçðYŸõY|ög6’¸ßgögó/ùîïþnn½õVžþô§óà?˜û½ök¿6¿ó;¿€mžÛgögó9Ÿó9|Ög}ŸýÙŸÍ óÒ/ýÒüÍßü OúÓyðƒÌý¾ú«¿šù˜à§~ê§xë·~kþ+|ôG4_ó5_ÿ×û½ösÕÿ\Ç~7mÕ³Ç)®úŸë¾ê5yÔKŸá~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêIÜï·~ë·xí×~mþ=$q?ÛüK~û·›×y×à³>ë³øìÏþl$ñoõ[¿õ[¼ök¿6÷{í×~m~çw~Û<·ÏþìÏæs>çsø©Ÿú)Þú­ßšæ«¿ú«ù˜ù¾ê«¾Šþèæ~yÈC¸õÖ[yЃÄ­·ÞÊ•'N°»»Ë¿×û½ösÕÿ\Ç~7mÕ³Ç)®úŸë¾ê5yÔKŸá~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêIÜï·~ë·xí×~mþ=$q?ÛüK~û·›×y×à³>ë³øìÏþl$pìØ1^ú¥_š¯þê¯æ¥_ú¥¹ßk¿ökó;¿ó;Øæ¹}ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ¼0»»»œ8q€—~é—æ¯þê¯øë¿þk^æe^€ú¨â«¿ú«ù¯rüøq.]ºÄ¿×û½ösÕÿ\Ç~7mÕ³Ç)®úŸë¾ê5yÔKŸá~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêIÜï·~ë·xí×~mþ=üàóŒg<ÛüK>û³?›ÏùœÏà³>ë³øìÏþl$q?Ûü{¼ök¿6¿ó;¿€mžÛgögó9Ÿó9üÖoý¯ýگͿä½ßû½ùžïùþê¯þŠ—~é—æ£?ú£ùš¯ùžþô§óà?˜ÿ*ýÑÍ×|Í×ðïõ~¯ýã\õ?×ñ‡ßM[õìßqŠ«þçú„¯zMõÒgø…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßüÈðR/õR<úÑæªÿù~à~€Wz¥Wâá8WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàÞé8}ú4WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ú_@÷û­ßú-^ûµ_›×~í׿w~çwø­ßú-^ûµ_›æ½ßû½ùžïù~ê§~Š·~ë·à¥_ú¥ù›¿ùžþô§óà?˜«×~í׿w~çw°ÍsûìÏþl>çs>€ßú­ßâµ_ûµù—üôOÿ4oó6oÀG}ÔGñÕ_ýÕ<ä!áÖ[oå¥^ê¥øë¿þkþ«}ôG4ßýÝßÍ¥K—ø·z¿×þq®úŸëøÃ玲zöï8ÅUÿs}ÂW½&zé3üÂ/ü×^{-/ÿò/ÏUÿóýÌÏü zЃxé—~i®úŸïG~äGx©—z)ýèGsÕÿ|?ð?À+½Ò+ñð‡?œ«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7|Ã7ðNïôNœ>}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUWý/ ‰ûýÖoý¯ýگͿÇWõWó1ó1|ÔG}_ýÕ_Í sâÄ vwwxúӟ΃ü`>ú£?š¯ùš¯à³>ë³øìÏþl^ÝÝ]ò‡püøqüàó[¿õ[<Ðk¿ökó;¿ó;Øæ¹}ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ¼(üàóŒg<ƒ—~é—æ»¾ë»x™—y¾ë»¾‹÷~ï÷æ2Içsø­ßú-^ûµ_›ÅGôGó5_ó5¼×{½ßó=ßÀÅ‹9~ü8ÿ“Iâùy¿×þq®úŸëøÃ玲zöï8ÅUÿs}ÂW½&zé3üÂ/ü×^{-/ÿò/ÏUÿóýÌÏü zЃxé—~i®úŸïG~äGx©—z)ýèGsÕÿ|?ð?À+½Ò+ñð‡?œ«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7|Ã7ðNïôNœ>}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUWý/ ‰ûýÖoý¯ýگͿ×GôGó5_ó5?~œŸú©Ÿâµ_ûµ¹ßîî._ó5_ÃgögpìØ1~û·›—~é—æÞû½ß›ïùžïàøñãüÔOý¯ýÚ¯Íývwwùš¯ù>û³?›ûýÕ_ý/ýÒ/ͽök¿6¿ó;¿ÀgögóVoõV¼ôK¿4ŸýÙŸÍç|Îçð[¿õ[¼ök¿6/Š[o½•‡<ä!<Ð{½×{ñÝßýÝüO'‰ççý^ûǹê®ã¿›¶êÙ¿ãWýÏõ _õš<ê¥Ïð ¿ð \{íµ¼üË¿û³?›ÏùœÏà·~ë·xí×~m^T/ýÒ/ÍßüÍßp¿Ÿú©Ÿâ­ßú­ùŸNÏÏû½ösÕÿ\Ç~7mÕ³Ç)®úŸë¾ê5yÔKŸá~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêIÜï·~ë·xí×~mþ£|ög6_ýÕ_Í¥K—x~^ëµ^‹¯þê¯æ¥_ú¥ya>û³?›¯þê¯æÒ¥Kû³?›ÏùœÏà·~ë·xí×~m^TßýÝßÍû¼Ïûð =ˆ[o½•ÿ $ñü¼ßkÿ8WýÏuüáwÓV=ûwœâªÿ¹>á«^“G½ô~á~k¯½–—ù—çªÿù~æg~†=èA¼ôK¿4WýÏ÷#?ò#¼ÔK½~ô£¹ê¾øà•^é•xøÃÎUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áx§wz'NŸ>ÍUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«þøíßþmî÷Ò/ýÒ?~œÿH»»»üôOÿ4·Þz+ý×̓ü`Ž?Î[¿õ[óÒ/ýÒ¼¨vwwùéŸþin½õVþú¯ÿšãÇóà?˜—~é—æ­ßú­yQüôOÿ4ý× Àƒü`Þú­ßšãÇsë­·rë­·ðÒ/ýÒ?~œÕ_ÿõ_ó2/ó2|ÔG}_ýÕ_Íÿ’x~Þﵜ«þç:þð»i«žý;NqÕÿ\ŸðU¯É£^ú ¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUWýŸ÷ÙŸýÙ|Îç|OúÓyðƒÌÿ’x~Þﵜ«þç:þð»i«žý;NqÕÿ\ŸðU¯É£^ú ¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUWýŸ÷‡<„[o½•×z­×â·û·ùßBÏÏû½ösÕÿ\Ç~7mÕ³Ç)®úŸë¾ê5yÔKŸá~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêªÿÓÞç}Þ‡ïþîï໾ë»xï÷~oþ·Äóó~¯ýã\õ?×ñ‡ßM[õìßqŠ«þçú„¯zMõÒgø…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßüÈðR/õR<úÑæªÿù~à~€Wz¥Wâá8WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oø†oàÞé8}ú4WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêÿ”¿þë¿æ}Þç}8~ü8·Þz+·Þz+/õR/Å_ÿõ_ó¿‰$žŸ÷{íçªÿ¹Ž?ünÚªgÿŽS\õ?×'|Õkò¨—>Ã/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUÿçHâŽ;ÆoÿöoóÒ/ýÒüo"‰ççý^ûǹê®ã¿›¶êÙ¿ãWýÏõ _õš<ê¥Ïð ¿ð \{íµ¼üË¿Ã/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰ççý^ûǹê®ã¿›¶êÙ¿ãWýÏõ _õš<ê¥Ïð ¿ð \{íµ¼üË¿}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]uÕÿP’x~Þﵜ«þç:þð»i«žý;NqÕÿ\ŸðU¯É£^ú ¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçû‘ù^ê¥^ŠG?úÑ\õ?ßüÀðJ¯ôJ<üáçªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ ßð ¼Ó;½§OŸæªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žŸ÷{íçªÿ¹Ž?ünÚªgÿŽS\õ?×'|Õkò¨—>Ã/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~äG~„—z©—âÑ~4WýÏ÷?ð¼Ò+½øÃ¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}Ã7|ïôNïÄéÓ§¹ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ+¿ò+;vŒW~åWæªÿù~á~k¯½–—ù—çªÿù~æg~†=èA¼ôK¿4WýÏ÷#?ò#¼ÔK½~ô£¹ê¾øà•^é•xøÃÎUÿó}Ï÷|¯õZ¯Åƒü`®úŸï;¾ã;x£7z#nºé&®úŸï¾áx§wz'NŸ>ÍUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I}š«þçûš¯ùÞë½Þ‹ãÇsÕUW]uÕs@¶ÍUW]uÕÿP’x~lsÕÿ\¿ò+¿Â±cÇxåW~e®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK½ÔKñèG?š«þçûø^é•^‰‡?üá\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾wz§wâôéÓ\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçú•_ùvvvx•Wy®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ò#?ÂK¾äKò˜Ç<†«þçûø^ñ_‘G<â\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾á¾w|ÇwäÌ™3\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóó~¯ýã\õ?×ñ‡ßM[uìßqš«þç;ñÈ»æÜu’«þç;ùè;XïnrxÏ ®úŸïô‹ßÆá=ÇYžÛáªÿùμä3Ø¿ã$« Û\õ?ß5/ýt.Ýz ëÝM®úŸïÚ—}ŸrÃÞWýÏwÝ+>™ó ãÑŒ«þç»þ•ŸÈÙ¿~Óªç¿Ê·ÿÖÛqÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\õ?×ñ‡ßM[uìßqš«þç;ñÈ»æÜu’«þç;ùè;XïnrxÏ ®úŸïô‹ßÆá=ÇYžÛáªÿùμä3Ø¿ã$« Û\õ?ß5/ýt.Ýz ëÝM®úŸïÚ—}ŸrÃÞWýÏwÝ+>™ó ãÑŒ«þç»þ•ŸÈÙ¿~Óªç¿Ê·ÿÖÛqÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\õ?×ñ‡ßM[uìßqš«þç;ñÈ»æÜu’«þç;ùè;XïnrxÏ ®úŸïô‹ßÆá=ÇYžÛáªÿùμä3Ø¿ã$« Û\õ?ß5/ýt.Ýz ëÝM®úŸïÚ—}ŸrÃÞWýÏwÝ+>™ó ãÑŒ«þç»þ•ŸÈÙ¿~Óªç¿Ê·ÿÖÛqÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\õ?×ñ‡ßM[uìßqš«þç;ñÈ»æÜu’«þç;ùè;XïnrxÏ ®úŸïô‹ßÆá=ÇYžÛáªÿùμä3Ø¿ã$« Û\õ?ß5/ýt.Ýz ëÝM®úŸïÚ—}ŸrÃÞWýÏwÝ+>™ó ãÑŒ«þç»þ•ŸÈÙ¿~Óªç¿Ê·ÿÖÛqÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\õ?×ñ‡ßM[uìßqš«þç;ñÈ»æÜu’«þç;ùè;XïnrxÏ ®úŸïô‹ßÆá=ÇYžÛáªÿùμä3Ø¿ã$« Û\õ?ß5/ýt.Ýz ëÝM®úŸïÚ—}ŸrÃÞWýÏwÝ+>™ó ãÑŒ«þç»þ•ŸÈÙ¿~Óªç¿Ê·ÿÖÛqÕUW]õ¿²m®ºê¿Ùç|Îçð =ˆ÷~ï÷檫î'‰ççý^ûǹê®ã¿›¶êØ¿ã4WýÏwâ‘w1Ì9¸ë$WýÏwòÑw°ÞÝäðž\õ?ßé¿Ã{޳<·ÃUÿóyÉg°ÇIV¶¹ê¾k^úé\ºõÖ»›\õ?ßµ/û4.>å:†½ ®úŸïºW|2çÿþÆ£WýÏwý+?‘³ý¦UÏ•oÿ­·ãª«®ºêdÛ\uÕ3I¼Ök½¿ýÛ¿ÍUWÝOÏÏû½ösÕÿ\Ç~7mÕ±Çi®úŸïÄ#ïb<˜sp×I®úŸïä£ï`½»Éá='¸ê¾Ó/~‡÷gyn‡«þç;ó’Ï`ÿŽ“¬.lsÕÿ|×¼ôÓ¹të5¬w7¹ê¾k_öi\|Êu {\õ?ßu¯ødÎÿý-ŒG3®úŸïúW~"gÿú!L«žÿ*ßþ[oÇUW]uÕÿȶ¹êªÿf’x­×z-~û·›«®ºŸ$žŸ÷{íçªÿ¹Ž?ünÚªcÿŽÓ\õ?߉GÞÅx0çஓ\õ?ßÉGßÁzw“Ã{NpÕÿ|§_ü6ï9ÎòÜWýÏwæ%ŸÁþ'Y]Øæªÿù®yé§séÖkXïnrÕÿ|×¾ìÓ¸ø”ëö6¸ê¾ë^ñÉœÿû[f\õ?ßõ¯üDÎþõC˜V=ÿU¾ý·ÞŽ«®ºêªÿmsÕUÿÍ^ûµ_€—~é—æ«¿ú«¹êªûIâùy¿×þq®úŸëøÃ玲:öï8ÍUÿóxä]Œsî:ÉUÿó|ô¬w79¼çWýÏwúÅoãðžã,ÏípÕÿ|g^òìßq’Õ…m®úŸïš—~:—n½†õî&WýÏwíË>‹O¹Žaoƒ«þç»îŸÌù¿¿…ñhÆUÿó]ÿÊOäì_?„iÕó_åÛëí¸êª«®ú_Ù6W]uÕUÿCIâùy¿×þq®úŸëøÃ玲:öï8ÍUÿóxä]Œsî:ÉUÿó|ô¬w79¼çWýÏwúÅoãðžã,ÏípÕÿ|g^òìßq’Õ…m®úŸïš—~:—n½†õî&WýÏwíË>‹O¹Žaoƒ«þç»îŸÌù¿¿…ñhÆUÿó]ÿÊOäì_?„iÕó_åÛëí¸êª«®ú_Ù6W]uÕUÿCIâùy¿×þq®úŸëøÃî¡ •ýÛOsÕÿ|'yãÁœƒ»NrÕÿ|'}ëÝMï9ÁUÿó~±Û8¼ï8˳;\õ?ß™—xûwdu~›«þç»æ¥ŸÎ¥[¯a½»ÉUÿó]û²OãâS®cØÛàªÿù®{…'sþq·0θê¾ë_ù‰œýë‡0­zþ«|ûo½W]uÕUÿ Ûæª«þ~çw~€cÇŽñÒ/ýÒ|Ï÷|·Þz+ý×ÍK¿ôKóà?˜÷z¯÷â…ùßùŽ;ÆK¿ôKó@¿ó;¿Àƒô üà³»»ËÏüÌÏpë­·ò×ý×¼ôK¿4~ðƒy¯÷z/þ#íîîò;¿ó;üõ_ÿ5ý×ÍñãÇyðƒÌK¿ôKóZ¯õZ?~œçö;¿ó;Üïµ^ëµø—üõ_ÿ5—.]àµ^뵸õÖ[yÆ3žÀk½Ökpë­·ò3?ó3Üzë­Üzë­¼ôK¿4/ýÒ/Í[½Õ[ñ¢ú™Ÿùþú¯ÿš[o½•ÝÝ]^ú¥_š—~é—æ­Þê­øŸLÏÏû½ösÕÿ\Çvm¨ìß~š«þç;ñÈ»æÜu’«þç;ùè;XïnrxÏ ®úŸïô‹ÝÆá}ÇYžÝáªÿùμÄ3Ø¿ë$«óÛ\õ?ß5/ýt.Ýz ëÝM®úŸïÚ—}ŸrÃÞWýÏwÝ+<™ó»…ñpÆUÿó]ÿÊOäì_?„iÕó_åÛëí¸êª«®ú_Ù6W]õo$ €×z­×â«¿ú«y›·yn½õVžÛƒü`¾ê«¾Š·~ë·æù‘Àk½ÖkñÛ¿ýÛ<$>ë³>‹·~ë·æu^çuØÝÝå¹=øÁæ§~ê§xé—~iþ½¾û»¿›ù˜aww—ççøñã|ôG4ŸõYŸÅ=øÁæÏxOúÓyðƒÌ ²»»Ë‰'x«·z+~ú§€ÏþìÏæs>çs°Íç|ÎçðÙŸýÙúÖ»›Þs‚«þç;ýb·qxßq–gw¸ê¾3/ñ öï:Éêü6WýÏwÍK?K·^Ãzw“«þç»öeŸÆÅ§\ǰ·ÁUÿó]÷ Oæüãna<œqÕÿ|׿ò9û×aZõüWùößz;®ºêª«þ@¶ÍUWýIàÁ~0»»»ìîîrìØ1Þû½ß›ãÇóÓ?ýÓüÍßü ÷û®ïú.Þû½ß›ç& €×z­×â·û·y I¼õ[¿5¿ýÛ¿Íîî.zЃxë·~kŽ?ÎOÿôOó7ó7?~œŸú©Ÿâµ_ûµù·úéŸþiÞæmÞ†û½ÔK½Ç`ww—¿ù›¿á~ŸõYŸÅgögs¿ÏþìÏæs>çsø¬Ïú,>û³?›ä«¿ú«ù˜ù¾ë»¾‹÷~ï÷à³?û³ùœÏùÞû½ß›ïþîïæ~¯õZ¯Å­·ÞÊ3žñ îwüøqžþô§süøqžÛWõWó1ó1ÜïAz~ðƒøë¿þk.]ºÀñãÇù­ßú-^ú¥_šÿi$ñü¼ßkÿ8WýÏuüa÷ІÊþí§¹ê¾¼‹ñ`ÎÁ]'¹ê¾“¾ƒõî&‡÷œàªÿùN¿ØmÞwœåÙ®úŸïÌK<ƒý»N²:¿ÍUÿó]óÒOçÒ­×°ÞÝäªÿù®}Ù§qñ)×1ìmpÕÿ|׽“9ÿ¸[g\õ?ßõ¯üDÎþõC˜V=ÿU¾ý·ÞŽ«®ºêªÿmsÕUÿF’x ÷z¯÷â«¿ú«9~ü8÷ûîïþnÞç}Þ€ãÇóô§?ãÇó@’x­×z-~û·›’Ľ×{½ßýÝßÍ}õW5ó1ÀñãÇyúÓŸÎñãÇù·xÈC­·ÞʱcÇøéŸþi^ûµ_›úéŸþiÞæmÞ€ãÇóô§?ãÇpë­·ò‡<€?øÁ<ýéOçy™—yþú¯ÿšcÇŽ±»»Ëý>û³?›ÏùœÏá~zЃøê¯þjÞú­ßšûýöoÿ6oýÖoÍ¥K—øª¯ú*>ú£?šúíßþm^çu^‡û}ÕW}ýÑÍývwwùèþh¾ç{¾€—~é—æ¯þê¯øŸFÏÏû½ösÕÿ\Çvm¨ìß~š«þç;ñÈ»æÜu’«þç;ùè;XïnrxÏ ®úŸïô‹ÝÆá}ÇYžÝáªÿùμÄ3Ø¿ë$«óÛ\õ?ß5/ýt.Ýz ëÝM®úŸïÚ—}ŸrÃÞWýÏwÝ+<™ó»…ñpÆUÿó]ÿÊOäì_?„iÕó_åÛëí¸êª«®ú_Ù6W]õo$‰û½ÔK½ý×ÍóóÙŸýÙ|Îç|ïõ^ïÅw÷wó@’x­×z-~û·›’Äý^ëµ^‹ßþíßæùùèþh¾æk¾€Ïú¬Ïâ³?û³ù×úë¿þk^æe^€ú¨â«¿ú«y~>ú£?š¯ùš¯à§~ê§xë·~kî÷Ú¯ýÚüÎïüõWÅK¿ôKóÜþú¯ÿš—y™—à½Þë½øîïþnî÷ÙŸýÙ|Îç|ÇŽã¯ÿú¯yðƒÌsûîïþnÞç}Þ€×z­×â·û·y ×y×á·û·ø©Ÿú)Þú­ßšççµ_ûµùßùþê¯þŠ—~é—æIïó>|ÔG}_ýÕ_Ísûèþh¾æk¾€ßú­ßâµ_ûµ¹ßgögó9Ÿó9¼×{½ßýÝßÍ " €×z­×â·û·¹ß­·ÞÊCò^ê¥^Š¿þë¿æùîïþn>û³?›?øÁ|ôG4oýÖoÍÿ$’x~Þﵜ«þç:þ°{hCeÿöÓ\õ?߉GÞÅx0çஓ\õ?ßÉGßÁzw“Ã{NpÕÿ|§_ì6ï;ÎòìWýÏwæ%žÁþ]'Yßæªÿù®yé§séÖkXïnrÕÿ|×¾ìÓ¸ø”ëö6¸ê¾ë^áÉœÜ-Œ‡3®úŸïúW~"gÿú!L«žÿ*ßþ[oÇUW]uÕÿȶ¹êª#I;vŒÝÝ]^˜·~ë·æg~ægø«¿ú+^ú¥_šûIàµ^ëµøíßþmH/õR/Å_ÿõ_ó¼ök¿6¿ó;¿ÀÓŸþtüàó¯uüøq.]ºÀk¿ökóÑýѼÕ[½ÿÇçÒ¥K?~œ‹/òÜò‡pë­·ò =ˆ[o½•úìÏþl>çs>€ïú®ïâ½ßû½yA$q?ÛÜû»yŸ÷y>ë³>‹ÏþìÏæŠÝÝ]>çs>‡ïþîïfww—«÷{íçªÿ¹Ž?ìÚPÙ¿ý4WýÏwâ‘w1Ì9¸ë$WýÏwòÑw°ÞÝäðž\õ?ßé»Ãû޳<»ÃUÿóy‰g°×IVç·¹ê¾k^úé\ºõÖ»›\õ?ßµ/û4.>å:†½ ®úŸïºWx2çw ãጫþç»þ•ŸÈÙ¿~Óªç¿Ê·ÿÖÛqÕUW]õ¿²m®ºêßH¯õZ¯ÅoÿöoóÂ|ög6Ÿó9ŸÀoýÖoñÚ¯ýÚÜO¯õZ¯Åoÿöoó@’x¯÷z/¾û»¿›æ£?ú£ùš¯ù~ë·~‹×~í׿Ö[oå{¾ç{xaÞë½Þ‹?øÁ|õW5ó1Ãs{í×~mÞú­ßš×z­×â¥_ú¥yaÞû½ß›ïùžïà§~ê§xë·~kî÷Ó?ýÓ¼ÍÛ¼ ŸõYŸÅgögó@ŸýÙŸÍç|Îçð[¿õ[¼ök¿6/ˆ$îg›û}ög6Ÿó9ŸÀOýÔOñÖoýÖüOñÑýÑ|Í×| ÿ^ï÷Ú?ÎUÿsØ=´¡²ûi®úŸïÄ#ïb<˜sp×I®úŸïä£ï`½»Éá='¸ê¾Ó/v‡÷gyv‡«þç;óÏ`ÿ®“¬ÎosÕÿ|×¼ôÓ¹të5¬w7¹ê¾k_öi\|Êu {\õ?ßu¯ðdÎ?îÆÃWýÏwý+?‘³ý¦UÏ•oÿ­·ãª«®ºêdÛ\uÕ¿‘$Þê­ÞŠŸþéŸæ…ùìÏþl>çs>€Ïú¬Ïâ³?û³¹Ÿ$^ëµ^‹ßþíßæ$ðYŸõY|ög6/Ìgögó9Ÿó9üÖoý¯ýÚ¯Íoÿöoó:¯ó:¼0¿õ[¿Åk¿öks¿ÏþìÏæ«¿ú«¹téÏσü`Þû½ß›Ïú¬Ïâùùë¿þk^æe^€÷z¯÷⻿û»¹ß{¿÷{ó=ßó=<ýéOçÁ~0ôÙŸýÙ|Îç|¿õ[¿Åk¿ökó‚Hâ~¶¹ßgögó9Ÿó9üÖoý¯ýÚ¯Íÿ'Nœ`ww—¯÷{íçªÿ¹Ž?ìÚPÙ¿ý4WýÏwâ‘w1Ì9¸ë$WýÏwòÑw°ÞÝäðž\õ?ßé»Ãû޳<»ÃUÿóy‰g°×IVç·¹ê¾k^úé\ºõÖ»›\õ?ßµ/û4.>å:†½ ®úŸïºWx2çw ãጫþç»þ•ŸÈÙ¿~Óªç¿Ê·ÿÖÛqÕUW]õ¿²m®ºêßHïõ^ïÅw÷wóÂ|ög6Ÿó9ŸÀW}ÕWñÑýÑÜO¯õZ¯Åoÿöoó@’ø¬Ïú,>û³?›æ³?û³ùœÏù~ë·~‹×~í׿·û·y×y^˜ßú­ßâµ_ûµy ÝÝ]~ú§šŸþéŸæg~ægx~^ú¥_šßú­ßâøñã<·?øÁ<ãÏàâÅ‹?~œÝÝ]ò‡°»»Ëk½ÖkñÛ¿ýÛ<·ÏþìÏæs>çsø­ßú-^ûµ_›D÷³Íý>ú£?š¯ùš¯à·~ë·xí×~mþ§8~ü8—.]âßëý^ûǹê®ã»‡6Töo?ÍUÿóxä]Œsî:ÉUÿó|ô¬w79¼çWýÏwúÅnãð¾ã,ÏîpÕÿ|g^âìßu’Õùm®úŸïš—~:—n½†õî&WýÏwíË>‹O¹Žaoƒ«þç»îžÌùÇÝÂx8ãªÿù®å'rö¯´êù¯òí¿õv\uÕUWý/€l›«®ú7’Àk½ÖkñÛ¿ýÛ¼0ŸýÙŸÍç|Îçð[¿õ[¼ök¿6÷“Àk½ÖkñÛ¿ýÛ<$>ê£>Нþê¯æ…yï÷~o¾ç{¾€ßú­ßâµ_ûµ¹õÖ[ùîïþn^˜÷~ï÷æÁ~0/ÌoÿöoóÓ?ýÓüôOÿ4ÏxÆ3¸ß{½×{ñÝßýÝ<·¯þê¯æc>æcø®ïú.Þû½ß›ïþîïæ}Þç}ø®ïú.Þû½ß›çöÙŸýÙ|Îç|¿õ[¿Åk¿ökó‚Hâ~¶¹ßgögó9Ÿó9üÔOýoýÖoÍÿýÑÍ×|Í×ðïõ~¯ýã\õ?×ñ‡ÝC*û·ŸæªÿùN<ò.ƃ9wäªÿùN>úÖ»›Þs‚«þç;ýb·qxßq–gw¸ê¾3/ñ öï:Éêü6WýÏwÍK?K·^Ãzw“«þç»öeŸÆÅ§\ǰ·ÁUÿó]÷ Oæüãna<œqÕÿ|׿ò9û×aZõüWùößz;®ºêª«þ@¶ÍUWýIàÁ~0OúÓya^ûµ_›ßùßà¯þê¯xé—~iî' €×z­×â·û·y I¼Ök½¿ýÛ¿Í ó2/ó2üõ_ÿ5/^äøñãügøîïþnÞç}Þ‡ûÙæ¹Ýzë­<ä!à­Þê­øéŸþiÞú­ßšŸù™ŸáرcÜzë­?~œçöÙŸýÙ|Îç|¿õ[¿Åk¿ökó‚Hâ~¶¹ßWõWó1ó1|Ög}ŸýÙŸÍ ²»»ËÛ¼ÍÛðÒ/ýÒ¼ÔK½ïýÞïͶþèæ»¿û»¹téÿVï÷Ú?ÎUÿsØ=´¡²ûi®úŸïÄ#ïb<˜sp×I®úŸïä£ï`½»Éá='¸ê¾Ó/v‡÷gyv‡«þç;óÏ`ÿ®“¬ÎosÕÿ|×¼ôÓ¹të5¬w7¹ê¾k_öi\|Êu {\õ?ßu¯ðdÎ?îÆÃWýÏwý+?‘³ý¦UÏ•oÿ­·ãª«®ºêdÛ\uÕ¿‘$î÷ô§??øÁû³?›ÏùœÏà·~ë·xí×~m^IÜÏ6ôÒ/ýÒüÍßü ?õS?Å[¿õ[óü¼Î뼿ýÛ¿ ÀoýÖoñÚ¯ýÚüO"‰ççý^ûǹê®ã»‡6Töo?ÍUÿóxä]Œsî:ÉUÿó|ô¬w79¼çWýÏwúÅnãð¾ã,ÏîpÕÿ|g^âìßu’Õùm®úŸïš—~:—n½†õî&WýÏwíË>‹O¹Žaoƒ«þç»îžÌùÇÝÂx8ãªÿù®å'rö¯´êù¯òí¿õv\uÕUWý/€l›«®ú7’Ľ÷{¿7_õU_Åñãǹßç|ÎçðÙŸýÙ<èAâ¯ÿú¯9~ü8$ €×z­×â·û·y I<ÐgögóYŸõYÜoww—ù˜á»¿û»x©—z)þú¯ÿš‹ÝÝ]üàséÒ%>û³?›ú¨âøñãÜoww—÷yŸ÷á§ú§ø¬Ïú,>û³?›äøñã\ºt‰û=èAâÖ[oåùìÏþl>çs>€ßú­ßâµ_ûµyA$q?Û<Ðoÿöoó:¯ó:?~œïú®ïâ­ßú­¹ßîî.ó1Ãw÷wðZ¯õZüöoÿ6ÿÓHâùy¿×þq®úŸëøÃî¡ •ýÛOsÕÿ|'yãÁœƒ»NrÕÿ|'}ëÝMï9ÁUÿó~±Û8¼ï8˳;\õ?ß™—xûwdu~›«þç»æ¥ŸÎ¥[¯a½»ÉUÿó]û²OãâS®cØÛàªÿù®{…'sþq·0θê¾ë_ù‰œýë‡0­zþ«|ûo½W]uÕUÿ Ûæª«þ$q¿cÇŽqéÒ%Ž?ÎK¿ôKð×ý×ìîîpìØ1~û·›—~é—æ¹Iàµ^ëµøíßþmHÇŽãÒ¥K?~œ—~é—à¯ÿú¯ÙÝÝàAz?ýÓ?ÍK¿ôKóoõÓ?ýÓ¼ÍÛ¼ ÷;~ü8/ýÒ/Íýþú¯ÿšÝÝ]^ê¥^Šßþíßæøñã¼ ýÑÍ×|Í×p¿Ïú¬Ïâ³?û³yA>û³?›ÏùœÏà·~ë·xí×~m^IÜÏ6Ïí«¿ú«ù˜ùî÷à?˜?øÁüõ_ÿ5»»»;vŒ¿þë¿æÁ~0ÿÓHâùy¿×þq®úŸëøÃî¡ •ýÛOsÕÿ|'yãÁœƒ»NrÕÿ|'}ëÝMï9ÁUÿó~±Û8¼ï8˳;\õ?ß™—xûwdu~›«þç»æ¥ŸÎ¥[¯a½»ÉUÿó]û²OãâS®cØÛàªÿù®{…'sþq·0θê¾ë_ù‰œýë‡0­zþ«|ûo½W]uÕUÿ Ûæª«þ$q¿¿ú«¿â­ßú­yÆ3žÁs{­×z-¾ú«¿š—~é—æù‘Àk½ÖkñÛ¿ýÛ<$^ëµ^‹þèæ½ßû½¹téÏíµ^ëµøéŸþiŽ?ο×OÿôOóÞïýÞ\ºt‰ä½Þë½øê¯þjŽ?Î ó×ý×¼Ì˼ ÷{úӟ΃ü`^ÏþìÏæs>çsø­ßú-^ûµ_›D÷³ÍóóÓ?ýÓ|ôG4ÏxÆ3x~^ëµ^‹ïþîïæÁ~0ÿIâùy¿×þq®úŸëøÃî¡ •ýÛOsÕÿ|'yãÁœƒ»NrÕÿ|'}ëÝMï9ÁUÿó~±Û8¼ï8˳;\õ?ß™—xûwdu~›«þç»æ¥ŸÎ¥[¯a½»ÉUÿó]û²OãâS®cØÛàªÿù®{…'sþq·0θê¾ë_ù‰œýë‡0­zþ«|ûo½W]uÕUÿ Ûæª«þ$q?ÛìîîòÓ?ýÓüõ_ÿ5»»»<øÁæµ_ûµyí×~m^˜ßþíßàøñã¼ôK¿4$ €×z­×â·û·ÙÝÝå§ú§ùë¿þkvwwyðƒÌ{¿÷{óà?˜ÿh?ýÓ?Í_ÿõ_së­·rë­·òÚ¯ýÚ?~œ·~ë·æÁ~0/*I¼Ök½¿ýÛ¿Í së­·rë­·ðÒ/ýÒ?~œä·û·¹ßk¿ökó‚ìîîòÛ¿ýÛüõ_ÿ5ý×ÍñãÇyðƒÌ[¿õ[óÒ/ýÒüO&‰ççý^ûǹê®ã»‡6Töo?ÍUÿóxä]Œsî:ÉUÿó|ô¬w79¼çWýÏwúÅnãð¾ã,ÏîpÕÿ|g^âìßu’Õùm®úŸïš—~:—n½†õî&WýÏwíË>‹O¹Žaoƒ«þç»îžÌùÇÝÂx8ãªÿù®å'rö¯´êù¯òí¿õv\uÕUWý/€l›«®ú7’ÄýlóŸA¯õZ¯Åoÿöoó¿ÍOÿôOó6oó6|×w}ïýÞïÍU/:Iå:†½ ®úŸïºWx2çw ãጫþç»þ•ŸÈÙ¿~Óªç¿Ê·ÿÖÛqÕUW]õ¿²m®ºêßH÷³ÍI¼Ök½¿ýÛ¿Íÿ&ßýÝßÍû¼ÏûðYŸõY|ög6WýëHâùy¿×þq®úŸëøÃî¡ •ýÛOsÕÿ|'yãÁœƒ»NrÕÿ|'}ëÝMï9ÁUÿó~±Û8¼ï8˳;\õ?ß™—xûwdu~›«þç»æ¥ŸÎ¥[¯a½»ÉUÿó]û²OãâS®cØÛàªÿù®{…'sþq·0θê¾ë_ù‰œýë‡0­zþ«|ûo½W]uÕUÿ Ûæª«þ$q?ÛügÀk½ÖkñÛ¿ýÛüO÷2/ó2?~œÝÝ]þú¯ÿ€cÇŽqë­·rüøq®úבÄóó~¯ýã\õ?×ñ‡ÝC*û·ŸæªÿùN<ò.ƃ9wäªÿùN>úÖ»›Þs‚«þç;ýb·qxßq–gw¸ê¾3/ñ öï:Éêü6WýÏwÍK?K·^Ãzw“«þç»öeŸÆÅ§\ǰ·ÁUÿó]÷ Oæüãna<œqÕÿ|׿ò9û×aZõüWùößz;®ºêª«þ@¶ÍUWýIâ~¶ùÏ €×z­×â·û·ùŸî¥_ú¥ù›¿ùè§~ê§xë·~k®úדÄóó~¯ýã\õ?×ñ‡ÝC*û·ŸæªÿùN<ò.ƃ9wäªÿùN>úÖ»›Þs‚«þç;ýb·qxßq–gw¸ê¾3/ñ öï:Éêü6WýÏwÍK?K·^Ãzw“«þç»öeŸÆÅ§\ǰ·ÁUÿó]÷ Oæüãna<œqÕÿ|׿ò9û×aZõüWùößz;®ºêª«þ@¶ÍUWý½ök¿6÷ûíßþmþ3¼ök¿6/ýÒ/ÍWõWó?ÝGôGó5_ó5¼ÔK½ŸýÙŸÍ[¿õ[sÕ¿$žŸ÷{íçªÿ¹Ž?ìÚPÙ¿ý4WýÏwâ‘w1Ì9¸ë$WýÏwòÑw°ÞÝäðž\õ?ßé»Ãû޳<»ÃUÿóy‰g°×IVç·¹ê¾k^úé\ºõÖ»›\õ?ßµ/û4.>å:†½ ®úŸïºWx2çw ãጫþç»þ•ŸÈÙ¿~Óªç¿Ê·ÿÖÛqÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\õ?×ñ‡ÝC*û·ŸæªÿùN<ò.ƃ9wäªÿùN>úÖ»›Þs‚«þç;ýb·qxßq–gw¸ê¾3/ñ öï:Éêü6WýÏwÍK?K·^Ãzw“«þç»öeŸÆÅ§\ǰ·ÁUÿó]÷ Oæüãna<œqÕÿ|׿ò9û×aZõüWùößz;®ºêª«þ@¶ÍUW]uÕÿP’x~Þﵜ«þç:þ°{hCeÿöÓ\õ?߉GÞÅx0çஓ\õ?ßÉGßÁzw“Ã{NpÕÿ|§_ì6ï;ÎòìWýÏwæ%žÁþ]'Yßæªÿù®yé§séÖkXïnrÕÿ|×¾ìÓ¸ø”ëö6¸ê¾ë^áÉœÜ-Œ‡3®úŸïúW~"gÿú!L«žÿ*ßþ[oÇUW]uÕÿȶ¹êª«®úJÏÏû½ösÕÿ\Çvm¨ìß~š«þç;ñÈ»æÜu’«þç;ùè;XïnrxÏ ®úŸïô‹ÝÆá}ÇYžÝáªÿùμÄ3Ø¿ë$«óÛ\õ?ß5/ýt.Ýz ëÝM®úŸïÚ—}ŸrÃÞWýÏwÝ+<™ó»…ñpÆUÿó]ÿÊOäì_?„iÕó_åÛëí¸êª«®ú_Ù6W]uÕUÿCIâùy¿×þq®úŸëøÃî¡ •ýÛOsÕÿ|'yãÁœƒ»NrÕÿ|'}ëÝMï9ÁUÿó~±Û8¼ï8˳;\õ?ß™—xûwdu~›«þç»æ¥ŸÎ¥[¯a½»ÉUÿó]û²OãâS®cØÛàªÿù®{…'sþq·0θê¾ë_ù‰œýë‡0­zþ«|ûo½W]uÕUÿ Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(I’«þçûžïù^ëµ^‹?øÁ\õ?ßw|ÇwðFoôFÜtÓM\õ?ß7~ã7òöoÿö\sÍ5\õ?ß×|Í×ð^ïõ^?~œ«®ºêª«ž²m®ºêª«þ‡’Äóc›«þçúÕ_ýU¶¶¶xÕW}U®úŸï~á¸öÚkyù—y®úŸïg~ægxЃÄK¿ôKsÕÿ|?ú£?Ê‹¿ø‹óØÇ>–«þçûÁüA^þå_žG>ò‘\õ?ß÷|Ï÷ðZ¯õZ<øÁæªÿù¾ã;¾ƒ7z£7⦛nâªÿù¾ñ¿‘·û·çšk®áªÿù¾æk¾†÷z¯÷âøñã\uÕUW]õmsÕUW]õ?”$žÛ\õ?ׯþ꯲µµÅ«¾ê«rÕÿ|¿ð ¿Àµ×^ËË¿üËsÕÿ|?ó3?Ãô ^ú¥_š«þçûÑýQ^üÅ_œÇ>ö±\õ?ßþàòò/ÿò<ò‘äªÿù¾ç{¾‡×z­×âÁ~0WýÏ÷ßñ¼Ñ½7ÝtWýÏ÷ßø¼ýÛ¿=×\s WýÏ÷5_ó5¼×{½Ç窫®ºêªç€l›«®ºêªÿ¡$ñüØæªÿ¹~õW•­­-^õU_•«þçû…_ø®½öZ^þå_ž«þçû™Ÿùô ñÒ/ýÒ\õ?ßþèòâ/þâ<ö±åªÿù~ð—ù—ç‘|$WýÏ÷=ßó=¼Ök½~ðƒ¹ê¾ïøŽïàÞè¸é¦›¸ê¾oüÆoäíßþí¹æšk¸ê¾¯ùš¯á½Þë½8~ü8W]uÕUW=dÛ\uÕUWý%‰çÇ6WýÏõ«¿ú«lmmñª¯úª\õ?ß/üÂ/píµ×òò/ÿò\õ?ßÏüÌÏð =ˆ—~é—æªÿù~ôG”ñç±},WýÏ÷ƒ?øƒ¼üË¿<|ä#¹ê¾ïùžïáµ^ëµxðƒÌUÿó}Çw|oôFoÄM7ÝÄUÿó}ã7~#oÿöoÏ5×\ÃUÿó}Í×| ïõ^ïÅñãǹꪫ®ºê9 Ûæª«®ºê(Iü[<øÁæ£?ú£ù¨ú(žŸ¿þë¿æs>çsøéŸþiþ5Þú­ßšÏú¬Ïâ¥_ú¥y~¾æk¾†¯þê¯æÖ[oåEuüøq>ú£?šÏú¬ÏâùÙÝÝåc>æcøîïþnþ5^ú¥_š¯úª¯âµ_ûµy~~ú§šÏùœÏá¯ÿú¯yQ?~œ·~ë·æ«¾ê«8~ü8ÏíÏxoó6oÃßÿýß3Ž#/ª?øÁ|Ög}ïýÞïÍóó×ý×|Îç|?ýÓ?Í¿Æ[¿õ[óU_õU<øÁæùùš¯ù¾ú«¿š[o½•Õƒü`Þû½ß›Ïú¬Ïâù¹õÖ[ù˜ù~ú§š—~é—æ«¾ê«xí×~mžŸŸþéŸæs>çsøë¿þk^TÇç½ßû½ù¬Ïú,Ž?Îs{Æ3žÁÛ¼ÍÛðÿð ÃÀ‹ê¥_ú¥ù¬Ïú,Þú­ßšçç¯ÿú¯yŸ÷yþú¯ÿš·~ë·æ«¾ê«xðƒÌóó9Ÿó9|÷w7·Þz+/ª?øÁ¼÷{¿7ŸõYŸÅósë­·ò1ó1üôOÿ4ÿ¯ýÚ¯ÍW}ÕWñÒ/ýÒë³>‹—~é—æùùš¯ù¾ú«¿š[o½•ÕñãÇyï÷~o>ë³>‹ãÇóÜžñŒgðú¯ÿúÜvÛm ÃÀ‹ê¥_ú¥ù¬Ïú,Þú­ßšçç·û·ù˜ùþú¯ÿš÷~ï÷櫾ê«8~ü8ÏÏç|ÎçðÕ_ýÕìîîò¢zðƒÌGôGóQõQçs>‡¿þë¿æEuüøqÞû½ß›Ïú¬Ïâøñã<·ÝÝ]>çs>‡ïþîïfww—ÕK¿ôKóYŸõY¼õ[¿5ÏÏoÿöoó1ó1üõ_ÿ5ÿoýÖoÍW}ÕWñà?˜ççs>çsøîïþnn½õV^T~ðƒyï÷~o>ë³>‹çç÷ÿ÷yÇw|Gî¾ûnþ5^ûµ_›¯úª¯â¥_ú¥y~~ú§šù˜áÖ[oåEuüøqÞû½ß›Ïú¬Ïâøñã<·ÝÝ]>æc>†ŸþéŸfww—ÕK¿ôKóYŸõY¼õ[¿5ÏÏoÿöoó1ó1üõ_ÿ5ÿoýÖoÍw}×wqüøqžŸÏùœÏỿû»¹õÖ[yQ=øÁæ£?ú£ù¨ú(žŸßÿýßçßñ¹ûî»ù×xë·~k>ë³>‹—~é—æùùš¯ù¾ú«¿š[o½•äøñã¼÷{¿7_õU_ÅUW]uÕÿ`ȶ¹êª«®úJÿ?õS?Å[¿õ[ó@»»»¼Ì˼ ·Þz+ÿ~ðƒù«¿ú+Ž?ÎýôOÿ4oó6oÿÕg}ÖgñÙŸýÙ<·×y×á·û·ù·8~ü8õWŃü`è¯ÿú¯y™—yþ­Þú­ßšŸú©Ÿâ¹½ì˾,õWÅ¿ÕOýÔOñÖoýÖ<Ðîî.yÈCØÝÝåßâÁ~0õWÅñãÇy ¯þê¯æc>æcø·ú¬Ïú,>û³?›çö2/ó2üõ_ÿ5ÿÇç¯þê¯xðƒÌýöoÿ6¯ó:¯Ã¿Õ[¿õ[óS?õS<·—}Ù—å¯þê¯ø·ú­ßú-^ûµ_›ÚÝÝå!y»»»ü[¼ôK¿4õWÅsûê¯þj>æc>†«¯úª¯â£?ú£yn/ó2/Ã_ÿõ_óoqüøqžþô§süøqè·û·y×yþ­>ê£>Нþê¯æ¹Ýpà Ü}÷Ýü[ýÖoý¯ýÚ¯ÍÝzë­¼Ì˼ »»»ü[¼ôK¿4õWÅsûìÏþl>çs>‡«¯úª¯â£?ú£y ÝÝ]^æe^†[o½•‹ãÇóô§?ãÇó@?ýÓ?ÍÛ¼ÍÛðoõQõQ|õW5ÏíĉìîîòoõWõW¼ôK¿4të­·ò2/ó2ìîîòoñÚ¯ýÚüÖoýÏí³?û³ùœÏùþ­~ê§~Š·~ë·ævwwy™—yn½õVþ-üàóWõW?~œúéŸþiÞæmÞ†«ú¨â«¿ú«yn'Nœ`ww—«¿ú«¿â¥_ú¥y ¿þë¿æe^æeø·zí×~m~ë·~‹çöÑýÑ|Í×| ÿV?õS?Å[¿õ[ó@»»»<ä!aww—‹?øÁüÕ_ýÇç¾û»¿›÷yŸ÷áßê³>ë³øìÏþlžÛÆÆËå’‹ãÇóWõW<øÁæþú¯ÿš—y™—áßê­ßú­ù©Ÿú)žÛGôGó5_ó5ü[ýÖoý¯ýÚ¯Ííîîò‡<„ÝÝ]þ-^ú¥_š¿ú«¿â¹}õW5ó1ÿÕg}ÖgñÙŸýÙ<· –Ë%ÿÇç¯þê¯xðƒÌýöoÿ6¯ó:¯Ã¿Õ[¿õ[óS?õS<··y›·á§ú§ù·ú­ßú-^ûµ_›ºõÖ[y™—yvwwù·xé—~iþê¯þŠçöÙŸýÙ|Îç|ÿV_õU_ÅGôGóÜ666X.—ü[?~œ§?ýé?~œúíßþm^çu^‡«ú¨â«¿ú«ynoó6oÃOÿôOóoõWõW¼ôK¿4të­·ò2/ó2ìîîòoñÒ/ýÒüÕ_ýÏí³?û³ùœÏùþ­¾ê«¾ŠþèæžñŒgðð‡?œišø·8~ü8OúÓ9~ü8ôÓ?ýÓ¼ÍÛ¼ /ªú¨â«¿ú«¹êª«®ú Ù6W]uÕUÿCIâßã­Þê­øéŸþiè§ú§y›·yþ=~ë·~‹×~í׿^ûµ_›ßùßáßêÁ~0OúÓy [o½•‡<ä!ü{|Ög}ŸýÙŸÍ}ög6Ÿó9ŸÃ¿ÇÅ‹9~ü8$‰·z«·â§ú§y ïþîïæ}Þç}ø÷ø­ßú-^ûµ_›zí×~m~çw~‡«—~é—æ¯þê¯x ¿þë¿æe^æeø÷øª¯ú*>ú£?šúèþh¾æk¾†Û<7Iü{¼×{½ßýÝßÍ}÷w7ïó>ïÿÇoýÖoñÚ¯ýÚ<Ð˼ÌËð×ý×ü[½Ök½¿ýÛ¿Íýöoÿ6¯ó:¯Ã¿Çw}×wñÞïýÞ<Ð{¿÷{ó=ßó=ü{Øæ¹Iâßã£>ê£øê¯þjè«¿ú«ù˜ùþ=þê¯þŠ—~é—æ^æe^†¿þë¿æßêµ^ëµøíßþmè·û·y×yþ=~ê§~Š·~ë·æÞú­ßšŸù™Ÿáßêøñã\¼x‘zÆ3žÁƒü`þ=>ë³>‹ÏþìÏæ>û³?›ÏùœÏáßãéO:~ðƒy ‡<ä!Üzë­ü[½Ök½¿ýÛ¿Íýöoÿ6¯ó:¯Ã¿ÇOýÔOñÖoýÖ<Ðk¿ökó;¿ó;ü[?~œ‹/ò@¿ÿû¿Ïk¼ÆkðïñYŸõY|ög6ôÙŸýÙ|Îç|ÿ/^äøñã<Љ'ØÝÝåßê­Þê­øéŸþiè§ú§y›·yþ=~ë·~‹×~í׿^ûµ_›ßùßáßêÁ~0OúÓy ù‘áßùù÷ø¬Ïú,>û³?›úèþh¾æk¾†Û<7Iü{¼×{½ßýÝßÍ}÷w7ïó>ïÿÇoýÖoñÚ¯ýÚ<Ðk¿ökó;¿ó;ü[½ôK¿4õWÅýÈüïüÎïÌ¿ÇW}ÕWñÑýÑ<ÐGôGó5_ó5ü{Øæ¹Iâßã£>ê£øê¯þjè«¿ú«ù˜ùþ=þê¯þŠ—~é—æ^æe^†¿þë¿æßêµ^ëµøíßþmè«¿ú«ù˜ùþ=¾ë»¾‹÷~ï÷æÞû½ß›ïùžïáßÃ6ÏMÿõQÅWõWó@ŸýÙŸÍç|ÎçðïñWõW¼ôK¿4ô‡<„[o½•«×z­×â·û·y ¯þê¯æc>æcø÷ø©Ÿú)Þú­ßšzë·~k~æg~†Õñãǹxñ"W]uÕUÿC!Ûæª«®ºê(Iü{¼Õ[½?ýÓ?ÍýôOÿ4oó6oÿÇoýÖoñÚ¯ýÚ<Ðk¿ökó;¿ó;ü[=èAâÖ[oån½õVò‡ðïñYŸõY|ög6ôÙŸýÙ|Îç|ÿ/^äøñã<$þ=Þê­ÞŠŸþéŸæ¾û»¿›÷yŸ÷áßã·~ë·xí×~mèµ_ûµùßùþ­^ê¥^Š¿þë¿æþú¯ÿš—y™—áßã«¾ê«øèþhè£?ú£ùš¯ùþ=lóÜ$ñïñ^ïõ^|÷w7ôÝßýݼÏû¼ÿ¿õ[¿Åk¿ökó@¯ýÚ¯ÍïüÎïðoõZ¯õZüöoÿ6ôÛ¿ýÛ¼Îë¼ÿßõ]ßÅ{¿÷{ó@ïýÞïÍ÷|Ï÷ðïa›ç&‰ú¨â«¿ú«y ¯þê¯æc>æcø÷ø«¿ú+^ú¥_šzé—~iþæoþ†«×z­×â·û·y ßþíßæu^çuø÷ø©Ÿú)Þú­ßšzë·~k~æg~†«cÇŽ±»»ËýüÏÿë³>‹ÏþìÏæ>û³?›ÏùœÏáßãâÅ‹?~œ:~ü8—.]âßê­Þê­øéŸþiè§ú§y›·yþ=~ë·~‹×~í׿^ûµ_›ßùßáßêAz·Þz+ôÃ?üü˻¼ ÿŸõYŸÅgögó@ŸýÙŸÍç|Îçðïa›ç&‰÷z¯÷⻿û»y ïþîïæ}Þç}ø÷ø­ßú-^ûµ_›zí×~m~çw~‡«—z©—â¯ÿú¯y oÿöoç>àø÷øª¯ú*>ú£?šúèþh¾æk¾†Û<7Iü{|ÔG}_ýÕ_Í}õW5ó1ÿÇ_ýÕ_ñÒ/ýÒ<ÐK¿ôKó7ó7ü[½Ök½¿ýÛ¿Í}õW5ó1ÿÇw}×wñÞïýÞ<Ð{¿÷{ó=ßó=ü{Øæ¹Iâßã£>ê£øê¯þjè³?û³ùœÏùþ=þê¯þŠ—~é—æüàóŒg<ƒ«×z­×â·û·y ¯þê¯æc>æcø÷ø©Ÿú)Þú­ßšzë·~k~æg~†Õ±cÇØÝÝ媫®ºê(dÛ\uÕUWý%‰Ÿú©Ÿâ­ßú­y ÝÝ]^ú¥_šg<ãü[<èAâ¯ÿú¯9~ü8ôÓ?ýÓ¼ÍÛ¼ ÿVŸõYŸÅgögóÜ^ûµ_›ßùßáßâØ±cüõ_ÿ5~ðƒy ¿þë¿æe^æeø·z«·z+~ú§šç&‰Ÿú©Ÿâ­ßú­y ÝÝ]üàséÒ%þ-ô ñ×ý×?~œúê¯þj>æc>†«Ïú¬Ïâ³?û³yn/ýÒ/ÍßüÍßðoqìØ1þú¯ÿš?øÁ<Ðoÿöoó:¯ó:ü[½Õ[½?ýÓ?Ís“Ä¿ÇoýÖoñÚ¯ýÚ<Ðîî.~ðƒ¹téÿ/õR/Å_ÿõ_óܾú«¿šù˜áßê«¾ê«øèþhžÛK¿ôKó7ó7ü[;vŒ[o½•ãÇó@¿ýÛ¿Íë¼ÎëðoõQõQ|õW5ÏMÿ¿õ[¿Åk¿ökó@·Þz+/ýÒ/Í¥K—ø·x©—z)þú¯ÿšçöÙŸýÙ|Îç|ÿV_õU_ÅGôGó@»»»¼ôK¿4ÏxÆ3ø·8vì·Þz+Çç~ú§š·y›·áßê£>ê£øê¯þjè·û·y×yþ=þê¯þŠ—~é—æn½õV^ú¥_šK—.ñoñZ¯õZüöoÿ6Ïí³?û³ùœÏùþ­~ê§~Š·~ë·ævwwyé—~ižñŒgðoñ =ˆ¿þë¿æøñã<ÐOÿôOó6oó6ü[}Ög}ŸýÙŸÍýöoÿ6¯ó:¯Ã¿ÇÓŸþtüàó@ý×Í˼ÌËðoõZ¯õZüöoÿ6Ïí£?ú£ùš¯ùþ­~ê§~Š·~ë·ævwwyðƒÌ¥K—ø·xЃÄ_ÿõ_süøq軿û»yŸ÷yþ­>ë³>‹ÏþìÏæ~û·›×y×áßêØ±cüõ_ÿ5~ðƒy ¿þë¿æe^æeø·z«·z+~ú§šçöÑýÑ|Í×| ÿV¿õ[¿Åk¿ökó@»»»<øÁæÒ¥Kü[¼ÔK½ý×Ísûê¯þj>æc>†«¯úª¯â£?ú£y ßþíßæu^çuø·:vìý×̓ü`è·û·y×yþ­Þê­ÞŠŸþéŸæ¹½õ[¿5?ó3?ÿÕoýÖoñÚ¯ýÚ<Э·ÞÊK¿ôKséÒ%þ-^ê¥^Š¿þë¿æ¹}õW5ó1ÿÕW}ÕWñÑýÑ<Ðoÿöoó:¯ó:ü[;vŒ[o½•ãÇó@¿ýÛ¿Íë¼ÎëðoõQõQ|õW5Ïí­ßú­ù™Ÿùþ­þê¯þŠ—~é—æn½õV^ú¥_šK—.ñoñZ¯õZüöoÿ6Ïí³?û³ùœÏùþ­¾ë»¾‹÷~ï÷æ~þçž·x‹·àßêØ±cÜzë­?~œúéŸþiÞæmÞ†Õg}ÖgñÙŸýÙ\uÕUWý…l›«®ºêªÿ¡$ñoñ =ˆþèæ£?ú£y~þú¯ÿšÏþìÏæg~ægø×x«·z+>û³?›—~é—æùùê¯þj¾ú«¿šg<㼨ô ñÞïýÞ|ög6ÏÏîî.ýÑÍOÿôOséÒ%^T/õR/ÅWõWóÚ¯ýÚû³?›ïþîïæÏx/ª=èA¼÷{¿7ŸýÙŸÍsûíßþm^çu^‡‹·z«·â³?û³yé—~ižŸŸþéŸæ£?ú£yÆ3žÁ‹êرc¼÷{¿7ŸýÙŸÍñãÇyn»»»|ôG4?ýÓ?Í¥K—xQ½ÔK½ŸýÙŸÍ[¿õ[óüüöoÿ6ýÑÍßüÍßð¯ñ^ïõ^|õW5ÇçùùìÏþl¾û»¿›g<㼨ô ñÑýÑ|ôG4Ïí·û·y×yþ-Þê­ÞŠÏþìÏæ¥_ú¥y~¾ú«¿š¯þê¯æÏx/ȱcÇøèþh>û³?›«®ºêªÿÁmsÕUW]õ?”$žÛ\õ?$žÛ\õ?$žÛ\õ?$žÛ\õ?Ëoÿöoó:¯ó:<·×z­×â·û·¹ê–ßþíßæu^çuxn¯õZ¯ÅoÿöosÕÿ,¿ýÛ¿Íë¼ÎëðÜ^ëµ^‹ßþíßæªÿY~û·›×y×á¹½Ök½¿ýÛ¿ÍUÿ³üöoÿ6¯ó:¯Ãs{­×z-~û·›«þgùíßþm^çu^‡çöZ¯õZüöoÿ6W]uÕUW]†l›«®ºêªÿ¡$ñüØæªÿy$ñüØæªÿy$ñüØæªÿy$ñüØæªÿY~û·›×y×á¹½Ök½¿ýÛ¿ÍUÿ³üöoÿ6¯ó:¯Ãs{­×z-~û·›«þgùíßþm^çu^‡çöZ¯õZüöoÿ6WýÏòÛ¿ýÛ¼Îë¼Ïíµ^ëµøíßþm®úŸå·û·y×yžÛk½ÖkñÛ¿ýÛ\õ?Ëoÿöoó:¯ó:<·×z­×â·û·¹êª«®ºê2dÛ\uÕUWý%‰çÇ6WýÏ#‰çÇ6WýÏ#‰çÇ6WýÏ#‰çÇ6WýÏòÛ¿ýÛ¼Îë¼Ïíµ^ëµøíßþm®úŸå·û·y×yžÛk½ÖkñÛ¿ýÛ\õ?Ëoÿöoó:¯ó:<·×z­×â·û·¹ê–ßþíßæu^çuxn¯õZ¯ÅoÿöosÕÿ,¿ýÛ¿Íë¼ÎëðÜ^ëµ^‹ßþíßæªÿY~û·›×y×á¹½Ök½¿ýÛ¿ÍUW]uÕU—!Ûæª«®ºê(Iê£>Нþê¯æªÿY~û·›×y×á¹}Ög}ŸýÙŸÍUW]uÕU—!Ûæª«®ºê¨þèæk¾ækx Ïú¬Ïâ³?û³¹êžþèæk¾ækx Ïú¬Ïâ³?û³¹êžþèæk¾ækx ¯úª¯â£?ú£¹êž·~ë·æg~ægx ¯úª¯â£?ú£¹êž·~ë·æg~ægx Ÿú©Ÿâ­ßú­¹êž×~í׿w~çwx Ÿú©Ÿâ­ßú­¹êž×~í׿w~çwx Ÿú©Ÿâ­ßú­¹êž—~é—æoþæox ßú­ßâµ_ûµ¹êž—~é—æoþæox ßú­ßâµ_ûµ¹ê–ÝÝ]^ú¥_šg<ã<Ð_ýÕ_ñÒ/ýÒ\uÕUW]u²m®ºêª«þûèþh¾û»¿€þèæ³?û³¹ê¦ÝÝ]>û³?›ïþîïæøñã¼÷{¿7ŸýÙŸÍUÿ3íîîòÙŸýÙ|÷w7Çç½ßû½ùìÏþl®úŸiww—ÏþìÏæ»¿û»9~ü8ýÑÍGôGsÕÿL»»»|ôG4?ýÓ?ÍñãÇùèþh>ú£?š«þgÚÝÝå£?ú£ùéŸþiŽ?ÎGôGóÑýÑ\õ?Óîî.ýÑÍ÷|Ï÷ð =ˆÏþìÏæ½ßû½¹ê¦[o½•þèæg~ægx©—z)>û³?›·~ë·æªÿ™n½õV>ú£?šŸù™Ÿá¥^ê¥øìÏþlÞú­ßš«þgºõÖ[ùèþh~æg~†—z©—â³?û³yë·~k®ºêª«®zdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUWýÚÝÝåk¾ækøéŸþiþú¯ÿ€—~é—æµ_ûµù¨ú(üàóå·û·ùžïù~û·›[o½€×~í׿­ßú­y¯÷z/Ž?ÎU/Ü­·ÞÊ×|Í×ðÛ¿ýÛüõ_ÿ5/ýÒ/Ík¿ökóQõQ<øÁæ?Âç|Îçð¢z­×z-^ûµ_›«þe»»»<ä!aww—ßú­ßâµ_ûµùòÛ¿ýÛ|Í×| ¿ýÛ¿Íîî.¯ýÚ¯Í[¿õ[ó^ïõ^?~œ«þuvwwyÈCÂîî.¶ù°»»Ë×|Í×ð¢z¯÷z/üàsÕóú™Ÿù¾û»¿›¿þë¿æÖ[oàøñã¼ök¿6oýÖoÍ{½×{ñå·û·ùš¯ù~û·›ÝÝ]Ž?ÎK¿ôKóÞïýÞ¼×{½WýË~æg~†ïþîïæ¯ÿú¯¹õÖ[8~ü8¯ýÚ¯Í{¿÷{óVoõVü{Ýzë­|Ï÷|/ª÷z¯÷âÁ~0W=¯ïùžïá§ú§ùíßþmvww9~ü8¯ýÚ¯Í[¿õ[óVoõV?~œÿ(ßó=ßÃOÿôOóÛ¿ýÛìîîrüøq^ûµ_›·~ë·æ½Þ뽸ê_ö=ßó=|÷w7¿ýÛ¿ ÀñãÇyí×~mÞú­ßš÷z¯÷â?­·ÞÊ÷|Ï÷ð¢z¯÷z/üàsÕ‹æ«¿ú«ù˜ù~ë·~‹×~í׿?Â÷|Ï÷ðÝßýÝüöoÿ6Ççµ_ûµyë·~kÞë½Þ‹«þcìîîò‡<„ÝÝ]>ë³>‹ÏþìÏæßës>çsxQ½Ök½¯ýÚ¯ÍUW]uÕÿȶ¹êª«®úò×ý×¼Îë¼»»»¼ ßõ]ßÅ{¿÷{óïõÕ_ýÕ|ÌÇ| /ȃü`~ê§~Š—~é—æªçï¯ÿú¯y×yvwwy~Ž?ÎW}ÕWñÞïýÞü{üöoÿ6¯ó:¯Ã‹ê³>ë³øìÏþl®ú—½ÍÛ¼ ?ýÓ? ÀoýÖoñÚ¯ýÚüGø˜ù¾ú«¿šä¥_ú¥ù®ïú.^ú¥_š«^toó6oÃOÿôO`›ÿ¿ýÛ¿Íë¼Îëð¢ú­ßú-^ûµ_›«žmww—·y›·á·û·ya^ú¥_šßú­ßâøñãü{¼Ïû¼ßýÝßÍ òÒ/ýÒüÖoýÇçªçµ»»ËÛ¼ÍÛðÛ¿ýÛ¼0¯ýÚ¯ÍOýÔOqüøqþ­¾û»¿›÷yŸ÷áEõ[¿õ[¼ök¿6W=Ûîî.oó6oÃoÿöoó‚¼ôK¿4ßõ]ßÅK¿ôKóïõ>ïó>|÷w7/Èk¿ökóS?õS?~œ«ž×îî.¯ó:¯Ã_ÿõ_ó‚¼ôK¿4ßõ]ßÅK¿ôKóïñÕ_ýÕ|ÌÇ| /ªßú­ßâµ_ûµ¹ê_ö×ý×¼Ì˼ ÷û­ßú-^ûµ_›ÝÝ]ÞæmÞ†ßþíßæyë·~k¾ë»¾‹ãÇsÕ¿Ïë¼ÎëðÛ¿ýÛ|Ög}ŸýÙŸÍ¿Çîî.'NœàEõYŸõY|ög6W]uÕUÿ Ûæª«®ºê?À_ÿõ_ó:¯ó:ìîîðZ¯õZ¼õ[¿5Çç·û·ùžïùî÷]ßõ]¼÷{¿7ÿVßýÝßÍû¼ÏûpìØ1Þû½ß›×~í׿Ö[oå§ú§ùßùŽ?ÎÓŸþtŽ?ÎUÏé¯ÿú¯y×yvwwx¯÷z/^ûµ_€ßþíßæ{¾ç{¸ßOýÔOñÖoýÖü[}÷w7ïó>ïËê³>ë³øìÏþl®záÞç}Þ‡ïþîïæ~¿õ[¿Åk¿ökóïõÕ_ýÕ|ÌÇ| ÇŽã½ßû½yí×~mþú¯ÿšïþîïæÏx/ýÒ/ÍoýÖoqüøq®ú—½Ïû¼ßýÝßÍýlóá³?û³ùœÏù^T¿õ[¿Åk¿öksÕ³½Ì˼ ý× À±cÇxï÷~oüàpë­·òÝßýÝ\ºt €—~é—æ·~ë·8~ü8ÿïó>ïÃw÷wpìØ1Þû½ß›×~í׿¯ÿú¯ùîïþnžñŒgðÒ/ýÒüÖoýÇçªgÛÝÝåu^çuøë¿þkŽ;Æ{¿÷{óà?€[o½•ïþîïæÒ¥K¼ôK¿4¿õ[¿ÅñãÇù·øìÏþl>çs>‡ÕoýÖoñÚ¯ýÚ\õl/ó2/Ã_ÿõ_pìØ1>ú£?š—~é—æ¯ÿú¯ùíßþm~çw~€ãÇóWõW<øÁæßê}Þç}øîïþnô ñÞïýÞ¼ök¿6¿ýÛ¿Íw÷wóŒg<€×~í׿·~ë·¸ê9íîîò:¯ó:üõ_ÿ5zЃxï÷~o^ú¥_š¿þë¿æ§ú§ù›¿ùŽ?ÎÓŸþtŽ?οÕ{¿÷{ó=ßó=¼¨~ë·~‹×~í׿ªÙ˼ÌËð×ý×Üï·~ë·xí×~mþ=Þç}Þ‡ïþîïàAzïýÞïÍK¿ôKóÛ¿ýÛ|÷w7—.]à­ßú­ù©Ÿú)®ú·ûê¯þj>æc>†û}Ög}ŸýÙŸÍ¿Çoÿöoó:¯ó:¼¨>ë³>‹ÏþìÏæª«®ºêÿdÛ\uÕUWýx×y~û·€ïú®ïâ½ßû½y ßþíßæu^çu8~ü8OúÓ9~ü8ÿZ»»»<ä!aww—cÇŽñÛ¿ýÛ¼ôK¿4ôÞïýÞ|Ï÷|ïõ^ïÅw÷wsÕsz×y~û·€ïú®ïâ½ßû½y ïþîïæ}Þç}xðƒÌÓŸþtþ­>ú£?š¯ùš¯à¯þê¯xé—~i®ú·ÛÝÝå}Þç}øéŸþiè·~ë·xí×~mþ=n½õVò‡pìØ1þú¯ÿš?øÁ<Ð{¿÷{ó=ßó=|ÔG}_ýÕ_ÍU/Øîî.ïó>ïÃOÿôOó@¶ùðÖoýÖüÌÏü /^äøñã\õ¢ûìÏþl>çs>€—z©—â§ú§yðƒÌÝzë­¼õ[¿5ó7Àg}ÖgñÙŸýÙükýöoÿ6¯ó:¯ÀK½ÔKñÛ¿ýÛ?~œûíîîòÞïýÞüÌÏü ŸõYŸÅgögsÕ³}ög6Ÿó9ŸÀK½ÔKñÓ?ýÓ<øÁæn½õVÞú­ßš¿ù›¿à³>ë³øìÏþlþ-^ûµ_›ßùßÀ6Wýë|ög6Ÿó9ŸÀK½ÔKñÛ¿ýÛ?~œúèþh¾æk¾€÷z¯÷⻿û»ù·øíßþm^çu^€—z©—â·û·9~ü8÷ÛÝÝåµ_ûµù›¿ù¾ë»¾‹÷~ï÷æªgûìÏþl>çs>€×z­×â§ú§9~ü8ôÞïýÞ|Ï÷|ïõ^ïÅw÷wóoõ2/ó2üõ_ÿ5ÇŽcww—«þc|ôG4_ó5_ÃýÖoý¯ýگͿÕw÷wó>ïó>¼Ök½?ýÓ?Íñãǹßîî.¯ýÚ¯ÍßüÍßð]ßõ]¼÷{¿7Wýëýõ_ÿ5/ó2/Ã}Ög}ŸýÙŸÍ¿Çgögó9Ÿó9üÖoý¯ýÚ¯ÍUW]uÕÿȶ¹êª«®úwúíßþm^çu^€×z­×â·û·y~>û³?›ÏùœÏà«¾ê«øèþhþµ>û³?›ÏùœÏ໾ë»xï÷~ožŸ×~í׿w~çwxúӟ΃ü`®ºâ·û·y×yÞë½Þ‹ïþîïæùùìÏþl>çs>€¯úª¯â£?ú£ù·xí×~m~çw~Û\õo÷Û¿ýÛ¼Ïû¼·Þz+Ïí·~ë·xí×~mþ=Þû½ß›ïùžï໾ë»xï÷~ožŸ?øÁ<ãÏàâÅ‹?~œ«ž×oÿöoó>ïó>Üzë­<7ÛüGx™—yþú¯ÿš=èAÜzë­\õ¯ó‡<„[o½€¿ú«¿â¥_ú¥y~þú¯ÿš—y™—àøñã\¼x‘­÷~ï÷æ{¾ç{ø­ßú-^ûµ_›ç¶»»Ëƒü`.]ºÄñãǹxñ"W=Û‰'ØÝÝàéO:~ðƒy~þú¯ÿš—y™—àÁ~0OúÓù·8qâ»»»¼ÔK½ý×ÍUÿ:yÈC¸õÖ[xúӟ΃ü`žŸ?øÁ<ãÏàâÅ‹?~œ­×y×á·û·xúӟ΃ü`žÛîî.~ðƒ¹té~ðƒyúÓŸÎUÏvâÄ vwwxúӟ΃ü`žÛîî./ýÒ/Í3žñ lóo% €×z­×â·û·¹êßï·û·y×yžÛoýÖoñÚ¯ýÚü[=ä!áÖ[oàéO:~ðƒyn·Þz+yÈCxðƒÌÓŸþt®ú×{™—yþú¯ÿšcÇŽqéÒ%>ë³>‹ÏþìÏæßã½ßû½ùžïù.^¼Èñãǹꪫ®úÙ6W]uÕUÿNïýÞïÍ÷|Ï÷ðS?õS¼õ[¿5ÏÏîî.'Nœà¥_ú¥ù«¿ú+þµNœ8Áîî.ÇŽcww—ä§ú§y›·y¾ê«¾Šþèæª+Þû½ß›ïùžïà·~ë·xí×~mžŸÝÝ]Nœ8ÀK¿ôKóWõWü[Hàµ^ëµøíßþm®ú×ÛÝÝå}Þç}øéŸþiî÷Z¯õZìîîò7ó7üÖoý¯ýگͿ‡$ô që­·ò‚|õW5ó1Àw}×wñÞïýÞ\õl»»»¼ÍÛ¼ ¿ýÛ¿Íý^ëµ^‹ÝÝ]þæoþÛüGÀ[½Õ[ñÓ?ýÓ\õ¢ûë¿þk^æe^€×z­×â·û·ya^ú¥_š¿ù›¿à¯þê¯xé—~i^T»»»œ8q€—z©—â¯ÿú¯yA>ú£?š¯ùš¯à§~ê§xë·~k®‚ßþíßæu^çux­×z-~û·›æÁ~0ÏxÆ3¸xñ"Çç_ãÖ[oå!yïõ^ïÅw÷wsÕ‹î¯ÿú¯y™—y^ëµ^‹ßþíßæyë·~k~æg~€ßú­ßâµ_ûµù×øë¿þk^æe^€·z«·â§ú§yA>ú£?š¯ùš¯à¯þê¯xé—~i®‚ßþíßæu^çux«·z+~ú§šäµ_ûµùßù~ë·~‹×~í׿_ë·û·y×y>ë³>‹ÏþìÏæªŸÝÝ]^æe^†[o½•×z­×àw~çwø­ßú-^ûµ_›‹ßþíßæu^çux¯÷z/¾û»¿›ä½ßû½ùžïùžþô§óà?˜«^týÑÍ×|Í×pìØ1>û³?›ù˜à³>ë³øìÏþlþ=^æe^†¿þë¿æAz·Þz+W]uÕUÿ Ûæª«®ºêßé!y·Þz+¶ya^ûµ_›ßùßàâÅ‹?~œÕ_ÿõ_ó2/ó2¼Õ[½?ýÓ?Í ²»»Ë‰'x­×z-~û·›«®8qâ»»»;vŒÝÝ]^˜—~é—æoþæo°Í¿Ö­·ÞÊCò>ê£>Нþê¯æª½ßþíßæu^çu¸ßg}ÖgñÙŸýÙ¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚü[ýöoÿ6¯ó:¯À{½×{ñÝßýݼ ý×Í˼ÌËðVoõVüôOÿ4W=Ûoÿöoó:¯ó:;vŒÏþìÏæ£?ú£yí×~m~çw~Ûü{ýöoÿ6¯ó:¯Àg}ÖgñÙŸýÙ\õ¢ûë¿þk>ú£?€?øÁ|÷w7/Ìk¿ökó;¿ó;üÖoý¯ýگ͋껿û»yŸ÷y>ê£>Нþê¯æùíßþm^çu^€÷z¯÷⻿û»¹ ~û·›ÏþìÏà¥_ú¥ùê¯þj^˜×~í׿w~çwø­ßú-^ûµ_›ŸþéŸæmÞæmøª¯ú*>ú£?š«þuvwwùë¿þkŽ?ÎK¿ôKó‚¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚük|õW5ó1ÀW}ÕWñÑýѼ ?ýÓ?ÍÛ¼ÍÛðYŸõY|ög6W]±»»Ë_ÿõ_ðÚ¯ýÚ¼ ¯ýÚ¯ÍïüÎïðWõW¼ôK¿4ÿZßýÝßÍû¼ÏûðS?õS¼õ[¿5Wýû¼ÍÛ¼ ?ýÓ?ͱcǸõÖ[yë·~k~çw~€ßú­ßâµ_ûµù·øìÏþl>çs>€ïú®ïâ½ßû½yA¾û»¿›÷yŸ÷à«¾ê«øèþh®zÑüöoÿ6¯ó:¯ÀW}ÕWñÒ/ýÒ¼Î뼟õYŸÅgögóï! €·z«·â§ú§¹êª«®úÙ6W]uÕUÿ»»»œ8q€×z­×â·û·ya>ú£?š¯ùš¯à·~ë·xí×~m^TßýÝßÍû¼ÏûðYŸõY|ög6/Ìñãǹté¶¹ n½õVò‡ðZ¯õZüöoÿ6/Ì{¿÷{ó=ßó=üÖoý¯ýگͿÆOÿôOó6oó6|×w}ïýÞïÍý~çw~‡×z­×âªÙoÿöoó:¯ó:¼×{½Ÿýٟ̓ü`^ûµ_›ßùßà·~ë·xí×~mþ­>û³?›ÏùœÏà³>ë³øìÏþl^I<øÁæéO:W=Ûoÿöoó:¯ó:¼×{½Ÿýٟ̓ü`^ûµ_›ßùßÀ6ÿ^_ýÕ_ÍÇ|ÌÇð[¿õ[¼ök¿6÷ûßù^ëµ^‹«þãœ8q‚ÝÝ].^¼ÈñãÇyQ}ög6Ÿó9ŸÀOýÔOñÖoýÖ¼0’x­×z-~û·›«þõNœ8Áîî.¶ù×úìÏþl>çs>€ßú­ßâµ_ûµØÝÝåoþæox­×z-®ú÷ÛÝÝå!y»»»;vŒÝÝ]þµÞû½ß›ïùžïà·~ë·xí×~m^¿þë¿æe^æex­×z-~û·›«^t»»»<ä!aww—cÇŽ±»»Ë¿ÅGôGó5_ó5<ýéOçÁ~0»»»Üzë­¼ôK¿4W½è¾û»¿›÷yŸ÷à§~ê§xë·~k^ûµ_›ßùßà·~ë·xí×~mþ-^ûµ_›ßùßà·~ë·xí×~m^ßþíßæu^çux¯÷z/¾û»¿›«þe»»»<ä!aww—×z­×â·û·ùíßþm^çu^€Ïú¬Ïâ³?û³ù·úíßþm^çu^€Ïú¬Ïâ³?û³¹ßïüÎïðR/õR?~œ«®ºêªÿ£msÕUW]õïðÛ¿ýÛ¼Î뼯õZ¯ÅoÿöoóÂ|ög6Ÿó9ŸÀW}ÕWñÑýѼ¨>û³?›ÏùœÏ໾ë»xï÷~o^˜×~í׿w~çwxúӟ΃ü`þ¿ûíßþm^çu^€·z«·â§ú§ya>û³?›ÏùœÏ໾ë»xï÷~oþ5>û³?›ÏùœÏà·~ë·øßù¾û»¿›[o½•û=øÁæ½ßû½ù¬Ïú,®zþn½õVüàó@¯ýÚ¯ÍïüÎïð[¿õ[¼ök¿6ÿVýÑÍ×|Í×ð[¿õ[¼ök¿6/̃ü`žñŒg`›«žíÖ[oàÁ~0ôÚ¯ýÚüÎïü¶ù÷zï÷~o¾ç{¾€§?ýé|Ï÷|_ýÕ_Íîî.÷{é—~i>ú£?š÷z¯÷⪻ŸþéŸæmÞæmxЃÄ­·ÞÊ¿Æk¿ökó;¿ó;üÖoý¯ýÚ¯Í #‰ûÙæªïþîïæ}Þç}x©—z)þú¯ÿš­·~ë·æg~ægxúÓŸÎç|ÎçðÓ?ýÓìîîr¿—~é—æ£?ú£y¯÷z/®ú×ÛÝÝåc>æcøîïþn>ë³>‹ÏþìÏæ_ëµ_ûµùßùþê¯þŠ—~é—æ…‘Àƒü`žþô§sÕ‹fww—÷yŸ÷á§ú§ø¬Ïú,>û³?›‹×~í׿w~çw8vìý×Íç|ÎçðÓ?ýÓìîîr¿—~é—æ£?ú£y¯÷z/®zÁn½õV^æe^†ÝÝ]Þë½Þ‹ïþîïàµ_ûµùßù~ë·~‹×~í׿ßâµ_ûµùßùlóÂìîîrâÄ ^ëµ^‹ßþíßæªÙÛ¼ÍÛðÓ?ýÓ;vŒ[o½•ãÇóÛ¿ýÛ¼Î뼟õYŸÅgögóoõÕ_ýÕ|ÌÇ| ¿õ[¿ÅïüÎïðÝßýÝÜzë­ÜïÁ~0ïýÞïÍG}ÔGqüøq®ºêª«þA¶ÍUW]uÕ¿Ãoÿöoó:¯ó:|Ög}ŸýÙŸÍ óÕ_ýÕ|ÌÇ| ŸõYŸÅgögó¢úìÏþl>çs>€ßú­ßâµ_ûµya^ûµ_›ßùßà·~ë·xí×~mþ¿ûíßþm^çu^€Ïú¬Ïâ³?û³ya>û³?›ÏùœÏà³>ë³øìÏþlþ5^ûµ_›ßùßàøñãìîîò‚¼ôK¿4?õS?Ńü`®zѼök¿6¿ó;¿ÀoýÖoñÚ¯ýÚü[½ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ¼0¯ýÚ¯ÍïüÎï`›«þe¯ýÚ¯ÍïüÎï`›¯×~í׿w~çw8~ü8»»»¼ /ýÒ/ÍoýÖoqüøq®ú×ÙÝÝåe^æe¸õÖ[øª¯ú*>ú£?š×~í׿w~çwø«¿ú+^ú¥_šæµ_ûµùßùlsÕ‹nww——y™—áÖ[o໾ë»xï÷~oþµò‡pë­·ò¢xí×~m~ê§~ŠãÇsÕ ÷×ý×üÍßü ý×Íw÷w³»» À{½×{ñÝßýÝü[¼ök¿6¿ó;¿€mþ%’¸Ÿm®zÁþú¯ÿšßùßáÖ[o廿û»ÙÝÝà½Þë½øîïþnþ­$püøqvwwyaÞû½ß›¯úª¯âøñã\õ¼^çu^‡ßþíßæAzý×ÍñãÇxí×~m~çw~€ßú­ßâµ_ûµù·Äýló/‘Àk½ÖkñÛ¿ýÛ\õÂ}÷w7ïó>ïÀOýÔOñÖoýÖüöoÿ6¯ó:¯Àg}ÖgñÙŸýÙü[}ôG4_ó5_ÀñãÇÙÝÝåyðƒÌOýÔOñÒ/ýÒ\uÕUWýl›«®ºêª‡ßþíßæu^çuø¬Ïú,>û³?›æ·û·y×y>ë³>‹ÏþìÏæEõÚ¯ýÚüÎïü¿õ[¿Åk¿ökó¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚü÷ÝßýݼÏû¼ŸõYŸÅgögóÂ|ög6Ÿó9ŸÀg}ÖgñÙŸýÙükœ8q‚ÝÝ]î÷Z¯õZ¼ök¿6¯ýگͭ·ÞÊoÿöoó=ßó=Üï¥_ú¥ù­ßú-Ž?ÎUÿ²×~í׿w~çwø­ßú-^ûµ_›«×~í׿w~çwø­ßú-^ûµ_›æµ_ûµùßùþê¯þŠ—~é—æªîµ_ûµùßùlóï%‰z­×z-Þú­ßš—~é—æÖ[oå·û·ùžïùî÷Ò/ýÒüÖoýÇçªÝû¼ÏûðÝßýݼÔK½ý×Í¿Ök¿ökó;¿ó;Øæ_òÚ¯ýÚüÎïü¶¹êE÷6oó6üôOÿ4/õR/Å_ÿõ_󯵻»Ë‰'x ·z«·âµ_ûµyé—~in½õV¾û»¿›ßùßá~¯ýÚ¯ÍoýÖoqÕ ÷2/ó2üõ_ÿ5ô^ïõ^|÷w7ÿV'Nœ`wwÛüK$q?Û\õ‚=ä!áÖ[oå>ê£>Нþê¯æßêÖ[oå!yôVoõV¼ök¿6/ýÒ/Í_ÿõ_óÓ?ýÓüÎïü÷{ï÷~o¾ë»¾‹«žÓgögó9Ÿó9üÖoý¯ýÚ¯Íý^ûµ_›ßùßà·~ë·xí×~mþ-$q?ÛüK$q?Û\õ‚Ýzë­¼Ì˼ »»»¼Õ[½?ýÓ?Íý~û·›×y×à³>ë³øìÏþlþ­^ûµ_›ßùßá~/õR/Å[¿õ[óÚ¯ýÚìîîòÛ¿ýÛ|÷w7—.]àøñãüÕ_ý~ðƒ¹êª«®ú?Ù6W]uÕUÿ¿ýÛ¿Íë¼ÎëðYŸõY|ög6/Ìoÿöoó:¯ó:|Ög}ŸýÙŸÍ‹êµ_ûµùßù~ë·~‹×~í׿…yí×~m~çw~€ßú­ßâµ_ûµùÿî³?û³ùœÏù>ë³>‹ÏþìÏæ…ùéŸþiÞæmÞ€Ïú¬Ïâ³?û³yQíîîrâÄ î÷]ßõ]¼÷{¿7Ïí¯ÿú¯yí×~m.]ºÀ{½×{ñÝßýÝ\õ/{í×~m~çw~€ßú­ßâµ_ûµù·zí×~m~çw~€ßú­ßâµ_ûµya^ûµ_›ßùßà·~ë·xí×~m®zá^ûµ_›ßùßÀ6ÿý×Í˼ÌËp¿Ÿú©Ÿâ­ßú­yný×Ík¿ökséÒ%>ë³>‹ÏþìÏæªÍû¼ÏûðÝßýÝ;vŒ¿þë¿æÁ~0ÿZ¯ýÚ¯ÍïüÎï`›Ék¿ökó;¿ó;ØæªÍû¼ÏûðÝßýÝ;vŒ¿þë¿æÁ~0ÿZ¿ýÛ¿Íë¼ÎëpìØ1~ú§š×~í׿¹ýôOÿ4oó6oÃý¾ë»¾‹÷~ï÷æªLzЃxÆ3žÁ=øÁæ§~ê§xé—~iþµ$q?ÛüKüàóŒg<Û\õ‚IâAzÏxÆ3x ?øÁüÔOý/ýÒ/Í¿ÖOÿôOó6oó6;vŒßþíßæ¥_ú¥ynßýÝßÍû¼Ïûp¿ïú®ïâ½ßû½¹êŠ¿þë¿æe^æeø¨ú(¾ú«¿šzí×~m~çw~€ßú­ßâµ_ûµù·Äýló/‘ÄýlsÕ ö:¯ó:üöoÿ6zЃøë¿þkŽ?Îý~û·›×y×à³>ë³øìÏþlþ­$q¿Ïú¬Ïâ³?û³yn·Þz+oýÖoÍßüÍßðÚ¯ýÚüÖoýW]uÕUÿ Ûæª«®ºêßá·û·y×y>ë³>‹ÏþìÏæ…ùíßþm^çu^€Ïú¬Ïâ³?û³yQ½õ[¿5?ó3?ÀoýÖoñÚ¯ýÚ¼0¯ýÚ¯ÍïüÎïð[¿õ[¼ök¿6ÿßýôOÿ4oó6oÀg}ÖgñÙŸýÙ¼0_ýÕ_ÍÇ|ÌÇðYŸõY|ög6/ªÝÝ]þú¯ÿš¿þë¿æÁ~0oýÖoÍ òÛ¿ýÛ¼Îë¼÷³ÍUÿ²×~í׿w~çwø­ßú-^ûµ_›«×~í׿w~çwø­ßú-^ûµ_›æµ_ûµùßù~ë·~‹×~í׿ªîµ_ûµùßùlóï±»»Ë_ÿõ_ó×ý×<øÁæ­ßú­yA¾û»¿›÷yŸ÷àøñã\¼x‘«þeïó>ïÃw÷wpìØ1~û·›—~é—æßâµ_ûµùßùló/yí×~m~çw~Û\õÂíîîò1ó1|÷w7ÇŽã·û·yé—~iþ-vwwùë¿þkþú¯ÿš—~é—æµ_ûµyA¾ú«¿šù˜àÁ~0OúÓ¹êEsë­·òÑýÑüÌÏü Çç·~ë·xé—~iþ5üàóŒg<ÛüK$q?Û\õ¢¹õÖ[ùèþh~æg~€ãÇó[¿õ[¼ôK¿4ÿ·Þz+·Þz+¿ýÛ¿Í[¿õ[óÒ/ýÒ¼ ŸýÙŸÍç|ÎçðÒ/ýÒüÕ_ýWÁîî.¯ó:¯Ã_ÿõ_óR/õRüõ_ÿ5Ïíµ_ûµùßù~ë·~‹×~í׿ßB÷³Í¿D÷³ÍUÏßgögó9Ÿó9üÖoý¯ýÚ¯Íýöoÿ6¯ó:¯Àg}ÖgñÙŸýÙü[ýöoÿ6·Þz+ïýÞïÍ ò×ý×¼Ì˼ ÷{úӟ΃ü`®ºêª«þ—C¶ÍUW]uÕ¿Ãoÿöoó:¯ó:|Ög}ŸýÙŸÍ óÝßýݼÏû¼ŸõYŸÅgögó¢úìÏþl>çs>€ßú­ßâµ_ûµya^ûµ_›ßùßà·~ë·xí×~mþ¿ûíßþm^çu^€Ïú¬Ïâ³?û³ya>û³?›ÏùœÏà³>ë³øìÏþlþ³¼ôK¿4ó7ÀoýÖoñÚ¯ýÚ\õ½ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚü[½ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ¼0¯ýÚ¯ÍïüÎïðWõW¼ôK¿4W½p¯ýÚ¯ÍïüÎï`›ÿJ~ðƒyÆ3žÀ_ýÕ_ñÒ/ýÒ\õüíîîò6oó6üöoÿ6ÇŽã·û·yé—~iþ­^ûµ_›ßùßàâÅ‹?~œæµ_ûµùßùlsÕ ¶»»Ëë¼Îëð×ý×;vŒßþíßæ¥_ú¥ù¯rüøq.]ºÀÓŸþtüàsÕ‹î£?ú£ùš¯ù^ûµ_›ßú­ßâ_ãµ_ûµùßùló/‘ÄýlsÕ¿ÎGôGó5_ó5¼ök¿6¿õ[¿Å–ÝÝ]Nœ8Áýls|ôG4_ó5_À_ýÕ_ñÒ/ýÒ<·×~í׿w~çwø­ßú-^ûµ_› IÜÏ6ÿI;vŒÝÝ]®z^ý×Í˼ÌËðQõQ|õW5Ïí·û·y×y>ë³>‹ÏþìÏæ¿Â[¿õ[ó3?ó3|ÕW}ýÑÍUW]uÕÿrȶ¹êª«®úwøíßþm^çu^€÷z¯÷⻿û»ya>û³?›ÏùœÏà³>ë³øìÏþl^TŸýÙŸÍç|Îçð[¿õ[¼ök¿6/Ìk¿ökó;¿ó;<ýéOçÁ~0ÿßýöoÿ6¯ó:¯ÀG}ÔGñÕ_ýÕ¼0ŸýÙŸÍç|ÎçðU_õU|ôG4ÿY>ú£?š¯ùš¯à³>ë³øìÏþl®zá^ûµ_›ßùßà·~ë·xí×~mþ­Þû½ß›ïùžïà·~ë·xí×~m^˜×~í׿w~çw°ÍUÿ²×~í׿w~çw°Í¥×~í׿w~çwø­ßú-^ûµ_›«ž×_ÿõ_ó>ïó>üõ_ÿ5ÇŽã·û·yé—~iþ=^ûµ_›ßùßà·~ë·xí×~m^˜'N°»» €m®zþþú¯ÿš·y›·áÖ[oàAz?ýÓ?ÍK¿ôKó_éµ_ûµùßù~ë·~‹×~í׿ªÝîî./ýÒ/Í3žñ þê¯þŠ—~é—æEõÚ¯ýÚüÎïü¶ù—HàAz·Þz+Wýëìîîòà?˜K—.ðô§??øÁügyé—~iþæoþ€ßú­ßâµ_ûµùÿì§ú§y›·y>ë³>‹ÏþìÏæùyí×~m~çw~€ßú­ßâµ_ûµù·xé—~iþæoþÛüK$ðZ¯õZüöoÿ6W=§ÝÝ]^æe^†[o½•—z©—â·û·9~ü8Ïí·û·y×y>ë³>‹ÏþìÏæ¿Âgögó9Ÿó9|Ög}ŸýÙŸÍUW]uÕÿrȶ¹êª«®úwØÝÝåĉ¼Ök½¿ýÛ¿Í óÙŸýÙ|Îç|?õS?Å[¿õ[ó¢úê¯þj>æc>€Ïú¬Ïâ³?û³ya^æe^†¿þë¿À6WÁ­·ÞÊCò^ëµ^‹ßþíßæ…ùèþh¾æk¾€ßú­ßâµ_ûµùÏòÙŸýÙ|Îç|ŸõYŸÅgögsÕ ÷Ú¯ýÚüÎïü¿õ[¿Åk¿ökóoõÙŸýÙ|Îç|?õS?Å[¿õ[óÂHâ~¶¹ê_öÚ¯ýÚüÎïü¶ù¯ôÙŸýÙ|Îç|ßõ]ßÅ{¿÷{sÕsúë¿þk^çu^‡ÝÝ]^ê¥^Šßþíßæøñãü{}ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ¼0’xЃÄ­·ÞÊUÏë¯ÿú¯y×yvwwx©—z)~û·›ãÇó_íµ_ûµùßù~ë·~‹×~í׿ª×~í׿w~çwø­ßú-^ûµ_›Õ{¿÷{ó=ßó=üÖoý¯ýÚ¯Í ²»»Ë‰'x­×z-~û·›«þõ^ûµ_›ßùßà·~ë·xí×~mþ³¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚüöÙŸýÙ|Îç|ÿV¿õ[¿Åk¿ökó¢zí×~m~çw~€§?ýé<øÁæùíßþm^çu^€×z­×â·û·¹ê9ýöoÿ6¯ó:¯Ã¿Õk½ÖkñÛ¿ýÛügùìÏþl>çs>€Ïú¬Ïâ³?û³¹êª«®ú_Ù6W]uÕUÿNÇçÒ¥K<øÁæéO:/Ì[¿õ[ó3?ó3üÕ_ý/ýÒ/Í‹ê·û·y×y>ê£>Нþê¯æ…‘Àƒô n½õV®ºB/ýÒ/Í_ýÕ_ñ¼ök¿6¿ó;¿ÀÓŸþtüà󯱻»ËßüÍßðR/õR?~œæ£?ú£ùš¯ù¾ë»¾‹÷~ï÷æªîµ_ûµùßù~ë·~‹×~í׿ßê§ú§y›·y>ë³>‹ÏþìÏæÙÝÝåĉ¼Ök½¿ýÛ¿ÍUÿ²×~í׿w~çw°Í¿×îî.ó7ÃK½ÔKqüøq^˜×~í׿w~çwø­ßú-^ûµ_›«ží§ú§yŸ÷yvwwx«·z+¾û»¿›ãÇóỿû»yŸ÷y¾ê«¾Šþèæ¹õÖ[yÈCÀk½ÖkñÛ¿ýÛ\õœ¾û»¿›÷yŸ÷á~ïõ^ïÅWõWsüøqþ£Üzë­<ãÏàµ^ëµø—¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ\¿ó;¿Àƒô üàóÂ|ög6Ÿó9ŸÀw}×wñÞïýÞ¼¨>û³?›ÏùœÏà§~ê§xë·~k^ßþíßæu^çux¯÷z/¾û»¿›«®øßùvwwy©—z)üàó¼õ[¿5?ó3?ÀoýÖoñÚ¯ýÚükÜzë­<ãÏàµ^ëµø—¼Ì˼ ý× À_ýÕ_ñÒ/ýÒüöÙŸýÙ|Îç|ÿV¿õ[¿Åk¿ökó¢úìÏþl>çs>€ßú­ßâµ_ûµyA~ú§š·y›·à³>ë³øìÏþl®zN¿ýÛ¿Íë¼ÎëðoõZ¯õZüöoÿ6ÿZ¿ó;¿Ãƒô üàóÂ|ög6Ÿó9ŸÀg}ÖgñÙŸýÙ\uÕUWý/‡l›«®ºêª§·~ë·æg~ægxúӟ΃ü`^'N°»»Ë±cÇØÝÝå_cww—'NðÒ/ýÒüÕ_ý/Èoÿöoó:¯ó:¼×{½ßýÝßÍUW¼ök¿6¿ó;¿ÀÅ‹9~ü8/ȉ'ØÝÝåAz·Þz+ÿïýÞïÍ÷|Ï÷ðU_õU|ôG4/Ì˼ÌËð×ý×üÕ_ý/ýÒ/ÍU/Ük¿ökó;¿ó;üÖoý¯ýگͿխ·ÞÊCò^ëµ^‹ßþíßæùéŸþiÞæmÞ€ú¨â«¿ú«¹ê_öÚ¯ýÚüÎïü¶ù÷xí×~m~çw~€ïú®ïâ½ßû½yaò‡pë­·pñâEŽ?ÎUWüõ_ÿ5¯ó:¯Ãîî.ïõ^ïÅw÷wóé¯ÿú¯y™—yÞê­ÞŠŸþéŸæùîïþnÞç}Þ€¯úª¯â£?ú£¹êÙ~ú§š·y›·á~õQÅWõWóéµ_ûµùßù~ê§~Š·~ë·æ…9qâ»»»Øæ*øìÏþl>çs>€¯úª¯â£?ú£ya^ûµ_›ßùßà·~ë·xí×~m^T¿ýÛ¿Íë¼ÎëðQõQ|õW5/Ègögó9Ÿó9|×w}ïýÞïÍUðÙŸýÙ|Îç|_õU_ÅGôGó¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ¼¨üàóŒg<€¿ú«¿â¥_ú¥yAvww9qâÇŽcww—ÿï~û·›ßþíßæ_òÝßýÝ<ãÏà½Þë½xðƒ À{¿÷{óà?˜Õw÷wó>ïó>|Ög}ŸýÙŸÍ òÑýÑ|Í×| ?õS?Å[¿õ[sÕsºõÖ[ùîïþnþ%·Þz+ßó=ßÀk½ÖkñÚ¯ýÚ<øÁæ½ßû½yQ}ög6Ÿó9ŸÀG}ÔGñÕ_ýÕ¼0oýÖoÍÏüÌÏðS?õS¼õ[¿5W]uÕUÿË!Ûæª«®ºêß黿û»yŸ÷y>ë³>‹ÏþìÏæùùíßþm^çu^€÷z¯÷⻿û»ù×zë·~k~æg~€§?ýé<øÁæùyï÷~o¾ç{¾€ßú­ßâµ_ûµ¹êНþê¯æc>æcø¬Ïú,>û³?›ç绿û»yŸ÷yÞë½Þ‹ïþîïæ_ã«¿ú«ù˜ù^ú¥_š¿ú«¿âùíßþm^çu^€=èAÜzë­\õ/{í×~m~çw~€ßú­ßâµ_ûµù÷xé—~iþæoþ€§?ýé<øÁæùyï÷~o¾ç{¾€ßú­ßâµ_ûµ¹ê_öÚ¯ýÚüÎïü¶ù÷øê¯þj>æc>€×~í׿·~ë·xA~û·›×y×àµ^ëµøíßþm®ºâÖ[oåe^æeØÝÝà³>ë³øìÏþlþ3<øÁæÏxÇçéO:Ççùy×y~û·€§?ýé<øÁæª+þú¯ÿš×y×aww€ïú®ïâ½ßû½ùöÞïýÞ|Ï÷|õQÅWõWó‚|÷w7ïó>ïÀ[½Õ[ñÓ?ýÓ\?ýÓ?ÍÛ¼ÍÛðÒ/ýÒüÕ_ý/È­·ÞÊCòîwñâEŽ?οÆñãǹtéÇçéO:ÇçùyÈC­·Þ ÀÅ‹9~ü8WÁOÿôOó6oó6¼ôK¿4õWÅ rë­·ò‡<€cÇŽ±»»Ë¿Æ{¿÷{ó=ßó=|ÔG}_ýÕ_Í òÙŸýÙ|Îç|ïõ^ïÅw÷wsÕ‹æµ_ûµùßù~ë·~‹×~í׿ßbww—'Nðà?˜§?ýé¼ yÈC¸õÖ[9vì·Þz+Ç窛ßþíßæu^çuø¬Ïú,>û³?›‹ŸþéŸæmÞæmxðƒÌÓŸþt^[o½•‡<ä!;vŒ[o½•ãÇsÕUW]õ¿²m®ºêª«þvwwyðƒÌ¥K—8~ü8¿õ[¿ÅK¿ôKó@»»»¼Îë¼ý× ÀÓŸþtüàóÜ~çw~‡û½ÔK½Çç~ú§š·y›·à¥_ú¥ù«¿ú+žÛ_ÿõ_ó2/ó2<èAâÖ[oåªgÛÝÝåÁ~0—.]âøñãüÖoý/ýÒ/Ííîîò2/ó2Üzë­<ýéOçÁ~0Ïíw~çw¸ßK½ÔKqüøqî·»»Ëƒü`.]ºÀW}ÕWñÑýÑ<·ÝÝ]^çu^‡¿þë¿໾ë»xï÷~o®ú—½ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ¼0¿ó;¿Ãý^ê¥^ŠãÇó@ßýÝßÍû¼ÏûðÖoýÖüÔOýÏí§ú§y›·y^ëµ^‹ßþíßæªÍk¿ökó;¿ó;Øæ_ò;¿ó;Üïµ^ëµx [o½•‡<ä!Üï§~ê§xë·~kžÛîî./ó2/í·Þ ÀOýÔOñÖoýÖ\uÅë¼ÎëðÛ¿ýÛ¼×{½ßýÝßÍ¿Åîî.ó7Ãý^ëµ^‹çöÙŸýÙ|Îç|ïýÞïÍw}×wñܾû»¿›÷yŸ÷à½Þë½øîïþn®z¶—y™—á¯ÿú¯ø¨ú(¾ú«¿š‹ÝÝ]þæoþ†û½Ök½ô×ý×¼Ì˼ ÷û­ßú-^ûµ_›çö×ý×¼Îë¼»»»üÖoý¯ýÚ¯ÍUW<øÁæÏx_õU_ÅGôGóÜvwwy×yþú¯ÿ€ú¨â«¿ú«y ÝÝ]þæoþ†û½Ök½Ïí£?ú£ùš¯ù>ê£>Нþê¯æ¹}õW5ó1À{½×{ñÝßýÝ\õl~ðƒyÆ3žÀW}ÕWñÑýÑ<·ÝÝ]^çu^‡¿þë¿à³>ë³øìÏþlhww—¿ù›¿á~¯õZ¯Åýöoÿ6¯ó:¯Ãýþê¯þŠ—~é—æ¹ýõ_ÿ5¯ó:¯Ãîî.OúÓyðƒÌU/š×~í׿w~çwø­ßú-^ûµ_›çgww—¿ù›¿á~¯õZ¯Ås{ï÷~o¾ç{¾€¯úª¯â£?ú£ynýÑÍ×|Í×ðYŸõY|ög6WýÛýöoÿ6¯ó:¯Àg}ÖgñÙŸýÙ¼ ¿ó;¿Ãý^ê¥^ŠãÇó@~ðƒyÆ3žÀg}ÖgñÙŸýÙ<·ÝÝ]^çu^‡¿þë¿à³>ë³øìÏþl®ºêª«þ@¶ÍUW]uÕ€¯þê¯æc>æc8~ü8ßýÝßÍ[½Õ[ðÛ¿ýÛ|ÌÇ| ý× ÀG}ÔGñÕ_ýÕæc>†¿þë¿à£>ê£øê¯þjžIÜï·~ë·xí×~mè³?û³ùœÏùî÷ÑýÑ|ÔG}~ðƒø™Ÿù>ú£?š[o½€×z­×â·û·¹êEóÚ¯ýÚüÎïü¿õ[¿Åk¿ökóÂHâ~¿õ[¿Åk¿ökóÜ^ûµ_›ßùßà­ßú­ùª¯ú*üà³»»Ë÷|Ï÷ðÙŸýÙìîîðWõW¼ôK¿4W½h^ûµ_›ßùßÀ6ÿIÜÏ6Ïí³?û³ùœÏùî÷ÙŸýÙ¼×{½~ðƒÙÝÝåg~ægøèþhvwwx¯÷z/¾û»¿›«®øíßþm^çu^‡û½ôK¿4ÇçEñU_õU¼ôK¿4÷ûíßþm^çu^‡ûÙæùyðƒÌ3žñ Þú­ßš¯úª¯âÁ~0»»»|Ï÷|ýÑ À±cÇøë¿þküàsÕßýÝßÍû¼Ïûp¿×~í׿EõU_õU¼ôK¿4÷ûíßþm^çu^‡ûÙæ¹}ôG4_ó5_ÀñãÇùèþh>ê£>Šãdz»»ËÏüÌÏðÑýÑìîîðQõQ|õW5W=Ûoÿöoó:¯ó:Üï£?ú£ù¨ú(üàð3?ó3|ög6ý× ÀK½ÔKñÛ¿ýÛ?~œúíßþm^çu^‡ûÙæ¹íîîòà?˜K—.ðÞïýÞ|Ög}~ðƒÙÝÝåk¾ækøìÏþlŽ;Æ_ÿõ_óà?˜«ží§ú§y›·yî÷ÑýÑ|ÔG}~ðƒø™Ÿù>û³?›¿þë¿à¥^ê¥øíßþmŽ?Îýöoÿ6¯ó:¯ÃýlóÜÞú­ßšŸù™Ÿàøñã|ög6ïõ^ïÅñãÇÙÝÝå{¾ç{øìÏþlvwwø¬Ïú,>û³?›«^t¯ýÚ¯ÍïüÎïð[¿õ[¼ök¿6ÏÏoÿöoó:¯ó:ÜÏ6Ïmww—?øÁ\ºt €þèæ³>ë³8~ü8·Þz+_ó5_ÃWõWpìØ1n½õVŽ?ÎUÿv¿ýÛ¿Íë¼ÎëðYŸõY|ög6/ˆ$î÷[¿õ[¼ök¿6ôÓ?ýÓ¼ÍÛ¼ ÷{ï÷~o>ë³>‹?øÁüÌÏü ŸýÙŸÍ_ÿõ_ðR/õRüõ_ÿ5W]uÕUÿG Ûæª«®ºê?È{¿÷{ó=ßó=¼0¯õZ¯Åoÿöoó‚Hâ~¿õ[¿Åk¿ökóÜvwwyí×~mþæoþ†滾ë»xï÷~o®zþÞû½ß›ïùžïá…y¯÷z/¾û»¿›D÷û­ßú-^ûµ_›çöÞïýÞ|Ï÷|ÿ’×z­×â§ú§9~ü8W½h^ûµ_›ßùßà·~ë·xí×~m^IÜï·~ë·xí×~mžÛ_ÿõ_óÚ¯ýÚ\ºt‰滾ë»xï÷~o®zѽök¿6¿ó;¿€mþ%’¸ŸmžŸ÷~ï÷æ{¾ç{ø—¼×{½ßýÝßÍUÏöÖoýÖüÌÏü ÿ¿õ[¿Åk¿öks¿ßþíßæu^çu¸ŸmžŸ¿þë¿æµ_ûµ¹té/ÌOýÔOñÖoýÖ\õloýÖoÍÏüÌÏðoñ[¿õ[¼ök¿6÷ûíßþm^çu^‡ûÙæùyë·~k~æg~†É{½×{ñÝßýÝ\õ¼¾û»¿›÷yŸ÷á_òR/õR|÷w7/ýÒ/Ísûíßþm^çu^‡ûÙæùùë¿þk^ûµ_›K—.ñ‚;vŒßþíßæ¥_ú¥¹êy}÷w7ïó>ïÿä¥^ê¥øíßþmŽ?Îsûíßþm^çu^‡ûÙæ¹íîîòÖoýÖüÎïüÿ’÷z¯÷⻿û»¹ê_çµ_ûµùßù~ë·~‹×~í׿ùùíßþm^çu^‡ûÙæùùë¿þk^ûµ_›K—.ñ‚;vŒßþíßæ¥_ú¥¹êßç·û·y×y>ë³>‹ÏþìÏæ‘Äý~ë·~‹×~í׿¹}÷w7ïó>ïÿä¥^ê¥øíßþmŽ?ÎUW]uÕÿȶ¹êª«®úôÕ_ýÕ|õW5ÏxÆ3x cÇŽñÞïýÞ|ög6Çç‘Äý~ë·~‹×~í׿ùÙÝÝå³?û³ùš¯ùžÛƒô ¾ú«¿š·~ë·æªî«¿ú«ùìÏþl.]ºÄ;vŒþèæ³?û³ya$q¿ßú­ßâµ_ûµy~~ú§šÏþìÏæoþæoxnzЃøèþh>ú£?š«þu^ûµ_›ßùßà·~ë·xí×~m^IÜï·~ë·xí×~mžŸÝÝ]>ú£?šïùžïá¹=èAâ«¿ú«yë·~k®ú×yí×~m~çw~ÛüK$q?Û¼ ?ýÓ?ÍGôGóŒg<ƒçö =ˆÏþìÏæ½ßû½¹ê9½ök¿6¿ó;¿Ã¿ÅoýÖoñÚ¯ýÚÜï·û·y×yîg›äÖ[oå³?û³ùžïùžÛK½ÔKñÕ_ýÕ¼ök¿6W=§'N°»»Ë¿ÅoýÖoñÚ¯ýÚÜï·û·y×yîg›仿û»ùìÏþlžñŒgðÜ^ê¥^ŠÏþìÏæ­ßú­¹êûë¿þk>ú£?šßùßá¹;vŒþèæ£?ú£9~ü8ÏÏoÿöoó:¯ó:ÜÏ6/È­·ÞÊ{¿÷{ó;¿ó;<·×z­×â«¿ú«yé—~i®zÁ~û·›ÏþìÏæw~çwxnÇŽã£?ú£ùèþhŽ?ÎóóÛ¿ýÛ¼Îë¼÷³Í òÙŸýÙ|÷w7ÏxÆ3xn/õR/ÅWõWóÚ¯ýÚ\õ¯÷Ú¯ýÚüÎïü¿õ[¿Åk¿ökóüüöoÿ6¯ó:¯Ãýló‚üõ_ÿ5ýÑÍïüÎïðÜ^ëµ^‹¯þê¯æ¥_ú¥¹êßï·û·y×y>ë³>‹ÏþìÏæ‘Äý~ë·~‹×~í׿ùùíßþm>û³?›ßùßá¹=èAâ½ßû½ùìÏþl®ºêª«þA¶ÍUW]uÕ‚¿þë¿fww—ÝÝ]Ž?Îk¿ökóŸaww—¿þë¿fww—ãÇsüøq^ú¥_š«þuþú¯ÿšÝÝ]vww9~ü8¯ýگ͆[o½•[o½•ÝÝ]Ž?ÎñãÇyé—~i®úŸiww—¿þë¿æÖ[oåÁ~0Çç¥_ú¥¹ê¦[o½•[o½•[o½•?øÁ?~œ—~é—æªÿ™vwwùë¿þkvww9~ü8~ðƒyðƒÌUÿ3ýõ_ÿ5»»»Üzë­<øÁæÁ~0~ðƒ¹êEwë­·rë­·ò×ý×¼ôK¿4Çç¥_ú¥ùϰ»»Ë_ÿõ_³»»ËñãÇyðƒ̃ü`®zÑÝzë­Üzë­üõ_ÿ5/ýÒ/ÍñãÇyé—~iþ3üõ_ÿ5»»»ìîîrüøqüàóà?˜«þgºõÖ[¹õÖ[¹õÖ[yðƒ̃ü`üàsÕÿ»»»üõ_ÿ5·Þz+~ðƒ9~ü8/ýÒ/ÍUW]uÕÿQȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêªgùîïþnžñŒgð^ïõ^<øÁæ¿ÒWõWséÒ%>ê£>ŠãÇsÕUW]uÕUW]uÕUWý« Ûæª«®ºêª«®ºêª«®z¯ó:¯Ã•¯úª¯â¥_ú¥ùë¿þk>æc>†û½ÔK½_ýÕ_ÍUÿµ~ú§š·y›·à¥^ê¥øë¿þkþ«}õW5ó1À[¿õ[óS?õS\uÕUW]uÕUW]uÕUÿ*ȶ¹êª«®ºêª«®ºêª«žƒ$þ«üÖoý¯ýÚ¯Íoÿöoó:¯ó:Üïµ^ëµøíßþm®ú¯³»»ËCòvwwø­ßú-^ûµ_›ÿ~ðƒyÆ3žÀW}ÕWñÑýÑ\uÕUW]uÕUW]uÕU/2dÛ\uÕUW]uÕUW]uÕUÏAÿU~ë·~‹×~í׿·û·y×yî÷Z¯õZüöoÿ6Wý×y×y~û·€×z­×â·û·ùïòÓ?ýÓ¼ÍÛ¼ Çç¯þê¯xðƒÌUW]uÕUW]uÕUW]õ"A¶ÍUW]uÕUW]uÕUW]õ$ñ_å·~ë·xí×~m~û·›×y×á~¯õZ¯ÅoÿöosÕŸþéŸæmÞæm¸ßÓŸþtüàóßéµ_ûµùßùÞú­ßšŸú©Ÿâª«®ºêª«®ºêª«®z‘ Ûæª«®ºêª«®ºêª«®z¿ýÛ¿Í‹ê£?ú£ù›¿ùè·~ë·xQ½ôK¿4Çç·û·y×yî÷Z¯õZüöoÿ6WýçÛÝÝåe^æe¸õÖ[x¯÷z/¾û»¿›ÿn¿ýÛ¿Íë¼Îëp¿ßú­ßâµ_ûµ¹êª«®ºêª«®ºêª«þEȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßálsÕÿŸýÙŸÍç|Îçp¿§?ýé<øÁæ‚×~í׿w~çwxðƒÌÓŸþt®ºêª«®ºêª«®ºêª²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Û\õ¿Ãîî.yÈCØÝÝà½Þë½øîïþnþ§øíßþm^çu^‡û}×w}ïýÞïÍUW]uÕUW]uÕUW]õB!Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²ÍUÿ;|ög6Ÿó9ŸÃý~ë·~‹×~í׿’?øÁ<ãÏàÁ~0Oúӹꪫ®ºêª«®ºêª«^(dÛ\uÕUW]uÕUW]uÕUÿf¯ýÚ¯ÍïüÎïð@¶¹ê¾ÝÝ]ò‡°»» Àƒô n½õVþ§ùê¯þj>æc>†û}×w}ïýÞïÍUW]uÕUW]uÕUW]õ!Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Ö­·ÞÊ÷|Ï÷p¿=èA¼÷{¿7ÏÏw÷wóŒg<ƒû}Ög}÷ÛÝÝåg~ægøîïþn~û·›—~é—æøñã¼ök¿6ïõ^ïŃü`žŸÝÝ]¾ç{¾‡ŸþéŸfww—¿þë¿æÁ~0/ýÒ/Ík¿ökó^ïõ^?~œ«Ÿù™Ÿá·û·ùë¿þkn½õVn½õV^ú¥_šãÇóÒ/ýÒ¼õ[¿5¯õZ¯Å•ïþîïæ}Þç}¸ßW}ÕWñÑýÑük}Ï÷|¿ýÛ¿Í­·ÞÊ_ÿõ_³»»Ëƒü`üàsüøqÞú­ßš×z­×âÁ~0ÿ·Þz+yÈC¸ßK¿ôKóWõW\uÕUW]uÕUW]uÕU/²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡ûÙ໿û»ù˜ùvwwya¾ë»¾‹÷~ï÷æ>çs>‡ÏþìÏæ…9~ü8ßõ]ßÅ[¿õ[ó¯ñ=ßó=|ög6·Þz+ÿ’?øÁ|×w}¯ýگͶ—y™—á¯ÿú¯¹ßÓŸþtüàó¢úš¯ù>û³?›ÝÝ]^ïýÞïÍg}Ögñà?˜­—~é—æoþæo¸ß_ýÕ_ñÒ/ýÒ\uÕUW]uÕUW]uÕUϲm®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡ûÙæ}Þç}øîïþn^TõQÅWõWð:¯ó:üöoÿ6/ªŸú©Ÿâ­ßú­ù—ìîîò>ïó>üôOÿ4ÿZïýÞïÍw}×wñŸå¯ÿú¯y™—yî÷ =ˆ[o½•Õû¼ÏûðÝßýÝük?~œßú­ßâ¥_ú¥ù×øìÏþl>çs>‡û½×{½ßýÝßÍUW]uÕUW]uÕUW]õ|!Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßW}ÕWñ1ó1<Ѓô üàð;¿ó;æc>†û½Ök½¿ýÛ¿Í¿ä½ßû½ùžïùîwüøq.^¼È¿Æk¿ökó;¿ó;Üï³>ë³øìÏþl®ºêª«®ºêª«®ºêªçl›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6ÿZ¿ýÛ¿Íë¼Îëp¿×z­×â·û·y~^ûµ_›ßùßáÞë½Þ‹ïþîïæ…ùéŸþiÞæmÞ†çö[¿õ[¼ök¿6/ÌGôGó5_ó5ÜïÁ~0OúÓy~^æe^†¿þë¿æ~¯õZ¯Åoÿöoó¢ØÝÝå¥_ú¥yÆ3žÁý>ë³>‹ÏþìÏæ?Êoÿöoó:¯ó:çs¸ßƒô n½õVþ5vww9qâô[¿õ[¼ök¿6ÿÞû½ß›ïùžïá~ïõ^ïÅw÷wó¢øìÏþl>çs>‡û?~œŸú©Ÿâµ_ûµùÏöÛ¿ýÛ¼Îë¼d›«®ºêª«®ºêª«®ºêy Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßk½ÖkñÛ¿ýÛ¼($ñ@ßõ]ßÅ{¿÷{ó/ùíßþm^çu^‡²Ís{í×~m~çw~‡û}ÔG}_ýÕ_Í¿ÖK¿ôKó7ó7Üï«¾ê«øèþhþ#¼ök¿6¿ó;¿Ãý>ë³>‹ÏþìÏæEñÛ¿ýÛ¼Îë¼Ïí­ßú­yë·~kÞê­ÞŠãÇóŸá·û·y×yè¯þê¯xé—~i®ºêª«®ºêª«®ºêªç€l›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6ÿZ¿ýÛ¿Íë¼Îëp¿×z­×â·û·y~^ûµ_›ßùßá~ŸõYŸÅgögó¢ÄýÖoý¯ýگͿä·û·y×yÈ6ÏMôÞïýÞ¼÷{¿7ÿZýÑÍ_ÿõ_s¿÷z¯÷⻿û»ùð2/ó2üõ_ÿ5÷û¬Ïú,>û³?›Õk¿ökó;¿ó;¼ /ýÒ/Í[¿õ[óÚ¯ýÚ¼Ök½ÿQn½õVò‡ð@¿õ[¿Åk¿öksÕUW]uÕUW]uÕUW=dÛ\uÕUW]uÕUW]uÕUÿf¯ýÚ¯ÍïüÎïð@¶ù×úíßþm^çu^‡û½Ök½¿ýÛ¿ÍóóÚ¯ýÚüÎïü÷û¬Ïú,>û³?›…$è·~ë·xí×~mþ%¿ýÛ¿Íë¼Îëð@¶yn’øÏðZ¯õZüöoÿ6ÿ$ñ@ŸõYŸÅgögó¢ÚÝÝåÁ~0—.]â_rüøq^ûµ_›·~ë·æ­Þê­8~ü8ÿ’x ßú­ßâµ_ûµ¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍ‹Bô[¿õ[¼ök¿6ÿ’ßþíßæu^çux Û<7Iügx­×z-~û·›ÿ’x Ïú¬Ïâ³?û³ùרÝÝå½ßû½ù™Ÿù^TÇç­ßú­ùª¯ú*Ž?ο…$è·~ë·xí×~m®ºêª«®ºêª«®ºêªç€l›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6ÿZ¿ýÛ¿Íë¼Îëp¿×z­×â·û·y~^ûµ_›ßùßá~ŸõYŸÅgögó¢ÄýÖoý¯ýگͿä·û·y×yÈ6ô×ý×¼Ì˼ ÿ^ëµ^‹ßþíßæ?‚$è«¾ê«øèþhþ-n½õV¾ú«¿šŸþéŸæÏx/ŠãÇó[¿õ[¼ôK¿4ÿZ’x ßú­ßâµ_ûµ¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍ‹Bô[¿õ[¼ök¿6ÿ’ßþíßæu^çux Û<7I<ÐW}ÕWñÒ/ýÒü{?~œ—~é—æ?‚$è³>ë³øìÏþlþ½þú¯ÿšßþíßæ§ú§ùßù^˜ãÇó[¿õ[¼ôK¿4ÿ’x ßú­ßâµ_ûµ¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍ‹Bô[¿õ[¼ök¿6ÿ’ßþíßæu^çux Û<7I<Ðw}×wñÞïýÞüOòÚ¯ýÚüÎïü÷û¬Ïú,>û³?›ÿh¿ýÛ¿ÍOÿôOóÓ?ýÓ<ãÏ๽×{½ßýÝßÍ‹ê·û·y×yè·~ë·xí×~m®ºêª«®ºêª«®ºêªç€l›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6ÿZ¿ýÛ¿Íë¼Îëp¿×z­×â·û·y~^ûµ_›ßùßá~ŸõYŸÅgögó¢ÄýÖoý¯ýگͿä·û·y×yÈ6ÏíÁ~0ÏxÆ3¸ßG}ÔGñÕ_ýÕüOòÚ¯ýÚüÎïü÷{¯÷z/¾û»¿›ÿL¿ýÛ¿Í[¿õ[séÒ%È6/ªßþíßæu^çux ¿ú«¿â¥_ú¥¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍ‹Bô[¿õ[¼ök¿6ÿ’ßþíßæu^çux Û<·÷~ï÷æ{¾ç{¸ßK¿ôKóWõWük}÷w7’xðƒ̃ô üàóå½ßû½ùžïùî÷Z¯õZüöoÿ6/ŠÝÝ]þæoþ†[o½Û¼÷{¿7/ªïþîïæ}Þç}x ¿ú«¿â¥_ú¥yQüöoÿ6¯ó:¯ÃÙæª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóܾû»¿›÷yŸ÷á~ë·~‹×~í׿Euë­·ò‡<„úª¯ú*>ú£?šÿŸýÙŸÍç|Îçp¿×z­×â·û·ù—üôOÿ4oó6oÃÙæEõ×ý×¼Ì˼ ô[¿õ[¼ök¿6/Нþê¯æc>æc¸ßƒô n½õV®ºêª«®ºêª«®ºêªçl›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6ÿZ¿ýÛ¿Íë¼Îëp¿×z­×â·û·y~^ûµ_›ßùßá~ŸõYŸÅgögó¢ÄýÖoý¯ýگͿä·û·y×yÈ6Ïmww—?øÁ\ºt‰û½ôK¿4õWÅ‹êmÞæmøéŸþièéO:~ðƒùðÓ?ýÓ¼ÍÛ¼ d›Éîî.'Nœà¾ë»¾‹÷~ï÷æEñÛ¿ýÛ¼Îë¼d›Õ{¿÷{ó=ßó=Üï­Þê­øéŸþi®ºêª«®ºêª«®ºêªçl›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6ÿZ¿ýÛ¿Íë¼Îëp¿×z­×â·û·y~^ûµ_›ßùßá~ŸõYŸÅgögó¢ÄýÖoý¯ýگͿä·û·y×yÈ6ÏÏgögó9Ÿó9<Ð{¿÷{ó]ßõ]üK¾û»¿›÷yŸ÷áÞë½Þ‹ïþîïæ?Êîî.'Nœà~ë·~‹×~í׿_òÖoýÖüÌÏü ÷;~ü8OúÓ9~ü8ÿ’×y×á·û·¹ßƒô n½õV^T/ó2/Ã_ÿõ_s¿¯úª¯â£?ú£¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍ‹Bô[¿õ[¼ök¿6ÿ’ßþíßæu^çux Ûë³>‹ÏþìÏæE!‰ú­ßú-^ûµ_›Éoÿöoó:¯ó:çs¸ßG}ÔGñÕ_ýÕ\uÕUW]uÕUW]uÕUϲm®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍ‹Bô[¿õ[¼ök¿6ÿ’ßþíßæu^çux ÛüKvwwùê¯þj¾ú«¿šK—.ñ¢x­×z-¾ú«¿š—~é—æ?Óƒü`žñŒgp¿¿ú«¿â¥_ú¥yQ}ög6_ýÕ_Í¥K—xQ;vŒÏþìÏæ£?ú£ù×z™—yþú¯ÿšûýÕ_ý/ýÒ/ÍUW]uÕUW]uÕUW]õ|!Ûæª«®ºêª«®ºêª«®ú7ûîïþnn½õVè³?û³ù׺õÖ[ùîïþnî÷à?˜÷~ï÷æùùîïþnn½õVî÷Ú¯ýÚ¼ök¿6/ŠÏþìÏæÞû½ß›?øÁüKn½õV¾û»¿›úìÏþl^T»»»üôOÿ4?ýÓ?Í_ÿõ_óŒg<ƒz­×z-^ú¥_š÷~ï÷æ¥_ú¥ù¯ðÕ_ýÕ|ÌÇ| ÷û¨ú(¾ú«¿šÝÝ]~ú§šŸþéŸæÖ[oåoþæox —z©—â¥_ú¥yí×~mÞú­ßšãÇó¯õ×ý×¼Ì˼ ÷{ЃÄ­·ÞÊUW]uÕUW]uÕUW]õ!Ûæª«®ºêª«®ºêª«®ºêÿ¡ÝÝ]üàséÒ%Ž?ÎÅ‹ùŸæ£?ú£ùš¯ùî÷]ßõ]¼÷{¿7W]uÕUW]uÕUW]uÕ „l›«®ºêª«®ºêª«®ºêªÿ§Þû½ß›ïùžïá~ßõ]ßÅ{¿÷{ó?ɉ'ØÝÝàØ±cìîîrÕUW]uÕUW]uÕUW½Pȶ¹êª«®ºêª«®ºêª«®úêÖ[oå!y÷{í×~m~ë·~‹ÿ)¾û»¿›÷yŸ÷á~ŸõYŸÅgögsÕUW]uÕUW]uÕUW½Pȶ¹êª«®ºêª«®ºêª«®úì£?ú£ùš¯ùî÷ô§??øÁüOð:¯ó:üöoÿ6zЃøë¿þkŽ?ÎUW]uÕUW]uÕUW]õB!Ûæª«®ºêª«®ºêª«®ºêÿ±ÝÝ]üàséÒ%Þë½Þ‹ïþîïæ¿Ûoÿöoó:¯ó:Ü﻾ë»xï÷~o®ºêª«®ºêª«®ºêª²m®ºêª«®ºêª«®ºêª«þŸûê¯þj>æc>†û=ýéOçÁ~0ÿ^çu^‡ßþíßà¥^ê¥øë¿þk®ºêª«®ºêª«®ºêª ²m®ºêª«®ºêª«®ºêª«®â¥_ú¥ù›¿ùÞú­ßšŸú©Ÿâ¿Ëoÿöoó:¯ó:Üï·~ë·xí×~m®ºêª«®ºêª«®ºêª ²m®ºêª«®ºêª«®ºêª«®â¯ÿú¯y™—yî÷[¿õ[¼ök¿6ÿò‡pë­·ðQõQ|õW5W]uÕUW]uÕUW]uÕ‹ Ù6W]uÕUW]uÕUW]uÕUW]öÕ_ýÕ|ÌÇ| ¯ýÚ¯ÍoýÖoñ_í»¿û»yŸ÷y^ê¥^Š¿þë¿æª«®ºêª«®ºêª«®úWA¶ÍUW]uÕUW]uÕUW]uÕUÏòÕ_ýÕìîîðÑýÑ?~œÿJßýÝßÍ­·Þ À[¿õ[óÒ/ýÒ\uÕUW]uÕUW]uÕUÿ*ȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿþSèKšë>ÞëIEND®B`‚uv-0.9.17+ds1/assets/png/resolve-cold.png000066400000000000000000004427671520155276700201550ustar00rootroot00000000000000‰PNG  IHDR@è†{2ƒE¾IDATxíà$I’$I‹ª™»GDDfffVUUUUwwwww÷ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌtwwwwWWUUUUffFFD„»›™ ÏLfWwuwwOÏÌÌÌÌL¢l›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«þ_ÚÝÝåw~çwøë¿þk~û·€ßþíßà¥_ú¥9~ü8/ýÒ/ÍK¿ôKóVoõV?~œÿÉ~çw~‡z©—z)Ž?δÝÝ]þæoþ†û;vŒ—~é—æ?Ò­·ÞÊ3žñ þ#;vŒ—~é—æªÿ»»»üÍßü ôZ¯õZüotë­·òŒg<ƒû;vŒ—~é—æª«®ºêª«®ºêª«þ@¶ÍUW]uÕUW]uÕUÿ¯Üzë­|Îç|ßýÝßÍ¿Æ{¿÷{óQõQ¼ôK¿4ÿIâ~ë·~‹×~í׿?Úoÿöoó:¯ó:Üïµ^ëµøíßþmþ#}ög6Ÿó9ŸÃ¤×~í׿­ßú­y¯÷z/Ž?ÎUÿ9~û·›×y×áló?Í_ÿõ_süøqüàó‚|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæª«®ºêª«®ºêª«þ@¶ÍUW]uÕUW]uÕUÿ/ìîîò>ïó>üôOÿ4ÿŸýÙŸÍg}Ögñ?$è·~ë·xí×~mþ£ýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæ?Ògögó9Ÿó9üg8~ü8ßõ]ßÅ[¿õ[sÕ¼ßþíßæu^çux ÛüO±»»Ëç|ÎçðÕ_ýÕüÖoý¯ýÚ¯Í òÙŸýÙ|Îç|÷{­×z-~û·›«®ºêª«®ºêª«®ú_Ù6W]uÕUW]uÕUWýŸ÷×ý×¼Îë¼»»»üGxí×~m~ê§~ŠãÇó?…$è·~ë·xí×~mþ£ýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæ?Ògögó9Ÿó9ügú®ïú.Þû½ß›«þcýöoÿ6¯ó:¯ÃÙæ‚ïùžïá£?ú£ÙÝÝà·~ë·xí×~m^ÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛ\uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêÿ´ïþîïæ}Þç}x~^ê¥^Š÷~ï÷æ¥_ú¥yí×~mè·û·ùë¿þk¾ú«¿šg<ã<·×~í׿·~ë·øŸBô[¿õ[¼ök¿6ÿÑ~û·›×y×á~¯õZ¯Åoÿöoóé³?û³ùœÏùè³>ë³ø×úíßþmþú¯ÿšK—.ñüüÕ_ý/ýÒ/ÍUÿq~û·›×y×áló?$è·~ë·xí×~m^ÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛ\uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêÿ¬¿þë¿æu^çuØÝÝå^ê¥^Нþê¯æµ_ûµyQ|ög6Ÿó9ŸÃsû¨ú(¾ú«¿šÿ $ñ@¿õ[¿Åk¿ökóí·û·y×yî÷Z¯õZüöoÿ6ÿ‘>û³?›ÏùœÏálóoõÕ_ýÕ|ög6—.]â^ûµ_›ßú­ßâªÿ8¿ýÛ¿Íë¼Îëð@¶ùŸ@ô[¿õ[¼ök¿6/ÈoÿöoóÛ¿ýÛÜïÁ~0ïýÞïÍUW]uÕUW]uÕUWý/€l›«®ºêª«®ºêª«þOÚÝÝå!y»»»<Ð{½×{ñÕ_ýÕ?~œïþîïæ}Þç}xn¿õ[¿Åk¿ökóßMô[¿õ[¼ök¿6ÿÑ~û·›×y×á~¯õZ¯Åoÿöoóé³?û³ùœÏùÈ6ÿý×Ík¿ökséÒ%è¯þê¯xé—~i®úñÛ¿ýÛ¼Îë¼d›ÿ $ñ@¿õ[¿Åk¿öksÕUW]uÕUW]uÕUÿ!Ûæª«®ºêª«®ºêªÿ“>ú£?š¯ùš¯áÞë½Þ‹ïþîïæßê³?û³ùœÏùèµ_ûµù­ßú-þ»Iâ~ë·~‹×~í׿?Úoÿöoó:¯ó:Üïµ^ëµøíßþmþ#}ög6Ÿó9ŸÃÙæßë«¿ú«ù˜ùè³>ë³øìÏþl®úñÛ¿ýÛ¼Îë¼d›ÿ $ñ@¿õ[¿Åk¿öksÕUW]uÕUW]uÕUÿ!Ûæª«®ºêª«®ºêªÿsn½õVò‡ð@zЃøë¿þkŽ?οǃü`žñŒgð@õWÅK¿ôKóßIô[¿õ[¼ök¿6ÿÑ~û·›×y×á~¯õZ¯Åoÿöoóé³?û³ùœÏùÈ6ÿ^»»»œ8q‚z­×z-~û·›«þcüöoÿ6¯ó:¯ÃÙæI<ÐoýÖoñÚ¯ýÚ\uÕUW]uÕUW]uÕÿAȶ¹êª«®ºêª«®ºêÿœþèæk¾ækx ïú®ïâ½ßû½ù÷úîïþnÞç}Þ‡ú¨ú(¾ú«¿šÝÝ]þæoþ†[o½•[o½•?øÁ<øÁæ¥^ê¥8~ü8ÿZ’x ßú­ßâµ_ûµù×øë¿þkþæoþ†[o½•?øÁ<øÁæ¥^ê¥8~ü8÷ûíßþm^çu^‡û½Ök½¿ýۿͤÏþìÏæs>çsx ÛüGxðƒÌ3žñ î÷Z¯õZüöoÿ6ÿZ»»»üÍßü »»»üõ_ÿ5~ðƒyðƒ̃ô üàóáÖ[oåÏxý×Íîî./ýÒ/ÍñãÇyЃăü`þ£Üzë­<ãÏà¯ÿú¯ÙÝÝå¥_ú¥9~ü8¯õZ¯Å¿Öoÿöoó:¯ó:û³?›ÏùœÏálóáĉìîîr¿×z­×â·û·yQýôOÿ4ßó=ßÃOÿôOó‚¼ôK¿4ïýÞïÍG}ÔGñ¯µ»»Ë×|Í×ðÝßýÝÜzë­¼ ~ðƒyí×~m>ë³>‹?øÁükÝzë­|Ï÷|ßýÝßÍ­·ÞÊ òÖoýÖ|ÔG}¯ýگ͋â·û·y×yÈ6ÏÏgögó9Ÿó9Üïµ^ëµøíßþm^TŸýÙŸÍç|Îçp¿Ïú¬Ïâ³?û³y I¼¨>ë³>‹ÏþìÏæ~ŸýÙŸÍç|Îçp¿×z­×â·û·yQìîîò5_ó5|÷w7·Þz+/Èk¿ökóÞïýÞ¼×{½/*IÜïµ^ëµøíßþmn½õV>çs>‡ŸþéŸfww—çç­ßú­ù¨ú(^ûµ_›«®ºêª«®ºêª«þOC¶ÍUW]uÕUW]uÕUÿ§üöoÿ6¯ó:¯Ã}ÔG}_ýÕ_ͧßþíßæmÞæmØÝÝåEõà?˜ïú®ïâµ_ûµù—Hâ~ë·~‹×~í׿…ùéŸþiÞç}Þ‡ÝÝ]þ%Çç§~ê§x×yî÷Z¯õZüöoÿ6ÿ‘>û³?›ÏùœÏálóïµ»»Ë‰'x ×z­×â·û·ù—üõ_ÿ5ó1Ãoÿöoó¢zðƒÌOýÔOñÒ/ýÒ¼(~ú§š÷yŸ÷aww—þèæ«¾ê«xQ}Í×| ýÑÍ¿Æk¿ökó]ßõ]<øÁæ…ùíßþm^çu^‡²ÍóóÙŸýÙ|Îç|÷{­×z-~û·›Õgögó9Ÿó9Üï³>ë³øìÏþlH/ªÏú¬Ïâ³?û³¹ßgögó9Ÿó9Üïµ^ëµøíßþmþ%_ó5_Ãgög³»»Ë‹êµ_ûµùª¯ú*^ú¥_š‰$î÷Z¯õZüöoÿ6?ýÓ?Íû¼Ïû°»»Ë‹â£?ú£ùª¯ú*®ºêª«®ºêª«®ú? Ù6W]uÕUW]uÕUWýŸòÙŸýÙ|Îç|ôS?õS¼õ[¿5ÿ]¾ú«¿šù˜áß껾ë»xï÷~o^I<ÐoýÖoñÚ¯ýÚ¼ ßýÝßÍû¼Ïûð¯qüøq>ë³>‹ù˜á~¯õZ¯Åoÿöoóé³?û³ùœÏùÈ6ÿ^ŸýÙŸÍç|Îçð@ŸõYŸÅgögóÂüõ_ÿ5¯ó:¯Ãîî.ÿßõ]ßÅ{¿÷{óÂüôOÿ4oó6oÿÕG}ÔGñÕ_ýÕüKÞæmÞ†ŸþéŸæßâøñãüÖoý/ýÒ/Í òÛ¿ýÛ¼Îë¼d›çç³?û³ùœÏùî÷Z¯õZüöoÿ6/ªÏþìÏæs>çs¸ßg}ÖgñÙŸýÙ<$^TŸõYŸÅgögs¿ÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛ¼0ïó>ïÃw÷wóoqüøq~ê§~Š×~í׿…‘Äý^ëµ^‹÷~ï÷æ}Þç}ø×ú¨ú(¾ú«¿š«®ºêª«®ºêª«þOB¶ÍUW]uÕUW]uÕUÿ§¼õ[¿5?ó3?Ã]¼x‘ãÇóßỿû»yŸ÷yžÛ[½Õ[ñÖoýÖ¼ök¿6~ðƒùë¿þkþú¯ÿšïþîïæw~çwxnßõ]ßÅ{¿÷{ó‚Hâ~ë·~‹×~í׿ùùíßþm^çu^‡çöVoõV¼÷{¿7¯ýÚ¯ÍñãÇùíßþm~ú§šïþîïæÒ¥Kçs¸ßk½ÖkñÛ¿ýÛ¼¨>û³?›ÏùœÏá~ŸõYŸÅgögó@ŸýÙŸÍý>çs>‡z¯÷z/üàs¿×~í׿µ_ûµ¹ßgögó9Ÿó9Üïµ^ëµøíßþm^÷yŸ÷ỿû»ynïõ^ïÅ[¿õ[óÒ/ýÒ<øÁæ¯ÿú¯ùíßþm¾û»¿›¿ù›¿áŽ?ÎoýÖoñÒ/ýÒ¼ ’¸ßƒü`vwwÙÝÝàØ±c¼÷{¿7¯ýÚ¯ÍñãÇøíßþm¾û»¿›g<ã<·§?ýé<øÁ檫®ºêª«®ºêªÿsmsÕUW]uÕUW]uÕÿ)/ó2/Ã_ÿõ_s¿cÇŽ±»»Ë‡[o½•‡<ä!<бcÇøéŸþi^ûµ_›仿û»ùèþh.]ºÄýŽ?Î_ýÕ_ñà?˜çGô[¿õ[¼ök¿6ÏÏCòn½õV軾ë»xï÷~ožŸ[o½•·~ë·æoþæoxn¯õZ¯Åoÿöoóé³?û³ùœÏùÈ6ÿVßó=ßÃGôG³»»Ë½Ök½¿ýÛ¿Í ó2/ó2üõ_ÿ5ô]ßõ]¼÷{¿7/È_ÿõ_óÚ¯ýÚ\ºt‰û?~œ§?ýé?~œçöÝßýݼÏû¼÷;vì·Þz+Ççùë¿þk^æe^†z«·z+~ú§šç绿û»yŸ÷yè¥^ê¥øéŸþiüàó‚|ôG4_ó5_ýôK¿4õWÅóóÛ¿ýÛ¼Îë¼d›çç³?û³ùœÏùî÷Z¯õZüöoÿ6/ªÏþìÏæs>çs¸ßg}ÖgñÙŸýÙ¼ ’x ßú­ßâµ_ûµyA>û³?›ÏùœÏá~¯õZ¯Åoÿöoóü|÷w7ïó>ïÃ=èAâ§ú§yé—~i^¯þê¯æc>æcx ?øÁüÕ_ýÇçù‘Äóó^ïõ^|õW5Çç¹íîîòÑýÑ|Ï÷|ôQõQ|õW5W]uÕUW]uÕUWýŸƒl›«®ºêª«®ºêª«þO‘ĽÖk½¿ýۿ͇·y›·á§ú§y ¿ú«¿â¥_ú¥ù—üöoÿ6¯ó:¯Ã½×{½ßýÝßÍó#‰ú­ßú-^ûµ_›çöÝßýݼÏû¼ô]ßõ]¼÷{¿7/Ìîî./ýÒ/Í3žñ èµ^ëµøíßþmþ#}ög6Ÿó9ŸÃýöoÿ6ÿ·Þz+ý×Íoÿöoó×ý×<·cÇŽñ×ý×<øÁæùîïþnÞç}Þ‡ú®ïú.Þû½ß›É_ÿõ_óÚ¯ýÚ\ºt‰û}Ög}ŸýÙŸÍs{í×~m~çw~‡û}ÕW}ýÑÍ¿ä³?û³ùœÏùîwüøq.^¼ÈsÛÝÝåe^æe¸õÖ[¹ßƒô þú¯ÿšãÇó/ùìÏþl>çs>‡ú®ïú.Þû½ß›çöÛ¿ýÛ¼Îë¼d›çç³?û³ùœÏùî÷Z¯õZüöoÿ6/ªÏþìÏæs>çs¸ßg}ÖgñÙŸýÙ¼ ’x ßú­ßâµ_ûµyA>û³?›ÏùœÏá~¯õZ¯Åoÿöoóü<ä!áÖ[oå~ÇŽãÖ[oåøñãüK¾ú«¿šù˜á>ë³>‹ÏþìÏæù‘Äs{¯÷z/¾û»¿›Ƀü`žñŒgp¿?øÁ<ýéO窫®ºêª«®ºêªÿsmsÕUW]uÕUW]uÕÿ)’x ×z­×â·û·ù¯ö×ý×¼Ì˼ ôYŸõY|ög6/ªþèæk¾ækx §?ýé<øÁæ¹Iâ~ë·~‹×~í׿¹½õ[¿5?ó3?Ãý^ëµ^‹ßþíßæEñÓ?ýÓ¼ÍÛ¼ ôZ¯õZüöoÿ6ÿ‘>û³?›ÏùœÏá?˱cÇøíßþm^ú¥_šæ!y·Þz+÷{«·z+~ú§šÕw÷wó>ïó>Üïøñã<ýéOçøñã<Ðk¿ökó;¿ó;Üï·~ë·xí×~mþ%·Þz+yÈC8vì/ýÒ/̓ü`¾ú«¿šãÇó@_ýÕ_ÍÇ|ÌÇð@¿õ[¿Åk¿ökó¢zé—~iþæoþ†û=øÁæéO:Ïí·û·y×yÈ6ÏÏgögó9Ÿó9Üïµ^ëµøíßþm^TŸýÙŸÍç|Îçp¿Ïú¬Ïâ³?û³yA$ñ@¿õ[¿Åk¿ökó‚|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæ¹}÷w7ïó>ïÃýÔOýoýÖoÍ‹êµ_ûµùßùîwüøq.^¼Èó#‰:vì·Þz+Çç_òÙŸýÙ|Îç|d›«®ºêª«®ºêª«þÏA¶ÍUW]uÕUW]uÕUÿ§Hâ^ëµ^‹ßþíßæ¿ÚWõWó1ó1<ÐÅ‹9~ü8/ªÝÝ]Nœ8Á}Ög}ŸýÙŸÍs“ÄýÖoý¯ýÚ¯ÍíîîrâÄ è§~ê§xë·~k^T~ðƒyÆ3žÁý^ëµ^‹ßþíßæ?Ògögó9Ÿó9ügx«·z+¾ú«¿š?øÁ¼0ý×Í˼ÌËð@õWÅK¿ôKó¢ÚÝÝåĉ<ÐOýÔOñÖoýÖ<Ðk¿ökó;¿ó;Üï­ßú­ù©Ÿú)þ£¼õ[¿5?ó3?Ãý^ê¥^Š¿þë¿æ_ã§ú§y›·yè·~ë·xí×~mè·û·y×yÈ6ÏÏgögó9Ÿó9Üïµ^ëµøíßþm^TŸýÙŸÍç|Îçp¿Ïú¬Ïâ³?û³yA$ñ@¿õ[¿Åk¿ökó‚|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæ¹½÷{¿7ßó=ßÃýô që­·ò¯ñÛ¿ýÛ¼Îë¼ô[¿õ[¼ök¿6ÏMôQõQ|õW5/Šßþíßæu^çux Û\uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ú?EôZ¯õZüöoÿ6ÿÕÞú­ßšŸù™Ÿá~oõVoÅOÿôOó¯õÖoýÖüÌÏü ÷{­×z-~û·›ç&‰ú­ßú-^ûµ_›úéŸþiÞæmÞ†²Í¿ÆGôGó5_ó5Üïµ^ëµøíßþmþ#}ög6Ÿó9ŸÃ”=èA¼õ[¿5ïýÞïÍK¿ôKó¢øê¯þj>æc>†û=èAâÖ[oå_ëµ_ûµùßùî÷YŸõY|ög6ôÑýÑ|Í×| ôÖoýÖ|ÕW}~ðƒù÷:qâ»»»Üï«¾ê«øèþhþµŽ?Î¥K—¸ßg}ÖgñÙŸýÙ<Ðoÿöoó:¯ó:çs¸ßk½ÖkñÛ¿ýÛ¼¨>û³?›ÏùœÏá~ŸõYŸÅgögó‚Hâ~ë·~‹×~í׿ùìÏþl>çs>‡û½Ök½¿ýÛ¿Ís;qâ»»»Üï³>ë³øìÏþlþµüàóŒg<ƒû}Ög}ŸýÙŸÍs“Ä}×w}ïýÞïÍ‹â·û·y×yÈ6W]uÕUW]uÕUWýŸƒl›«®ºêª«®ºêª«þO‘ĽÖk½¿ýÛ¿Í5I<Ðg}ÖgñÙŸýÙük}ög6Ÿó9ŸÃýŽ?ÎÅ‹yn’x ßú­ßâµ_ûµy ÏþìÏæs>çs¸ßK½ÔKñ×ý×üküôOÿ4oó6oÃý^ëµ^‹ßþíßæ?Ògögó9Ÿó9<Ðk½Ökñ‚Üzë­<ãÏ๽ök¿6ßõ]ßŃü`þµÞú­ßšŸù™Ÿá~¯õZ¯Åoÿöoó¯õÙŸýÙ|Îç|÷{­×z-~û·›ºõÖ[yÈCÂóóÒ/ýÒ¼ök¿6oýÖoÍk½Ökñ¯õÛ¿ýÛ¼Îë¼ô[¿õ[¼ök¿6ÿZ¯ýÚ¯ÍïüÎïp¿·z«·â§ú§y ßþíßæu^çux Ûë³>‹ÏþìÏæ‘ÄýÖoý¯ýÚ¯Í òÙŸýÙ|Îç|÷{­×z-~û·›úë¿þk^æe^†ú­ßú-^ûµ_›­·~ë·æg~æg¸ßk½ÖkñÛ¿ýÛ<7I<ÐoýÖoñÚ¯ýÚ¼(vww9qâtñâEŽ?ÎUW]uÕUW]uÕUÿ§ Ûæª«®ºêª«®ºêªÿS^ú¥_š¿ù›¿á~¯õZ¯Åoÿöoó_Mô[¿õ[¼ök¿6ÿZ¿ýÛ¿Íë¼Îëð@¶yn’x ßú­ßâµ_ûµy ÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛüküöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæ?Ògögó9Ÿó9çs>‡û½Ök½¿ýÛ¿Ís“ÄýÖoý¯ýگ͋Jô[¿õ[¼ök¿6W]uÕUW]uÕUWýŸ‚l›«®ºêª«®ºêª«þOyí×~m~çw~‡²Í¥ÝÝ]Nœ8ÁýÖoý¯ýگͿÖoÿöoó:¯ó:çs>‡²Í‹â»¿û»yŸ÷yžÛw}×wñÞïýÞ¼¨ò‡pë­·òŸÁ6ÏÏGôGó5_ó5¼¨^ú¥_š÷~ï÷æ½Þë½8~ü8ÏÏOÿôOó6oó6û³?›ÏùœÏá~¯õZ¯Åoÿöoó@¿ýÛ¿Íë¼Îëð@¶y~>û³?›ÏùœÏá~¯õZ¯Åoÿöoó¢úìÏþl>çs>‡û}Ög}ŸýÙŸÍ "‰ú­ßú-^ûµ_›ä³?û³ùœÏùî÷Z¯õZüöoÿ6ôÛ¿ýÛ¼Îë¼d›‹ÏþìÏæs>çs¸ßñãǹxñ"ÏMtñâEŽ?΋Jô[¿õ[¼ök¿6W]uÕUW]uÕUWýŸ‚l›«®ºêª«®ºêª«þOùìÏþl>çs>‡zúӟ΃ü`þ«üöoÿ6¯ó:¯ÃýÖoý¯ýگͿÖoÿöoó:¯ó:<Ð_ýÕ_ñÒ/ýÒ<$è·~ë·xí×~mèµ_ûµùßùî÷YŸõY|ög6ÿZ’¸ßk½ÖkñÛ¿ýÛüGúìÏþl>çs>‡²Í‹ê»¿û»yŸ÷yžÛOýÔOñÖoýÖ¼($ñŸÅ6/ÈoÿöoóÕ_ýÕüÌÏü /ªãÇóÑýÑ|Ög}Ïí³?û³ùœÏùÈ6ÿŸýÙŸÍç|Îçp¿cÇŽ±»»Ëýöoÿ6¯ó:¯ÃÙæùùìÏþl>çs>‡û½Ök½¿ýÛ¿Í‹ê³?û³ùœÏùî÷YŸõY|ög6/ˆ$è·~ë·xí×~m^ÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛ<Ðoÿöoó:¯ó:û³?›ÏùœÏálóÜ$ñ@¶ù×ÄýÖoý¯ýÚ¯ÍUW]uÕUW]uÕUÿ§ Ûæª«®ºêª«®ºêªÿS~ú§š·y›·á¾ë»¾‹÷~ï÷æ?Êw÷wó9Ÿó9¼ök¿6¯ýÚ¯Ík½Ökñà?˜ûíîîrâÄ è·~ë·xí×~mþµ~û·›×y×álóÜ$ñ@¿õ[¿Åk¿ökó@¯ýÚ¯ÍïüÎïp¿÷z¯÷⻿û»ù×’Äý^ëµ^‹ßþíßæ?Ògögó9Ÿó9ú£?š¯ùš¯áŽ?ÎoýÖoñÒ/ýÒüK$ñ@zЃxðƒÌ„ßþíßæ_rë­·òÓ?ýÓüöoÿ6?ó3?Ëâ½ßû½ù®ïú.è§ú§y›·yÈ6ÿŸýÙŸÍç|Îçp¿×z­×â·û·y ßþíßæu^çux Ûë³>‹ÏþìÏæ‘ÄýÖoý¯ýÚ¯Í òÙŸýÙ|Îç|÷{­×z-~û·›úíßþm^çu^‡²Í¿Ågögó9Ÿó9û³?›ÏùœÏáló¯õÒ/ýÒüÍßü ôÒ/ýÒüÖoýÇç…yí×~m~çw~‡û}Ög}ŸýÙŸÍ—ßþíßæ·û·ùéŸþiþæoþ†仾ë»xï÷~oî÷Û¿ýÛ¼Îë¼d›‹ÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛ<Ðoÿöoó:¯ó:çs¸ßk½ÖkñÛ¿ýÛ¼¨^ûµ_›ßùßá~ŸõYŸÅgögó‚Hâ~ë·~‹×~í׿ùìÏþl>çs>‡û½Ök½¿ýÛ¿Íýöoÿ6¯ó:¯ÃÙæßâ³?û³ùœÏùî÷Z¯õZüöoÿ6ÏMd› I<ÐoýÖoñÚ¯ýÚ\uÕUW]uÕUW]õ ²m®ºêª«®ºêª«®ú?ç­ßú­ù™ŸùèéO:~ðƒù÷ÚÝÝå!y»»»ÜïAz·Þz+$‰úª¯ú*>ú£?š­ÏþìÏæs>çs¸ß±cÇØÝÝå¹Iâ~ë·~‹×~í׿>û³?›ÏùœÏá~/ýÒ/Í_ýÕ_ñ¯ñÛ¿ýÛ¼Îë¼÷{­×z-~û·›ÿHŸýÙŸÍç|Îçð@¶ù×úë¿þk^æe^†çöQõQ|õW5/Ìk¿ökó;¿ó;Üï½Þë½øîïþnþ'ØÝÝå§ú§ùìÏþlžñŒgð@¯ýÚ¯ÍoýÖoq¿ßþíßæu^çux ¿ú«¿â¥_ú¥ù×zí×~m~çw~‡û½Õ[½?ýÓ?Íýöoÿ6¯ó:¯ÃÙæùùìÏþl>çs>‡û½Ök½¿ýÛ¿Í‹êµ_ûµùßùî÷YŸõY|ög6/ˆ$è·~ë·xí×~m^ÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛ<Ð_ÿõ_ó2/ó2<ÐoýÖoñÚ¯ýÚük½õ[¿5?ó3?Ãý^ëµ^‹ßþíßæ¹Iâló¯!‰ú­ßú-^ûµ_›«®ºêª«®ºêª«þOA¶ÍUW]uÕUW]uÕUÿçüôOÿ4oó6oÃ}Ög}ŸýÙŸÍ¿×gögó9Ÿó9<ÐG}ÔGñÕ_ýÕ<Ðk¿ökó;¿ó;Üï½Þë½øîïþnþµÞú­ßšŸù™Ÿá~¯õZ¯ÅoÿöoóÜ$ñ@¿õ[¿Åk¿ökó@?ýÓ?ÍÛ¼ÍÛð@¶ù×øê¯þj>æc>†û½Ök½¿ýۿͤÏþìÏæs>çsx Ûü[|ög6Ÿó9ŸÃsû­ßú-^ûµ_›ä£?ú£ùš¯ùî÷à?˜§?ýéüO²»»Ëk¿ökó7ó7çs>‡û½Ök½¿ýÛ¿Í‹êe^æeøë¿þkî÷YŸõY|ög6/ˆ$è·~ë·xí×~m^ÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛ<·ãÇséÒ%î÷U_õU|ôG4ÿZyÈC¸õÖ[¹ßG}ÔGñÕ_ýÕ<7Içs>‡û?~œ§?ýé?~œÕîî.'Nœà>ë³>‹ÏþìÏæ¹Iâ~ë·~‹×~í׿vww9qâô]ßõ]¼÷{¿7/ª·~ë·æg~æg¸ßk½ÖkñÛ¿ýÛüGúìÏþl>çs>‡²Í¿ÕK¿ôKó7ó7<Ѓü`þê¯þŠãÇóüüôOÿ4oó6oÃýÖoý¯ýگͿÆë¼Îëð×ý×¼ôK¿4Çç­Þê­xï÷~oî÷Û¿ýÛüÌÏü ý×Í_ÿõ_óR/õRüöoÿ6/ªïþîïæ}Þç}x Û<Ðk¿ökó;¿ó;Üïµ_ûµù­ßú-þ5~ú§š·y›·á~ë·~‹×~í׿~û·›×y×álóü|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæE±»»Ë‰'x Ïú¬Ïâ³?û³yA$ñ@¿õ[¿Åk¿ökó‚|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæ¹½õ[¿5?ó3?Ãý^ú¥_š¿ú«¿â_ã·û·y×yè§~ê§xë·~kž›$È6ÿ’x ßú­ßâµ_ûµ¹êª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]õÒw÷wó>ïó><Ѓü`þê¯þŠãÇ󯵻»Ëë¼Îëð×ý×<Ð{½×{ñÝßýÝ<·¿þë¿æe^æex ¯úª¯â£?ú£yQ}ög6Ÿó9ŸÃýÕ_ý/ýÒ/Ís“ÄýÖoý¯ýÚ¯Ís{ë·~k~æg~†û½ök¿6¿õ[¿Å‹âÖ[oå!yôZ¯õZüöoÿ6ÿ‘>û³?›ÏùœÏálóoõ×ý×¼Ì˼ Ïí³>ë³øìÏþlžŸÝÝ]üàséÒ%î÷Ú¯ýÚüÖoý/ª¿þë¿æe^æex ú¨â«¿ú«¹ßOÿôOó6oó6<ÐÅ‹9~ü8/ŠŸþéŸæmÞæmx Û<ÐWõWó1ó1<Ð_ýÕ_ñÒ/ýÒ¼¨^çu^‡ßþíßæ~ÇŽcww—çöÛ¿ýÛ¼Îë¼d›çç³?û³ùœÏùîwüøq.^¼È‹â«¿ú«ù˜ùè³>ë³øìÏþl^I<ÐoýÖoñÚ¯ýÚ¼ ŸýÙŸÍç|Îçp¿×z­×â·û·ynßýÝßÍû¼Ïûð@¿õ[¿Åk¿ökó¢z›·y~ú§šºxñ"Çç¹Iâló¯!‰ú­ßú-^ûµ_›«®ºêª«®ºêª«þOA¶ÍUW]uÕUW]uÕUÿg½ök¿6¿ó;¿Ã=øÁæ§~ê§xé—~i^T»»»¼Îë¼ý×Í;vŒ[o½•ãÇóü¼ök¿6¿ó;¿ÃýŽ?ÎoýÖoñÒ/ýÒüKþú¯ÿš—y™—á^ê¥^Š¿þë¿æù‘ÄýÖoý¯ýÚ¯ÍsûéŸþiÞæmÞ†úª¯ú*>ú£?šÉë¼ÎëðÛ¿ýÛ<Ðk½ÖkñÛ¿ýÛüGúìÏþl>çs>‡²Í¿Çgögó9Ÿó9<·¿ú«¿â¥_ú¥y~>û³?›ÏùœÏá¾ë»¾‹÷~ï÷æEñ2/ó2üõ_ÿ5ôô§??øÁÜoww—'Nð@ŸõYŸÅgögó¢øèþh¾æk¾†û=èAâÖ[oåvwwyðƒÌ¥K—¸ßK¿ôKó[¿õ[?~œÉWõWó1ó1<ÐG}ÔGñÕ_ýÕ<·ßþíßæu^çux Ûû³?›ÏùœÏá~¯õZ¯Åoÿöoóü?~œK—.q¿?øÁüÕ_ýÇç_òÝßýݼÏû¼ô^ïõ^|÷w7Ï$È6ÿ’x ßú­ßâµ_ûµ¹êª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]õÖîî.~ðƒ¹téÏí½ßû½ù¬Ïú,üàó‚ìîîò3?ó3|ôG4»»»<·ßú­ßâµ_ûµyA~û·›×y×áŽ?ÎoýÖoñÒ/ýÒ¼ ý×Íë¼Îë°»»ËýÖoý¯ýÚ¯Íó#‰ú­ßú-^ûµ_›ççµ_ûµùßù軾ë»xï÷~o^÷yŸ÷ỿû»yn¯õZ¯Åoÿöoóé³?û³ùœÏùÈ6ÿ»»»¼ôK¿4ÏxÆ3x —~é—æ¯þê¯x~n½õV^ú¥_šK—.ñ@ßõ]ßÅ{¿÷{ó¼Ïû¼ßýÝßͽ×{½ßýÝßÍs{ï÷~o¾ç{¾‡ú®ïú.Þû½ß›æ§ú§y›·yè³>ë³øìÏþlžÛgögó9Ÿó9<ÐK¿ôKó[¿õ[?~œ仿û»yŸ÷yèØ±cÜzë­?~œçöÛ¿ýÛ¼Îë¼d›çgww—'Nð@Çç·~ë·xé—~ižŸ¿þë¿æu^çuØÝÝå¹}Ög}ŸýÙŸÍ "‰ú¬Ïú,>û³?›ä³?û³ùœÏùî÷Z¯õZüöoÿ6ÏÏWõWó1ó1<ÐK¿ôKó[¿õ[?~œ仿û»yŸ÷yèØ±cüõ_ÿ5~ðƒy~$ñ@¶ù×ÄýÖoý¯ýÚ¯ÍUW]uÕUW]uÕUÿ§ Ûæª«®ºêª«®ºêªÿÓþú¯ÿš×~í×æÒ¥Kê£xé—~iî÷×ý×|Í×| ßýÝßÍsû¨ú(¾ú«¿šDô[¿õ[¼ök¿6ÏÏ­·ÞÊK¿ôKséÒ%è­ßú­ù¨ú(^ûµ_›û}Ï÷|_ýÕ_Í_ÿõ_óü¼Ök½¿ýۿͤÏþìÏæs>çsx Ûü{ýöoÿ6¯ó:¯Ãsûª¯ú*>ú£?šçç§ú§y›·yžÛk¿ökóÑýѼÖk½ÇàÖ[oåw~çwøìÏþln½õVèAzý×ÍñãÇyn·Þz+/ýÒ/Í¥K—x ·~ë·æ½ßû½y«·z+è¯ÿú¯ùžïù¾ú«¿š:vì·Þz+Ççùyí×~m~çw~‡:~ü8ýÑÍ{½×{ñà?˜ûýÌÏü _ýÕ_ÍoÿöoóÜ~ê§~Š·~ë·æùùíßþm^çu^‡²Í òÞïýÞ|Ï÷|tüøqÞú­ßš÷~ï÷æ¥^ꥸõÖ[yÆ3žÁw÷wóÓ?ýÓÜï¥^ê¥ø›¿ùî÷YŸõY|ög6/Èk¿ökó;¿ó;<ÐK¿ôKóà?˜ÝÝ]Þë½Þ‹÷~ï÷æ~ŸýÙŸÍç|Îçp¿×z­×â·û·yAÞú­ßšŸù™ŸáŽ?ÎGôGó^ïõ^<øÁæ~?ó3?Ãw÷wóÓ?ýÓ<·ïú®ïâ½ßû½yA$ñ@¶ù×ÄýÖoý¯ýÚ¯ÍUW]uÕUW]uÕUÿ§ Ûæª«®ºêª«®ºêªÿóþú¯ÿš÷~ï÷æoþæoø÷:vì_ýÕ_Í{¿÷{ó¢zï÷~o¾ç{¾‡«÷z¯÷⻿û»ya$ñ@¿õ[¿Åk¿ökó‚üõ_ÿ5¯ýگͥK—ø×ø®ïú.Þç}Þ‡û½Ök½¿ýۿͤÏþìÏæs>çsx ÛüGxï÷~o¾ç{¾‡:~ü8õWŃü`žŸïþîïæ}Þç}ø·:vì¿ýÛ¿ÍK¿ôKó‚|÷w7ïó>ïÿձcÇøíßþm^ú¥_šdww—×~í׿oþæoø·ú®ïú.Þû½ß›ä·û·y×yÈ6/È­·ÞÊK¿ôKséÒ%þ5^ê¥^ŠÏþìÏæmÞæm¸ßg}ÖgñÙŸýÙ¼ ýÑÍ×|Í×ð‚¼×{½ßýÝßÍý>û³?›ÏùœÏá~¯õZ¯Åoÿöoó‚ìîîòÖoýÖüÎïüÿVßõ]ßÅ{¿÷{óÂHâló¯!‰ú­ßú-^ûµ_›«®ºêª«®ºêª«þOA¶ÍUW]uÕUW]uÕUÿ/ìîîòÕ_ýÕ|Îç|ÿV¯õZ¯Åw÷wóà?˜­ïþîïæ£?ú£¹té/ªcÇŽñÙŸýÙ|ôG4ÿI<ÐoýÖoñÚ¯ýÚ¼0ý×Í[¿õ[óŒg<ƒÅg}ÖgñÙŸýÙHâ~¯õZ¯Åoÿöoóé³?û³ùœÏùÈ6ÿvwwyðƒÌ¥K—x ×~í׿·~ë·xA~û·›÷~ï÷æÏxÿ¯õZ¯Åw÷wóà?˜Éw÷wóÑýÑ\ºt‰=èAüôOÿ4/ýÒ/Í‹â£?ú£ùš¯ùþ5ô ñÝßýݼök¿6/Ìoÿöoó:¯ó:çs>‡û½Ök½¿ýÛ¿Í¿ä£?ú£ùš¯ùþ5ô ñÝßýݼök¿6ÿIû³?›fww—÷~ï÷æg~ægx~ls¿ÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛ¼(n½õV>û³?›ïùžïá…yЃÄgögóÖoýÖ?~œ…$È6ÿ’x ßú­ßâµ_ûµ¹êª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]õÿÖîî.ý×ÍoÿöoóÜŽ?Îk¿ökóÒ/ýÒügøë¿þk~û·›ÝÝ]î÷à?˜—~é—æ¥_ú¥ùïðÛ¿ýÛüöoÿ6÷;~ü8¯ýÚ¯ÍK¿ôKsÕsºõÖ[ùíßþmn½õVè¥_ú¥yé—~iüàóïõ×ý×üõ_ÿ5·Þz+ôÚ¯ýÚ<øÁæÁ~0ÿ~û·›ßþíßæ^ú¥_š—~é—æÁ~0ÿÕ~û·›ßþíßæ~~ðƒyé—~i^ú¥_šÿ(»»»üõ_ÿ5ôÚ¯ýÚügøíßþm~û·›zðƒÌk¿ökóà?˜«®ºêª«®ºêª«®úO€l›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]õ?Ôîî.Ÿó9ŸÃw÷wðÞïýÞ|ÕW}W]uÕUW]uÕUW]uÕUW]uÕUW]uÕU/²m®ºêª«þ‡úèþh¾æk¾†ú¨ú(¾ú«¿š«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÙ6W]uÕUÿC8q‚ÝÝ]èøñã\¼x‘«®z~þê¯þŠ—z©—""¸êªäìÙ³ñ =ˆ«®zaþþïÿž‡?üáÌçs®ºêÙÛÛãÞ{ïåxW]õÂ<éIOâºë®cgg‡«®zAŽŽŽ¸õÖ[yìcËUW½0·Þz+[[[œ>}š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!Ÿð Ÿ@×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áò Ÿð t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!Ÿð Ÿ@×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~Þﵜ«à¾ê5yÔKŸáªgûÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâùy¿×þq®‚Oøª×äQ/}†«ží ¿ð ù„Oøº®ãª«^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰ççý^ûǹ >á«^“G½ô®z¶/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žŸ÷{íç*ø„¯zMõÒg¸êÙ¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~Þﵜ«à¾ê5yÔKŸáªgûÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâùy¿×þq®‚Oøª×äQ/}†«ží ¿ð ù„Oøº®ãª«^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰ççý^ûǹ >á«^“G½ô®z¶/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žŸ÷{íç*ø„¯zMõÒg¸êÙ¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~Þﵜ«à¾ê5yÔKŸáªgûÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâùy¿×þq®‚Oøª×äQ/}†«ží ¿ð ù„Oøº®ãª«^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰ççý^ûǹ >á«^“G½ô®z¶/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUWý¯ò×ý×\ºt €×z­×â…ùßùŽ;ÆK¿ôKpë­·òŒg<€=èA<øÁæ…ÙÝÝåoþæoxЃăü`þ«Hâùy¿×þq®‚Oøª×äQ/}†«ží ¿ð ù„Oøº®ãª«^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUÿ«¼ök¿6¿ó;¿€m^I¼Ök½¿ýÛ¿ ÀOÿôOó6oó6¼×{½ßýÝßÍ óÑýÑ|Í×| ?õS?Å[¿õ[ó_EÏÏû½ös|ÂW½&zé3\õl_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ú_åµ_ûµùßùlóÂHàµ^ëµøíßþmî÷à?˜g<ã\¼x‘ãÇó‚œ8q‚ÝÝ]Ž;Æîî.ÿ•$ñü¼ßkÿ8WÁ'|Õkò¨—>ÃUÏö…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêªÿU^ûµ_›ßùßÀ6/Œ$^ëµ^‹ßþíßæ~ýÑÍ×|Í×ð]ßõ]¼÷{¿7ÏÏOÿôOó6oó6|ÔG}_ýÕ_Í%Iá«^“G½ô®z¶/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUWý¯òÚ¯ýÚüÎïü¶ya$ðZ¯õZüöoÿ6ôÒ/ýÒüÍßü OúÓyðƒÌíîîrâÄ ^ê¥^Š¿þë¿æ¿š$žŸ÷{íç*ø„¯zMõÒg¸êÙ¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]õ¿Êk¿ökó;¿ó;Øæ…‘Àk½ÖkñÛ¿ýÛ<Ðw÷wó>ïó>|ÕW}ýÑÍ}÷w7ïó>ïÀW}ÕWñÑýÑü{íîîò9Ÿó9|÷w7»»»ü[½ßkÿ8WÁ'|Õkò¨—>ÃUÏö…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêªÿU^ûµ_›ßùßÀ6/Œ$^ëµ^‹ßþíßævwwyðƒÌ¥K—xé—~iþê¯þŠz×y~û·€‹/rüøqþ½>ú£?š¯ùš¯áßëý^ûǹ >á«^“G½ô®z¶/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUWý¯òÚ¯ýÚüÎïü¶ya$ðZ¯õZüöoÿ6Ïí½ßû½ùžïùžþô§óà?€[o½•‡<ä!¼Õ[½?ýÓ?Í„'N°»»Ë¿×û½ös|ÂW½&zé3\õl_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ú_åµ_ûµùßùló‚ìîîrâÄ ^ëµ^‹ßþíßæ¹ýöoÿ6¯ó:¯ÀG}ÔGñÕ_ýÕ|õW5ó1ÀOýÔOñÖoýÖüG8~ü8—.]âßëý^ûǹ >á«^“G½ô®z¶/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUWý¯òÚ¯ýÚüÎïü¶yA~û·›×y×àµ^ëµøíßþmžŸ?øÁ<ãÏàÁ~0OúÓxÈC­·Þʃô n½õVþ£|ôG4_ó5_ÿ×û½ös|ÂW½&zé3\õl_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ú_åµ_ûµùßùló‚üöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛçs>€×z­×â·û·y~n½õVò‡ðQõQ|Í×| OúÓyðƒÌIû³?›ç¶»»ËCòvwwx­×z-~û·›ä­ßú­ù™Ÿùüàpë­·òR/õRüõ_ÿ5ÿ$ñü¼ßkÿ8WÁ'|Õkò¨—>ÃUÏö…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêªÿU~ú§š·y›·á~_ýÕ_ÍG}ÔGq¿ßþíßæc>æcøë¿þkî÷Z¯õZüöoÿ6/Èw÷wó>ïó><Ðw}×wñÞïýÞüw’Äóó~¯ýã\ŸðU¯É£^ú W=Û~áò Ÿð t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«þ×yé—~iþæoþ†û=øÁæÁ~0·Þz+·Þz+ïõ^ïÅ­·ÞÊïüÎïðZ¯õZüöoÿ6/Ìñãǹté÷»xñ"Çç¿“$žŸ÷{íç*ø„¯zMõÒg¸êÙ¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]õ¿Îîî.oýÖoÍïüÎïðü|ÔG}_ýÕ_Ík¿ökó;¿ó;¼Ök½¿ýÛ¿Í óÞïýÞ|Ï÷|ïõ^ïÅw÷wóßMÏÏû½ös|ÂW½&zé3\õl_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ú_ë¯ÿú¯ùéŸþin½õVŽ?΃ü`Þú­ßš?øÁüõ_ÿ5»»»?~œ—~é—æ…ÙÝÝå¯ÿú¯xðƒ̃ü`þ»Iâùy¿×þq®‚Oøª×äQ/}†«ží ¿ð ù„Oøº®ãª«^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰ççý^ûǹ >á«^“G½ô®z¶/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žŸ÷{íç*ø„¯zMõÒg¸êÙ¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~Þﵜ«à¾ê5yÔKŸáªgûÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâùy¿×þq®‚Oøª×äQ/}†«ží ¿ð ù„Oøº®ãª«^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰ççý^ûǹ >á«^“G½ô®z¶/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žŸ÷{íç*ø„¯zMõÒg¸êÙ¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~Þﵜ«à¾ê5yÔKŸáªgûÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâùy¿×þq®‚Oøª×äQ/}†«ží ¿ð ù„Oøº®ãª«^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰ççý^ûǹ >á«^“G½ô®z¶/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žŸ÷{íç*ø„¯zMõÒg¸êÙ¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~Þﵜ«à¾ê5yÔKŸáªgûÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!Ÿð Ÿ@×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áò Ÿð t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!Ÿð Ÿ@×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áò Ÿð t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!Ÿð Ÿ@×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áò Ÿð t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!Ÿð Ÿ@×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâùy¿×þq®ºêù¹é5Ç¿ÿhœÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛsÕU/Ì5/ýt.Ýz ëÝM®ºêé¶VœzôÜóç窫^˜“¾ƒõî&‡÷œà¿Ú·ÿÖÛqÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\uÕósÓk>Ž;ÿÑ8ƒ«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·檫^˜k^úé\ºõÖ»›\uÕ Òm­8õè;¸çÏÎUW½0'}ëÝMï9Áµoÿ­·ãª«®ºêdÛ\uÕUWý%‰ççý^ûǹêªçç¦×|wþþ£qW]õ‚lÝpnkÅÅ'ÝÀUW½0×½â“9ÿ÷·0͸êªd~ò€í›ÎqöoÌUW½0×¼ôÓ¹të5¬w7¹êª¤ÛZqêÑwpÏŸ?œ«®zaN>úÖ»›Þs‚ÿjßþ[oÇUW]uÕÿȶ¹êª«®úJÏÏû½ösÕUÏÏM¯ù8îüýGã ®ºêÙºáÝÖŠ‹Oº«®za®{Å'sþïoa<šqÕU/ÈüäÛ7ãìß>˜«®za®yé§séÖkXïnrÕU/H·µâÔ£ïàž?8W]õœ|ô¬w79¼çÿÕ¾ý·ÞŽ«®ºêªÿmsÕUW]õ?”$žŸ÷{í窫žŸ›^óqÜùûÆ\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0W]õÂ\óÒOçÒ­×°ÞÝ䪫^nkÅ©GßÁ=þp®ºê…9ùè;XïnrxÏ þ«}ûo½W]uÕUÿ Ûæª«þŸúíßþm~çw~‡ÕñãÇy«·z+üàó‚Üzë­|Ï÷|/ª—~é—æµ^ëµ8~ü8ÿ’[o½•ïùžïá^ëµ^‹×~í׿_ã·û·ùßùî÷ =ˆ÷~ï÷æ"I鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®ºê…¹æ¥ŸÎ¥[¯a½»ÉUW½ ÝÖŠS¾ƒ{þüá\uÕ sòÑw°ÞÝäðžüWûößz;®ºêª«þ@¶ÍUWý?õÙŸýÙ|Îç|ÿZ¯ýÚ¯Íg}ÖgñÚ¯ýÚ<·ßþíßæu^çuø×zë·~k¾ê«¾Š?øÁ¼ ¿ýÛ¿Íë¼Îëð@/ýÒ/Í_ýÕ_ñ¯ñ2/ó2üõ_ÿ5÷{­×z-~û·›ÿ‰$ñü¼ßkÿ8W]õüÜôšãÎß4Îફ^­.Ðm­¸ø¤¸êªæºW|2çÿþÆ£W]õ‚ÌO°}Ó9Îþ탹êªæš—~:—n½†õî&W]õ‚t[+N=úîùó‡sÕU/ÌÉGßÁzw“Ã{Nð_íÛëí¸êª«®ú_Ù6W]õÿÔgögó9Ÿó9ü[ýÔOýoýÖoÍýöoÿ6¯ó:¯Ã¿ÅñãÇù®ïú.Þú­ßšçç·û·y×yžÛÓŸþtüàó¢¸õÖ[yÈC½Ök½¿ýÛ¿ÍÿD’x~Þﵜ«®z~nzÍÇqçï?gpÕU/ÈÖ è¶V\|Ò \uÕ sÝ+>™ó ãÑŒ«®zAæ'ؾégÿöÁ\uÕ sÍK?K·^Ãzw“«®zAº­§}÷üùùêªæä£ï`½»Éá='ø¯öí¿õv\uÕUWý/€l›«®úê³?û³ùœÏù>ë³>‹ÏþìÏæù¹õÖ[¹õÖ[ùë¿þk>û³?›K—.püøqžþô§süøqî÷Û¿ýÛ¼Î뼯õZ¯Åoÿöoóüüöoÿ6?ýÓ?Í×|Í×p¿ãÇó[¿õ[¼ôK¿4Ïí·û·y×yžÛW}ÕWñÑýѼ(¾ú«¿šù˜á^ëµ^‹ßþíßæ"I鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®ºê…¹æ¥ŸÎ¥[¯a½»ÉUW½ ÝÖŠS¾ƒ{þüá\uÕ sòÑw°ÞÝäðžüWûößz;®ºêª«þ@¶ÍUWý?õÙŸýÙ|Îç|ŸõYŸÅgögó/ùë¿þk^ûµ_›K—.ðU_õU|ôG4÷ûíßþm^çu^€×z­×â·û·ù—Üzë­¼õ[¿5ó7Àƒü`žþô§óÜ~û·›×y×à¥^ê¥ø›¿ù^ú¥_š¿ú«¿âEñ2/ó2üõ_ÿ5zЃxÆ3žÀk½ÖkñÛ¿ýÛüO$‰ççý^ûǹêªçç¦×|wþþ£qW]õ‚lÝpnkÅÅ'ÝÀUW½0×½â“9ÿ÷·0͸êªd~ò€í›ÎqöoÌUW½0×¼ôÓ¹të5¬w7¹êª¤ÛZqêÑwpÏŸ?œ«®zaN>úÖ»›Þs‚ÿjßþ[oÇUW]uÕÿȶ¹êªÿ§>û³?›ÏùœÏà³>ë³øìÏþl^ŸýÙŸÍç|ÎçðZ¯õZüöoÿ6÷ûíßþm^çu^€×z­×â·û·yQüõ_ÿ5¯ýگͥK—ø¬Ïú,>û³?›úíßþm^çu^€×z­×âøñãüÌÏü OúÓyðƒÌ së­·ò‡<€ú¨âk¾ækx­×z-~û·›ÿ‰$ñü¼ßkÿ8W]õüÜôšãÎß4Îફ^­.Ðm­¸ø¤¸êªæºW|2çÿþÆ£W]õ‚ÌO°}Ó9Îþ탹êªæš—~:—n½†õî&W]õ‚t[+N=úîùó‡sÕU/ÌÉGßÁzw“Ã{Nð_íÛëí¸êª«®ú_Ù6W]õÿÔgögó9Ÿó9|Ög}ŸýÙŸÍ‹â·û·y×yŽ?ÎÅ‹¹ßoÿöoó:¯ó:¼Ök½¿ýۿ͋껿û»yŸ÷yŽ?ÎÅ‹y ßþíßæu^çux­×z-Þû½ß›÷yŸ÷à«¾ê«øèþh^˜¯þê¯æc>æc8vì?ýÓ?Íë¼ÎëðZ¯õZüöoÿ6ÿIâùy¿×þq®ºêù¹é5Ç¿ÿhœÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛsÕU/Ì5/ýt.Ýz ëÝM®ºêé¶VœzôÜóç窫^˜“¾ƒõî&‡÷œà¿Ú·ÿÖÛqÕUW]õ¿²m®ºêÿ©ÏþìÏæs>çsø¬Ïú,>û³?›Åoÿöoó:¯ó:ÜÏ6÷ûíßþm^çu^€×z­×â·û·ù×8~ü8—.]à¯þê¯xé—~iî÷Û¿ýÛ¼Î뼯õZ¯Åoÿöosüøq.]ºÄK¿ôKóWõW¼0yÈC¸õÖ[y¯÷z/Þû½ß›×y×àµ^ëµøíßþmþ'’Äóó~¯ýã\uÕósÓk>Ž;ÿÑ8ƒ«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·檫^˜k^úé\ºõÖ»›\uÕ Òm­8õè;¸çÏÎUW½0'}ëÝMï9Áµoÿ­·ãª«®ºêdÛ\uÕÿSŸýÙŸÍç|ÎçðYŸõY|ög6/Šïþîïæ}Þç}x­×z-~û·›ûýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛük¼õ[¿5?ó3?ÀW}ÕWñÑýÑÜï·û·y×y^ëµ^‹ßþíßæ½ßû½ùžïùžþô§óà?˜çç¯ÿú¯y™—yþê¯þŠÝÝ]^çu^€×z­×â·û·ùŸHÏÏû½ösÕUÏÏM¯ù8îüýGã ®ºêÙºáÝÖŠ‹Oº«®za®{Å'sþïoa<šqÕU/ÈüäÛ7ãìß>˜«®za®yé§séÖkXïnrÕU/H·µâÔ£ïàž?8W]õœ|ô¬w79¼çÿÕ¾ý·ÞŽ«®ºêªÿmsÕUÿO}ög6Ÿó9ŸÀg}ÖgñÙŸýÙüKvwwy™—yn½õV>ê£>Нþê¯æ~¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿŸýÙŸÍç|ÎçðYŸõY|ög6÷ûíßþm^çu^€×z­×â·û·ùéŸþiÞæmÞ€¯úª¯â£?ú£y~>ú£?š¯ùš¯áAz·Þz+¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿIâùy¿×þq®ºêù¹é5Ç¿ÿhœÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛsÕU/Ì5/ýt.Ýz ëÝM®ºêé¶VœzôÜóç窫^˜“¾ƒõî&‡÷œà¿Ú·ÿÖÛqÕUW]õ¿²m®ºêÿ©ÏþìÏæs>çsø¬Ïú,>û³?›æ¯ÿú¯ù˜ù~û·›û=ýéOçÁ~0÷ûíßþm^çu^€×z­×â·û·ù×øìÏþl>çs>€×z­×â·û·¹ßoÿöoó:¯ó:¼Ök½¿ýÛ¿ Àñãǹté/ýÒ/Í_ýÕ_ñü<ä!áÖ[oå£>ê£øê¯þj~û·›×y×àµ^ëµøíßþmþ'’Äóó~¯ýã\uÕósÓk>Ž;ÿÑ8ƒ«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·檫^˜k^úé\ºõÖ»›\uÕ Òm­8õè;¸çÏÎUW½0'}ëÝMï9Áµoÿ­·ãª«®ºêdÛ\uÕÿSŸýÙŸÍç|Îçðà?˜?øÁ¼ ý×Íîî.ôU_õU|ôG4ôÛ¿ýÛ¼Î뼯õZ¯Åoÿöoó¯ñÙŸýÙ|Îç|¯õZ¯Åoÿöos¿ßþíßæu^çux­×z-~û·€÷~ï÷æ{¾ç{xúӟ΃ü`è¯ÿú¯y™—yþê¯þŠ—~é—æ·û·y×y^ëµ^‹ßþíßæ"I鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®ºê…¹æ¥ŸÎ¥[¯a½»ÉUW½ ÝÖŠS¾ƒ{þüá\uÕ sòÑw°ÞÝäðžüWûößz;®ºêª«þ@¶ÍUWý?õÙŸýÙ|Îç|ÿ_õU_ÅGôGóÜ~û·›×y×àµ^ëµøíßþmþ5>û³?›ÏùœÏàµ^ëµøíßþmî÷Û¿ýÛ¼Î뼯õZ¯ÅoÿöoðÓ?ýÓ¼ÍÛ¼ _õU_ÅGôGó@ýÑÍ×|Í×ð =ˆ[o½€ßþíßæu^çux­×z-~û·›ÿ »»»|Îç|ßýÝßÍîî.ÿVï÷Ú?ÎUW=?7½æã¸ó÷3¸êªdë† t[+.>鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®ºê…¹æ¥ŸÎ¥[¯a½»ÉUW½ ÝÖŠS¾ƒ{þüá\uÕ sòÑw°ÞÝäðžüWûößz;®ºêª«þ@¶ÍUWý?õÙŸýÙ|Îç|/Š—z©—âÁ~0¯ýÚ¯Í{¿÷{süøqžŸßþíßæu^çux­×z-~û·›ÏþìÏæs>çsx«·z+~ú§šûýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛÜïøñã\ºt‰—~é—æ¯þê¯x ‡<ä!Üzë­|ÔG}_ýÕ_ Àoÿöoó:¯ó:¼Ök½¿ýÛ¿Í…þèæk¾ækø÷z¿×þq®ºêù¹é5Ç¿ÿhœÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛsÕU/Ì5/ýt.Ýz ëÝM®ºêé¶VœzôÜóç窫^˜“¾ƒõî&‡÷œà¿Ú·ÿÖÛqÕUW]õ¿²m®ºêÿ©ÏþìÏæs>çsø¬Ïú,>û³?›¯ßþíßæu^çux­×z-~û·›÷~ï÷æ{¾ç{ø¬Ïú,>û³?›ûýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛÜï½ßû½ùžïùžþô§óà?€ßþíßæu^çuxúӟ΃ü`~û·›×y×àµ^ëµøíßþmþ+œ8q‚ÝÝ]þ½Þﵜ«®z~nzÍÇqçï?gpÕU/ÈÖ è¶V\|Ò \uÕ sÝ+>™ó ãÑŒ«®zAæ'ؾégÿöÁ\uÕ sÍK?K·^Ãzw“«®zAº­§}÷üùùêªæä£ï`½»Éá='ø¯öí¿õv\uÕUWý/€l›«®úê³?û³ùœÏù>ë³>‹ÏþìÏæßë·û·y×y^ëµ^‹ßþíßæ_ã!y·Þz+?õS?Å[¿õ[s¿ßþíßæu^çux­×z-~û·›ûýôOÿ4oó6oÀW}ÕWñÑýѼ÷{¿7ßó=ßÃK½ÔKñ×ý×Üï·û·y×y^ëµ^‹ßþíßæ¿Âñãǹtéÿ^ï÷Ú?ÎUW=?7½æã¸ó÷3¸êªdë† t[+.>鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®ºê…¹æ¥ŸÎ¥[¯a½»ÉUW½ ÝÖŠS¾ƒ{þüá\uÕ sòÑw°ÞÝäðžüWûößz;®ºêª«þ@¶ÍUWý?õÙŸýÙ|Îç|ŸõYŸÅgögóïõÛ¿ýÛ¼Î뼯õZ¯Åoÿöoó¢ºõÖ[yÈCÂýžþô§óà?˜ûýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛ<Ðñãǹté/ýÒ/Í_ýÕ_pâÄ vwwùª¯ú*>ú£?šûýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛüWøèþh¾æk¾†¯÷{í窫žŸ›^óqÜùûÆ\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0W]õÂ\óÒOçÒ­×°ÞÝ䪫^nkÅ©GßÁ=þp®ºê…9ùè;XïnrxÏ þ«}ûo½W]uÕUÿ Ûæª«þŸúìÏþl>çs>€Ïú¬Ïâ³?û³ù÷úíßþm^çu^€×z­×â·û·yQ½÷{¿7ßó=ßÀ[½Õ[ñÓ?ýÓ<Ðoÿöoó:¯ó:¼Ök½¿ýۿͽ÷{¿7ßó=ßÀÓŸþtþú¯ÿš·y›·àéO:~ðƒ¹ßoÿöoó:¯ó:¼Ök½¿ýÛ¿Í•þèæ»¿û»¹téÿVï÷Ú?ÎUW=?7½æã¸ó÷3¸êªdë† t[+.>鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®ºê…¹æ¥ŸÎ¥[¯a½»ÉUW½ ÝÖŠS¾ƒ{þüá\uÕ sòÑw°ÞÝäðžüWûößz;®ºêª«þ@¶ÍUWý?õÙŸýÙ|Îç|ŸõYŸÅgögóïõÛ¿ýÛ¼Î뼯õZ¯Åoÿöoó¢øéŸþiÞæmÞ†ûýÖoý¯ýÚ¯Íýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛ<ÐOÿôOó6oó6|ÕW}ý×Í÷|Ï÷ðR/õRüõ_ÿ5ôÛ¿ýÛ¼Î뼯õZ¯Åoÿöoó?‘$žŸ÷{í窫žŸ›^óqÜùûÆ\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0W]õÂ\óÒOçÒ­×°ÞÝ䪫^nkÅ©GßÁ=þp®ºê…9ùè;XïnrxÏ þ«}ûo½W]uÕUÿ Ûæª«þŸúìÏþl>çs>€Ïú¬Ïâ³?û³ù÷úíßþm^çu^€×z­×â·û·ù—|Í×| ŸýÙŸÍîî.oõVoÅOÿôOóÜ~û·›×y×àµ^ëµøíßþmžÛñãǹté¯õZ¯ÅßüÍß°»»ËW}ÕWñÑýÑ<Ðoÿöoó:¯ó:¼Ök½¿ýÛ¿ÍÿD’x~Þﵜ«®z~nzÍÇqçï?gpÕU/ÈÖ è¶V\|Ò \uÕ sÝ+>™ó ãÑŒ«®zAæ'ؾégÿöÁ\uÕ sÍK?K·^Ãzw“«®zAº­§}÷üùùêªæä£ï`½»Éá='ø¯öí¿õv\uÕUWý/€l›«®úê³?û³ùœÏù>ë³>‹ÏþìÏæßë·û·y×yüàóÞïýÞ¼ ý×Í_ÿõ_së­·r¿—z©—â·û·9~ü8Ïí·û·y×y^ëµ^‹ßþíßæ¹}ôG4_ó5_Ã]¼x‘ãÇó@¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿIâùy¿×þq®ºêù¹é5Ç¿ÿhœÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛsÕU/Ì5/ýt.Ýz ëÝM®ºêé¶VœzôÜóç窫^˜“¾ƒõî&‡÷œà¿Ú·ÿÖÛqÕUW]õ¿²m®ºêÿ©ÏþìÏæs>çsø¬Ïú,>û³?›¯ßþíßæu^çuø·x­×z-~ú§šãÇóüüöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛ<·¿þë¿æe^æe¸ß[½Õ[ñÓ?ýÓ<·ßþíßæu^çux­×z-~û·›ÿ‰$ñü¼ßkÿ8W]õüÜôšãÎß4Îફ^­.Ðm­¸ø¤¸êªæºW|2çÿþÆ£W]õ‚ÌO°}Ó9Îþ탹êªæš—~:—n½†õî&W]õ‚t[+N=úîùó‡sÕU/ÌÉGßÁzw“Ã{Nð_íÛëí¸êª«®ú_Ù6W]õÿÔgögó9Ÿó9|Ög}ŸýÙŸÍ¿×oÿöoó:¯ó:¼¨Ž;Æ[¿õ[óÞïýÞ¼ök¿6/Ìoÿöoó:¯ó:¼Ök½¿ýÛ¿Íóóà?˜g<ã|×w}ïýÞïÍsûíßþm^çu^€×z­×â·û·ùŸHÏÏû½ösÕUÏÏM¯ù8îüýGã ®ºêÙºáÝÖŠ‹Oº«®za®{Å'sþïoa<šqÕU/ÈüäÛ7ãìß>˜«®za®yé§séÖkXïnrÕU/H·µâÔ£ïàž?8W]õœ|ô¬w79¼çÿÕ¾ý·ÞŽ«®ºêªÿmsÕUÿOÝzë­Üzë­<øÁæÁ~0ÿ^»»»üõ_ÿ5/Š?øÁ<øÁæEµ»»Ë_ÿõ_püøq^ú¥_šçç¯ÿú¯ÙÝÝà¥_ú¥9~ü8Ïmww—¿þë¿àøñã¼ôK¿4ÿIâùy¿×þq®ºêù¹é5Ç¿ÿhœÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛsÕU/Ì5/ýt.Ýz ëÝM®ºêé¶VœzôÜóç窫^˜“¾ƒõî&‡÷œà¿Ú·ÿÖÛqÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\uÕósÓk>Ž;ÿÑ8ƒ«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·檫^˜k^úé\ºõÖ»›\uÕ Òm­8õè;¸çÏÎUW½0'}ëÝMï9Áµoÿ­·ãª«®ºêdÛ\uÕUWý%‰ççý^ûǹêªçç¦×|wþþ£qW]õ‚lÝpnkÅÅ'ÝÀUW½0×½â“9ÿ÷·0͸êªd~ò€í›ÎqöoÌUW½0×¼ôÓ¹të5¬w7¹êª¤ÛZqêÑwpÏŸ?œ«®zaN>úÖ»›Þs‚ÿjßþ[oÇUW]uÕÿȶ¹êª«®úJÏÏû½ösÕUÏÏM¯ù8îüýGã ®ºêÙºáÝÖŠ‹Oº«®za®{Å'sþïoa<šqÕU/ÈüäÛ7ãìß>˜«®za®yé§séÖkXïnrÕU/H·µâÔ£ïàž?8W]õœ|ô¬w79¼çÿÕ¾ý·ÞŽ«®ºêªÿmsÕUW]õ?”$žŸ÷{í窫žŸ›^óqÜùûÆ\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0W]õÂ\óÒOçÒ­×°ÞÝ䪫^nkÅ©GßÁ=þp®ºê…9ùè;XïnrxÏ þ«}ûo½W]uÕUÿ Ûæª«®ºê(I鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®ºê…¹æ¥ŸÎ¥[¯a½»ÉUW½ ÝÖŠS¾ƒ{þüá\uÕ sòÑw°ÞÝäðžüWûößz;®ºêª«þ@¶ÍUW]uÕÿP’x~Þﵜ«®z~nzÍÇqçï?gpÕU/ÈÖ è¶V\|Ò \uÕ sÝ+>™ó ãÑŒ«®zAæ'ؾégÿöÁ\uÕ sÍK?K·^Ãzw“«®zAº­§}÷üùùêªæä£ï`½»Éá='ø¯öí¿õv\uÕUWý/€l›«®ºêªÿ¡$ñü¼ßkÿ8W]õüÜôšãÎß4Îફ^­.Ðm­¸ø¤¸êªæºW|2çÿþÆ£W]õ‚ÌO°}Ó9Îþ탹êªæš—~:—n½†õî&W]õ‚t[+N=úîùó‡sÕU/ÌÉGßÁzw“Ã{Nð_íÛëí¸êª«®ú_Ù6W]uÕUÿCIâùy¿×þq®ºêù¹é5ÿ;ÿ18ƒ«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·檫^˜k^úé\ºõÖ»›\uÕ Òm­8õè;¸çÏÎUW½0'}ëÝMï9Áµoÿ­·ãª«®ºêdÛ\uÕUWý%‰ççý^ûǹêªçç¦×üîüýÇà ®ºêÙºáÝÖŠ‹Oº«®za®{Å'sþïoa<šqÕU/ÈüäÛ7ãìß>˜«®za®yé§séÖkXïnrÕU/H·µâÔ£ïàž?8W]õœ|ô¬w79¼çÿÕ¾ý·ÞŽ«®ºêªÿmsÕUW]õ?”$žŸ÷{í窫žŸ›^ó¸ó÷ƒ3¸êªdë† t[+.>鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®ºê…¹æ¥ŸÎ¥[¯a½»ÉUW½ ÝÖŠS¾ƒ{þüá\uÕ sòÑw°ÞÝäðžüWûößz;®ºêª«þ@¶ÍUW]uÕÿP’x~Þﵜ«®z~nzÍàÎß Îફ^­.Ðm­¸ø¤¸êªæºW|2çÿþÆ£W]õ‚ÌO°}Ó9Îþ탹êªæš—~:—n½†õî&W]õ‚t[+N=úîùó‡sÕU/ÌÉGßÁzw“Ã{Nð_íÛëí¸êª«®ú_Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'}Ò'Qk媫^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|ŸôIŸD­•«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|Ò'}µV®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|ÁðIŸôIÔZ¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïáµ^ëµxðƒÌUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'}Ò'Qk媫^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|ŸôIŸD­•«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|Ò'}µV®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|ÁðIŸôIÔZ¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïáµ^ëµxðƒÌUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'}Ò'Qk媫^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|ŸôIŸD­•«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|Ò'}µV®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|ÁðIŸôIÔZ¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïáµ^ëµxðƒÌUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'}Ò'Qk媫^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|ŸôIŸD­•«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|Ò'}µV®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|ÁðIŸôIÔZ¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïáµ^ëµxðƒÌUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'}Ò'Qk媫^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|ŸôIŸD­•«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|Ò'}µV®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|ÁðIŸôIÔZ¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïáµ^ëµxðƒÌUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'}Ò'Qk媫^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|ŸôIŸD­•«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|Ò'}µV®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|ÁðIŸôIÔZ¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïáµ^ëµxðƒÌUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'}Ò'Qk媫^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|ŸôIŸD­•«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|Ò'}µV®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|ÁðIŸôIÔZ¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïáµ^ëµxðƒÌUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'}Ò'Qk媫^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|ŸôIŸD­•«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|Ò'}µV®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|ÁðIŸôIÔZ¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïáµ^ëµxðƒÌUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'}Ò'Qk媫^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|ŸôIŸD­•«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|Ò'}µV®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|ÁðIŸôIÔZ¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïáµ^ëµxðƒÌUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'}Ò'Qk媫^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|ŸôIŸD­•«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|Ò'}µV®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ}Ññ Ÿð ÔZ¹êªä/þâ/¸çž{x³7{3®ºê…ùÆoüFÞñߑӧOsÕU/ÈSŸúTþøÿ˜w{·w㪫^˜ïýÞïå5_ó5yðƒÌUW½ ÷Þ{/?ó3?Ã~àrÕU/ÌÏþìÏrË-·ðÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I铨µrÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€Oú¤O¢ÖÊUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>é“>‰Z+W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I铨µrÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€Oú¤O¢ÖÊUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>é“>‰Z+W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I铨µrÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€Oú¤O¢ÖÊUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>é“>‰Z+W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I铨µrÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€Oú¤O¢ÖÊUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>é“>‰Z+W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I铨µrÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€Oú¤O¢ÖÊUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>é“>‰Z+W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I铨µrÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€Oú¤O¢ÖÊUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>é“>‰Z+W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I铨µrÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€Oú¤O¢ÖÊUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>é“>‰Z+W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I铨µrÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€Oú¤O¢ÖÊUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>é“>‰Z+W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I铨µrÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€Oú¤O¢ÖÊUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>é“>‰Z+W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I铨µrÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€Oú¤O¢ÖÊUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>é“>‰Z+W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I™ó ãÑŒ«®zAæ'ؾégÿöÁüOö _õš<ê¥ÏpÕŸïùžïáµ^ëµxðƒÌUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâùy¿×þq®ºêù¹é5ÿ;ÿ18ƒ«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·æ²Oøª×äQ/}†«þû|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏÏû½ösÕUÏÏM¯ùÜùûÁ\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0ÿ“}ÂW½&zé3\õßç{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~Þﵜ«®z~nzÍàÎß Îફ^­.Ðm­¸ø¤¸êªæºW|2çÿþÆ£W]õ‚ÌO°}Ó9ÎþíƒùŸì¾ê5yÔKŸáªÿ>ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóó~¯ýã\uÕósÓkþwþþcpW]õ‚lÝpnkÅÅ'ÝÀUW½0×½â“9ÿ÷·0͸êªd~ò€í›ÎqöoÌÿdŸðU¯É£^ú Wý÷ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUÿ |Îç|ÿ/ýÒ/Ík½Ökqüøq®úßKÏÏû½ösÕUÏÏM¯ùÜùûÁ\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0ÿ“}ÂW½&zé3\õßç{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUWý/ ‰‹÷~ï÷櫾ê«8~ü8ÿüõ_ÿ5ïó>ïÃ_ýÕ_qHâùy¿×þq®ºêù¹é5ÿ;ÿ18ƒ«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·æ²Oøª×äQ/}†«þû|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êªÿ$ñoõÒ/ýÒüÖoýÇç²ÏþìÏæs>çs°ÍU ‰ççý^ûǹêªçç¦×üîüýÇà ®ºêÙºáÝÖŠ‹Oº«®za®{Å'sþïoa<šqÕU/ÈüäÛ7ãìß>˜ÿÉ>á«^“G½ô®úïó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«þÄý~ë·~‹×~í׿ùùë¿þkvwwùîïþn¾ç{¾‡û½×{½ßýÝßÍÿd¯ýÚ¯ÍïüÎï`›«@ÏÏû½ösÕUÏÏM¯ùÜùûÁ\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0ÿ“}ÂW½&zé3\õßç{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUWý/ ‰ûýÖoý¯ýگͿ䫿ú«ù˜ùî÷ô§??øÁüOõÚ¯ýÚüÎïü¶¹ $ñü¼ßkÿ8W]õüÜôšÿÀ¿ÿœÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛó?Ù'|Õkò¨—>ÃUÿ}¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕÿ’¸ßoýÖoñÚ¯ýÚ¼(üàóŒg<€¯úª¯â£?ú£ùŸêµ_ûµùßùlsHâùy¿×þq®ºêù¹é5ÿ;ÿ18ƒ«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·æ²Oøª×äQ/}†«þû|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êªÿ$q¿ßú­ßâµ_ûµyQ¼÷{¿7ßó=ßÀg}ÖgñÙŸýÙÃUÿ}¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕÿ’¸ßoýÖoñÚ¯ýÚ¼(>û³?›ÏùœÏà³>ë³øìÏþlžÛç|ÎçðÕ_ýÕìîîòü¼ök¿6_õU_ÅK¿ôKóÂ|Îç|_ýÕ_Íîî.ÏÏ[¿õ[ó]ßõ]?~œzí×~m~çw~‡ççµ^ëµøíßþm>û³?›ÏùœÏÀ6/ó2/Ã_ÿõ_ó@_õU_ÅÇ|ÌÇpüøq.^¼È óÓ?ýÓ¼ÍÛ¼ õQÅWõWó?‰$žŸ÷{í窫žŸ›^ó¸ó÷ƒ3¸êªdë† t[+.>鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`þ'û„¯zMõÒg¸ê¿Ï÷|Ï÷ðZ¯õZ<øÁ檫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ú_@÷û­ßú-^ûµ_›Åk¿ökó;¿ó;|ÕW}ýÑͽÏû¼ßýÝßÍýô ñà?€¿þë¿æÒ¥K?~œ¯úª¯â½ßû½y~ÞæmÞ†ŸþéŸæ~zЃxðƒ À_ÿõ_séÒ%^ú¥_šŸú©ŸâÁ~0÷{í×~m~çw~‡ççµ^ëµøíßþm>û³?›ÏùœÏà³>ë³øœÏùžÛÅ‹yï÷~o~æg~€Ÿú©Ÿâ­ßú­yAÞû½ß›ïùžïàéO:~ðƒùŸDÏÏû½ösÕUÏÏM¯ùÜùûÁ\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0ÿ“}ÂW½&zé3\õßç{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUWý/ ‰ûýÖoý¯ýگͿä·û·y×yî÷WõW¼ôK¿4÷ûèþh¾æk¾€cÇŽñÓ?ýÓ¼ök¿6÷ÛÝÝå«¿ú«ùœÏùŽ?ÎoýÖoñÒ/ýÒ<ÐGôGó5_ó5<èA⻿û»yí×~mî·»»ËGôGó=ßó=¼ôK¿4õWÅs{í×~m~çw~Û<·ÏþìÏæs>çs¸ß±cÇøèþh^ûµ_›[o½•¿þë¿æ«¿ú«ùéŸþiÞæmÞ€÷z¯÷⻿û»y~vww9qâ/õR/Å_ÿõ_ó?$žŸ÷{í窫žŸ›^ó¸ó÷ƒ3¸êªdë† t[+.>鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`þ'û„¯zMõÒg¸ê¿Ï÷|Ï÷ðZ¯õZ<øÁ檫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ú_@÷û­ßú-^ûµ_›æ{¾ç{øèþhvwwx­×z-~û·›ûÝzë­<ä!àØ±cüöoÿ6/ýÒ/ÍóóÙŸýÙ|Îç|ïõ^ïÅw÷ws¿[o½•‡<ä!;vŒ¿þë¿æÁ~0ÏÏ[¿õ[ó3?ó3|×w}ïýÞïͽök¿6¿ó;¿€mžÛgögó9Ÿó9Üï·~ë·xí×~mžŸãÇséÒ%.^¼ÈñãÇynßýÝßÍû¼Ïûð]ßõ]¼÷{¿7ÿÓHâùy¿×þq®ºêù¹é5ÿ;ÿ18ƒ«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·æ²Oøª×äQ/}†«þû|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êªÿ$q¿—~é—æøñã¼ ¿ýÛ¿Í;vŒ¿þë¿æÁ~0÷ûê¯þj>æc>€ú¨â«¿ú«yaŽ?Î¥K—¸xñ"Çà£?ú£ùš¯ù>ë³>‹ÏþìÏæ¹õÖ[yÈCÀ[½Õ[ñÓ?ýÓ<Ðk¿ökó;¿ó;Øæ¹}ög6Ÿó9ŸÀK½ÔKñ×ý×¼ ýÑÍ×|Í×ð]ßõ]¼÷{¿7Ïíu^çuøíßþm.^¼ÈñãÇùŸFÏÏû½ösÕUÏÏM¯ùÜùûÁ\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0ÿ“}ÂW½&zé3\õßç{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUWý/ ‰‹—z©—⻿û»yé—~ièµ_ûµùßù~ë·~‹×~í׿…yï÷~o¾ç{¾€Ÿú©Ÿâ­ßú­xí×~m~çw~€¿ú«¿â¥_ú¥yaŽ?Î¥K—°Í½ök¿6¿ó;¿€mžÛgögó9Ÿó9|ÔG}_ýÕ_Í ò×ý×¼Ì˼ oõVoÅOÿôOó@·Þz+yÈCx¯÷z/¾û»¿›ÿ »»»|Îç|ßýÝßÍîî.ÿVï÷Ú?ÎUW=?7½æ?pçï?gpÕU/ÈÖ è¶V\|Ò \uÕ sÝ+>™ó ãÑŒ«®zAæ'ؾégÿöÁüOö _õš<ê¥ÏpÕŸïùžïáµ^ëµxðƒÌUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]õ¿€$^T¯õZ¯ÅK¿ôKóÚ¯ýÚ¼õ[¿5ÏÏ˼ÌËð×ýרæ_òÙŸýÙ|Îç|ŸõYŸÅgög ‰û}ög6ÿ’ïþîïæÖ[oàéO:~ðƒ¹ßk¿ökó;¿ó;Øæ¹}ög6Ÿó9ŸÀg}ÖgñÙŸýÙ¼0/ýÒ/ÍßüÍßðô§??øÁÜï«¿ú«ù˜ù~ê§~Š·~ë·æ¿ÂGôGó5_ó5ü{½ßkÿ8W]õüÜôšÿÀ¿ÿœÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛó?Ù'|Õkò¨—>ÃUÿ}¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕÿ’¸ßoýÖoñÚ¯ýÚü{Hâ~¶ù—üöoÿ6¯ó:¯Àg}ÖgñÙŸýÙHâßê·~ë·xí×~mî÷Ú¯ýÚüÎïü¶ynŸýÙŸÍç|ÎçðS?õS¼õ[¿5/ÌWõWó1ó1|ÕW}ýÑÍýò‡pë­·ò =ˆ[o½•ÿ*'Nœ`ww—¯÷{í窫žŸ›^ó¸ó÷ƒ3¸êªdë† t[+.>鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`þ'û„¯zMõÒg¸ê¿Ï÷|Ï÷ðZ¯õZ<øÁ檫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ú_@÷û­ßú-^ûµ_›IÜÏ6ÿ’ßþíßæu^çuø¬Ïú,>û³?I;vŒ—~é—æ_ã«¿ú«yé—~iî÷Ú¯ýÚüÎïü¶ynŸýÙŸÍç|Îçð[¿õ[¼ök¿6/Ìîî.'Nœà¥_ú¥ù«¿ú+þú¯ÿš—y™—à£>ê£øê¯þjþ«?~œK—.ñïõ~¯ýã\uÕósÓkþwþþcpW]õ‚lÝpnkÅÅ'ÝÀUW½0×½â“9ÿ÷·0͸êªd~ò€í›ÎqöoÌÿdŸðU¯É£^ú Wý÷ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUÿ Hâ~¿õ[¿Åk¿ökóïñà?˜g<ãØæ_òÙŸýÙ|Îç|ŸõYŸÅgög ‰ûÙæßãµ_ûµùßùlóÜ>û³?›ÏùœÏà·~ë·xí×~mþ%ïýÞïÍ÷|Ï÷ðWõW¼ôK¿4ýÑÍ×|Í×ðô§??øÁüWùèþh¾æk¾†¯÷{í窫žŸ›^ó¸ó÷ƒ3¸êªdë† t[+.>鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`þ'û„¯zMõÒg¸ê¿Ï÷|Ï÷ðZ¯õZ<øÁ檫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ú_@÷û­ßú-^ûµ_›×~í׿w~çwø­ßú-^ûµ_›æ½ßû½ùžïù~ê§~Š·~ë·à¥_ú¥ù›¿ùžþô§óà?˜«×~í׿w~çw°ÍsûìÏþl>çs>€ßú­ßâµ_ûµù—üôOÿ4oó6oÀG}ÔGñÕ_ýÕ<ä!áÖ[oå¥^ê¥øë¿þkþ«}ôG4ßýÝßÍ¥K—ø·z¿×þq®ºêù¹é5ÿ;ÿ18ƒ«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·æ²Oøª×äQ/}†«þû|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êªÿ$q¿ßú­ßâµ_ûµù÷øê¯þj>æc>€ú¨â«¿ú«yaNœ8Áîî.OúÓyðƒ ÀGôGó5_ó5|Ög}ŸýÙŸÍ ²»»ËCòŽ?΃ü`~ë·~‹zí×~m~çw~Û<·ÏþìÏæs>çsø­ßú-^ûµ_›Ńü`žñŒgðÒ/ýÒ|×w}/ó2/Àw}×wñÞïýÞüO&‰ççý^ûǹêªçç¦×üîüýÇà ®ºêÙºáÝÖŠ‹Oº«®za®{Å'sþïoa<šqÕU/ÈüäÛ7ãìß>˜ÿÉ>á«^“G½ô®úïó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«þÄý~ë·~‹×~í׿ßãÖ[oå!yÇç·~ë·xé—~ižŸÏþìÏæs>çsx­×z-~û·›ûÝzë­<ä!àøñãüÕ_ý~ðƒy~Þç}Þ‡ïþîïà­Þê­øéŸþièµ_ûµùßùlóÜ>û³?›ÏùœÏà·~ë·xí×~m^ýÑÍ×|Í×ð^ïõ^|Ï÷|/^äøñãüO&‰ççý^ûǹêªçç¦×üîüýÇà ®ºêÙºáÝÖŠ‹Oº«®za®{Å'sþïoa<šqÕU/ÈüäÛ7ãìß>˜ÿÉ>á«^“G½ô®úïó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«þÄý~ë·~‹×~í׿ßë£?ú£ùš¯ùŽ?ÎOýÔOñÚ¯ýÚÜoww—¯ùš¯á³?û³8vì¿ýÛ¿ÍK¿ôKó@ïýÞïÍ÷|Ï÷püøq~ê§~Š×~í׿~»»»|Í×| ŸýÙŸÍýþê¯þŠ—~é—æ^ûµ_›ßùßà³?û³y«·z+^ú¥_€ÏþìÏæs>çsø­ßú-^ûµ_›Å­·ÞÊCòè½Þë½øîïþnþ§“Äóó~¯ýã\uÕósÓkþwþþcpW]õ‚lÝpnkÅÅ'ÝÀUW½0×½â“9ÿ÷·0͸êªd~ò€í›ÎqöoÌÿdŸðU¯É£^ú Wý÷ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUÿ Hâ~¿õ[¿Åk¿ökóá½ßû½ùžïùî÷à?˜?øÁüõ_ÿ5»»»;vŒ¯þê¯æ½ßû½yn»»»¼ök¿6ó7Ãýüàóà?€¿þë¿fww—û}×w}ïýÞïÍsûèþh¾æk¾†:~ü8/^à³?û³ùœÏù~ë·~‹×~í׿EõÒ/ýÒüÍßü ÷û©Ÿú)Þú­ßšÿé$ñü¼ßkÿ8W]õüÜôšÿÀ¿ÿœÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛó?Ù'|Õkò¨—>ÃUÿ}¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕÿ’¸ßoýÖoñÚ¯ýÚüGùìÏþl¾ú«¿šK—.ñü¼Ök½_ýÕ_ÍK¿ôKóÂ|ög6_ýÕ_Í¥K—x~ô ñÕ_ýÕ¼õ[¿5ÏÏîî.¯ýÚ¯ÍßüÍßð@/^äøñã|ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ¼¨¾û»¿›÷yŸ÷àAz·Þz+ÿHâùy¿×þq®ºêù¹é5ÿ;ÿ18ƒ«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·æ²Oøª×äQ/}†«þû|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êªÿ~û·›û½ôK¿4Çç?Òîî.?ýÓ?Í­·ÞÊ_ÿõ_óà?˜ãÇóÖoýÖ¼ôK¿4/ªÝÝ]~ú§š[o½•¿þë¿æøñã<øÁæ¥_ú¥yë·~k^?ýÓ?Í_ÿõ_ðà?˜·~ë·æøñãÜzë­Üzë­¼ôK¿4ÇçEõ×ý×¼Ì˼ õQÅWõWó¿$žŸ÷{í窫žŸ›^ó¸ó÷ƒ3¸êªdë† t[+.>鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`þ'û„¯zMõÒg¸ê¿Ï÷|Ï÷ðZ¯õZ<øÁ檫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêÿ¼ÏþìÏæs>çsxúӟ΃ü`þ7Äóó~¯ýã\uÕósÓkþwþþcpW]õ‚lÝpnkÅÅ'ÝÀUW½0×½â“9ÿ÷·0͸êªd~ò€í›ÎqöoÌÿdŸðU¯É£^ú Wý÷ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUWýŸ÷‡<„[o½•×z­×â·û·ùßBÏÏû½ösÕUÏÏM¯ùÜùûÁ\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0ÿ“}ÂW½&zé3\õßç{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]õÚû¼ÏûðÝßýÝ|×w}ïýÞïÍÿ’x~Þﵜ«®z~nzÍàÎß Îફ^­.Ðm­¸ø¤¸êªæºW|2çÿþÆ£W]õ‚ÌO°}Ó9ÎþíƒùŸì¾ê5yÔKŸáªÿ>ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêªÿSþú¯ÿš÷yŸ÷áøñãÜzë­Üzë­¼ÔK½ý×Íÿ&’x~Þﵜ«®z~nzÍàÎß Îફ^­.Ðm­¸ø¤¸êªæºW|2çÿþÆ£W]õ‚ÌO°}Ó9ÎþíƒùŸì¾ê5yÔKŸáªÿ>ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêªÿs$ñ@ÇŽã·û·yé—~iþ7‘Äóó~¯ýã\uÕósÓkþwþþcpW]õ‚lÝpnkÅÅ'ÝÀUW½0×½â“9ÿ÷·0͸êªd~ò€í›ÎqöoÌÿdŸðU¯É£^ú Wý÷ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUWýŸóÒ/ýÒüÍßü oõVoÅgögóÒ/ýÒüo#‰ççý^ûǹêªçç¦×üîüýÇà ®ºêÙºáÝÖŠ‹Oº«®za®{Å'sþïoa<šqÕU/ÈüäÛ7ãìß>˜ÿÉ>á«^“G½ô®úïó=ßó=¼Ök½~ðƒ¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I™ó ãÑŒ«®zAæ'ؾégÿöÁüOö _õš<ê¥ÏpÕŸïùžïáµ^ëµxðƒÌUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâùy¿×þq®ºêù¹é5ÿ;ÿ18ƒ«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·æ²Oøª×äQ/}†«þû|Ï÷|¯õZ¯Åƒü`®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏÏû½ösÕUÏÏM¯ùÜùûÁ\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0ÿ“}ÂW½&zé3\õßç{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~Þﵜ«®z~nzÍàÎß Îફ^­.Ðm­¸ø¤¸êªæºW|2çÿþÆ£W]õ‚ÌO°}Ó9ÎþíƒùŸì¾ê5yÔKŸáªÿ>ßó=ßÃk½Ökñà?˜«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóó~¯ýã\uÕósÓkþwþþcpW]õ‚lÝpnkÅÅ'ÝÀUW½0×½â“9ÿ÷·0͸êªd~ò€í›ÎqöoÌÿdŸðU¯É£^ú Wý÷ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|Ò'}µV®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|ÁðIŸôIÔZ¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïáµ^ëµxðƒÌUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'}Ò'Qk媫^?ÿó?çž{îáÍßü͹êªæ¾áx§wz'NŸ>ÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×z­×âÁ~0W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|ŸôIŸD­•«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ëµ^‹?øÁ\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|Ò'}µV®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{x­×z-üàsÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾ç{¾‡×|Í×ä!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|Á|Ÿø‰ŸH×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷|Ï÷ðš¯ùš<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/ø‚/à?ñ麎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó_ð|â'~"]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßó=ßÃk¾ækò‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾à ¾€OüÄO¤ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ|Áð‰Ÿø‰t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|Ï÷|¯ùš¯ÉCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùù‚/ø>ñ?‘®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ð_À'~â'ÒuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®zÑ}ÂW½&zé3üò=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®zÑ}ÂW½&zé3üò=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®zÑ}ÂW½&zé3üò=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®zÑ}ÂW½&zé3üò=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(I鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®zÑ}ÂW½&zé3üò=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!ÛæªÿS~û·›ßùßà½Þë½xðƒÌUÿû|÷w7ÏxÆ3ø¬Ïú,軿û»yÆ3žÀg}Ögñ™$žŸ÷{í窫žŸ›^óqÜùûÆ\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0W½è>á«^“G½ôþ?ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕÿ)ŸýÙŸÍç|Îçð[¿õ[¼ök¿6WýïóÚ¯ýÚüÎïü¶y ×~í׿w~çw°Íÿe’x~Þﵜ«®z~nzÍÇqçï?gpÕU/ÈÖ è¶V\|Ò \uÕ sÝ+>™ó ãÑŒ«®zAæ'ؾégÿöÁ\õ¢û„¯zMõÒgøÿä{¾ç{xÍ×|Mò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUÿ§|ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ\õ¿Ïk¿ökó;¿ó;Øæ^ûµ_›ßùßÀ6ÿ—Iâùy¿×þq®ºêù¹é5Ç¿ÿhœÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛsÕ‹î¾ê5yÔKŸáÿ“ïùžïá5_ó5yÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6WýŸòÝßýÝ|÷w7_ýÕ_ÍK¿ôKsÕÿ>ýÑÍ_ÿõ_ðÛ¿ýÛ<Ðk¿ökó;¿ó;Øæÿ2I鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®zÑ}ÂW½&zé3üò=ßó=¼æk¾&yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ú_åµ_ûµùßùló™$žŸ÷{í窫žŸ›^óqÜùûÆ\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0W½è>á«^“G½ôþ?ùžïù^ó5_“‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUWý¯òÚ¯ýÚüÎïü¶ù¿LÏÏû½ösÕUÏÏM¯ñ8îüÃGã\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0W½è>á«^“G½ôþ?ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕÿ·Þz+ÏxÆ3x©—z)Ž?έ·ÞÊ÷|Ï÷pë­·rüøqŽ?Î{½×{ñà?˜äÖ[oåÏx/õR/Åñãǹ߭·ÞÊ3žñ ^ëµ^ €¿þë¿æg~æg¸õÖ[9~ü8Çç½Þë½xðƒÌ´ÝÝ]~æg~†[o½•¿þë¿æøñã<øÁæ½Þë½xðƒÌ¿dww—Ÿù™ŸáÖ[oå·û·yé—~iŽ?Î[½Õ[ñÒ/ýÒ¼ »»»üÍßü /õR/ÅñãÇÙÝÝåg~æg¸õÖ[ùë¿þk^ú¥_š×z­×âµ_ûµy ÝÝ]¾ç{¾‡[o½•[o½•—~é—æ­Þê­xé—~ižŸ[o½•g<ã¼Ök½ý×ÍÏüÌÏð×ý×<øÁæÁ~0ïõ^ïÅñãÇyAþú¯ÿšK—.ðZ¯õZ<Ðk¿ökó;¿ó;Øæ…ùë¿þk~æg~†ÝÝ]n½õV^ú¥_š—~é—æµ^ëµ8~ü8ÿÓIâùy¿×þq®ºêù¹é5ÇøhÜ‚«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·æªÝ'|Õkò¨—>Ãÿ'ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®úá³?û³ùœÏù~ë·~‹ßùßá³?û³y~Þû½ß›¯úª¯âøñã<·ÏþìÏæs>çsø­ßú-^ûµ_›û}ög6Ÿó9Ÿ€m>æc>†¯þê¯æùùèþh¾ê«¾Šÿ(Ÿó9ŸÃWõW³»»ËóóÖoýÖ|×w}Ççùù˜ù¾ú«¿šäµ_ûµùª¯ú*^ú¥_šçöÛ¿ýÛ¼Î뼿õ[¿Åîî.ïó>ïÃîî.Ïíµ_ûµù©Ÿú)Ž?Îw÷wó1ó1ìîîòÜÞû½ß›ïú®ïâ¹}ög6Ÿó9ŸÀÅ‹yŸ÷y~ú§šçç£?ú£ùª¯ú*žŸ×~í׿w~çw°Í½ök¿6¿ó;¿€mžŸ¿þë¿æc>æcøíßþmžŸãÇóU_õU¼÷{¿7ÿ“Iâùy¿×þq®ºêù¹é5ÇøhÜ‚«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·æªÝ'|Õkò¨—>Ãÿ'ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®úá³?û³ùœÏù^ú¥_š¿þë¿à¥^ê¥xë·~kn½õV~ú§šK—.ðÒ/ýÒüÖoýÇç>û³?›ÏùœÏà·~ë·xí×~mî÷ÙŸýÙ|Îç|ïýÞïÍw÷wðVoõV¼ôK¿4·Þz+?ýÓ?Í¥K—xé—~i~ë·~‹ãÇóïñ6oó6üôOÿ4÷{©—z)Ž?ÀïüÎïp¿—~é—æ·~ë·8~ü8÷ÛÝÝåu^çuøë¿þkî÷R/õR?~€ßùßá~Çç·~ë·xé—~iè·û·y×y>ê£>Нùš¯àرc¼ôK¿4ý×Í¥K—¸ß{½×{ñÚ¯ýÚ¼Ïû¼÷{­×z-n½õVžñŒgp¿Ïú¬Ïâ³?û³y ÏþìÏæs>çsxë·~k~ú§€·z«·â¥_ú¥ùë¿þk~æg~†û½÷{¿7ßõ]ßÅs{í×~m~çw~Û<Ðk¿ökó;¿ó;Øæ¹ýõ_ÿ5¯ó:¯Ãîî.÷{­×z-vwwù›¿ùî÷ÞïýÞ|×w}ÿSIâùy¿×þq®ºêù¹é5ÇøhÜ‚«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·æªÝ'|Õkò¨—>Ãÿ'ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®úá³?û³ùœÏùîwìØ1¾û»¿›·~ë·æ~»»»¼ök¿6ó7ÀG}ÔGñÕ_ýÕ<Ðgögó9Ÿó9üÖoý¯ýÚ¯Íý>û³?›ÏùœÏá~ÇŽã§ú§yí×~mîwë­·òÖoýÖüÍßü ŸõYŸÅgögóoõÙŸýÙ|Îç|zЃøîïþn^ûµ_›ûÝzë­¼õ[¿5ó7Àg}ÖgñÙŸýÙÜïmÞæmøéŸþi^ê¥^Šïþîïæ¥_ú¥¹ßîî.ýÑÍ÷|Ï÷püøqžþô§süøqî÷Û¿ýÛ¼Îë¼ôU_õU|ôG4÷ûîïþnÞç}Þ‡:vìßýÝßÍ[¿õ[s¿þèæk¾ækxðƒÌÓŸþtè³?û³ùœÏùîwìØ1~û·›—~é—æ~·Þz+oýÖoÍßüÍßð]ßõ]¼÷{¿7ôÚ¯ýÚüÎïü¶y ×~í׿w~çw°Ííîîò‡<„ÝÝ]>ê£>ŠÏþìÏæøñãÜï·û·yë·~k.]ºÀOýÔOñÖoýÖüO$‰ççý^ûǹêªçç¦×xwþá£q ®ºêÙºáÝÖŠ‹Oº«®za®{Å'sþïoa<šqÕU/ÈüäÛ7ãìß>˜«^tŸðU¯É£^ú ÿŸ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹ê„ÏþìÏæs>çs¸ßOýÔOñÖoýÖ<·ÝÝ]^ú¥_šg<ã<ýéOçÁ~0÷ûìÏþl>çs>€ßú­ßâµ_ûµ¹ßgögó9Ÿó9Üï·~ë·xí×~mžÛîî.~ðƒ¹téÇçéO:Çç_kww—‡<ä!ìîîrìØ1þú¯ÿš?øÁ<·ÝÝ]üàséÒ%Ž?ÎÅ‹øíßþm^çu^€cÇŽqë­·rüøqžŸ·~ë·æg~ægø¨ú(¾ú«¿šûýöoÿ6¯ó:¯Ãý¾ê«¾Šþèæ¹½ök¿6¿ó;¿Ãýþê¯þŠ—~é—æ¹½ôK¿4ó7ÀÓŸþtüàs¿ÏþìÏæs>çs¸ß_ýÕ_ñÒ/ýÒ<·[o½•‡<ä!?~œ‹/ò@¯ýÚ¯ÍïüÎï`›zí×~m~çw~Û<Ð{¿÷{ó=ßó=¼×{½ßýÝßÍóó×ý×¼Ì˼ ~ðƒyúÓŸÎÿD’x~Þﵜ«®z~nzÇqç>·àª«^­.Ðm­¸ø¤¸êªæºW|2çÿþÆ£W]õ‚ÌO°}Ó9Îþ탹êE÷ _õš<ê¥ÏðÿÉ÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«þGøìÏþl>çs>€—z©—â¯ÿú¯yA¾ú«¿šù˜à«¾ê«øèþhî÷ÙŸýÙ|Îç|¿õ[¿Åk¿öks¿ÏþìÏæs>çsx«·z+~ú§šä£?ú£ùš¯ù¾ë»¾‹÷~ï÷æ_뻿û»yŸ÷y>ê£>Нþê¯æyï÷~oþú¯ÿšãÇóÝßýÝ<øÁæ½ßû½ùžïù¾ë»¾‹÷~ï÷æÙÝÝåĉ?~œ‹/r¿ßþíßæu^çu8vì»»»û³?›ÏùœÏà½Þë½øîïþn^÷~ï÷æ{¾ç{ø©Ÿú)Þú­ßšû½ök¿6¿ó;¿€mèµ_ûµùßùló@'Nœ`ww€‹/rüøq^÷~ï÷æ{¾ç{ø©Ÿú)Þú­ßšÿi$ñü¼ßkÿ8W]õüÜôãÎ?|4nÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛsÕ‹î¾ê5yÔKŸáÿ“ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6WýðÙŸýÙ|Îç|_õU_ÅGôGó‚Üzë­<ä!àµ^ëµøíßþmî÷ÙŸýÙ|Îç|¿õ[¿Åk¿öks¿ÏþìÏæs>çsø©Ÿú)Þú­ßšä·û·y×yÞê­ÞŠŸþéŸæ_ë­ßú­ù™Ÿù~ë·~‹×~í׿_ã!y·Þz+/^äøñã¼0¯ýÚ¯ÍïüÎïðWõW¼ôK¿4¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÏÏgögó9Ÿó9|Ög}ŸýÙŸÍóóÙŸýÙ|Îç|¿õ[¿Åk¿öks¿ÏþìÏæs>çsø­ßú-^ûµ_›ä§ú§y›·y>ë³>‹ÏþìÏæ~¯ýÚ¯ÍïüÎï`›zí×~m~çw~ÛÜï·û·y×y^ëµ^‹ßþíßæ…ùîïþnÞç}Þ€Ïú¬Ïâ³?û³ù϶»»Ëç|ÎçðÝßýÝìîîòoõ~¯ýã\uÕósÓk<Ž;ÿðѸW]õ‚lÝpnkÅÅ'ÝÀUW½0×½â“9ÿ÷·0͸êªd~ò€í›ÎqöoÌU/ºOøª×äQ/}†ÿO¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\õ?Âgögó9Ÿó9üÖoý¯ýÚ¯Í # €?øÁ<ýéOç~ŸýÙŸÍç|Îçð[¿õ[¼ök¿6÷ûìÏþl>çs>€¿ú«¿â¥_ú¥ya$ðZ¯õZüöoÿ6ßýÝßÍ3žñ ^=èA¼÷{¿7¯ýÚ¯ÍïüÎïpñâEŽ?ο†$Ž;Æîî.ÿ’ÏþìÏæs>çsø©Ÿú)Þú­ß€ßþíßæu^çux¯÷z/¾û»¿›çç³?û³ùœÏù>ë³>‹ÏþìÏæùùìÏþl>çs>€ßú­ßâµ_ûµ¹ßgögó9Ÿó9\¼x‘ãÇó‚üõ_ÿ5/ó2/Àk½ÖkñÛ¿ýÛÜïµ_ûµùßùló@¯ýÚ¯ÍïüÎï`›û}ög6Ÿó9ŸÀk¿ökóÚ¯ýÚ¼0·Þz+ßýÝß Àk½ÖkñÛ¿ýÛügûèþh¾æk¾†¯÷{í窫žŸ›^ãqÜù‡Æ-¸êªdë† t[+.>鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®zÑ}ÂW½&zé3üò½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæªÿ>û³?›ÏùœÏà·~ë·xí×~m^IÜÏ6÷ûìÏþl>çs>€ßú­ßâµ_ûµ¹ßgögó9Ÿó9Øæ_" €?øÁ<ýéOàµ_ûµùßù^×z­×â·û·x™—yþú¯ÿÛükIàµ^ëµøíßþmþ%ŸýÙŸÍç|ÎçðYŸõY|ög6¿ýÛ¿Íë¼ÎëðYŸõY|ög6ÏÏgögó9Ÿó9|Ög}ŸýÙŸÍóóÙŸýÙ|Îç|¿õ[¿Åk¿öks¿ÏþìÏæs>çs°Í¿D¯õZ¯Åoÿöos¿×~í׿w~çw°Í½ök¿6¿ó;¿€mî÷ÙŸýÙ|Îç|ÿ¯õZ¯ÅoÿöoóŸíĉìîîòïõ~¯ýã\uÕósÓk<Ž;ÿðѸW]õ‚lÝpnkÅÅ'ÝÀUW½0×½â“9ÿ÷·0͸êªd~ò€í›ÎqöoÌU/ºOøª×äQ/}†ÿO¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\õ?Âgögó9Ÿó9üÖoý¯ýÚ¯Í #‰ûÙæ~ŸýÙŸÍç|Îçð[¿õ[¼ök¿6÷ûìÏþl>çs>ÛüK$ðR/õRüõ_ÿ5¯ýÚ¯ÍïüÎïð‚¼Ök½¿ýÛ¿ Àƒü`žñŒg`›-I¼Ök½¿ýÛ¿Í¿ä³?û³ùœÏù>ë³>‹ÏþìÏà·û·y×y>ë³>‹ÏþìÏæùùìÏþl>çs>€Ïú¬Ïâ³?û³y~>û³?›ÏùœÏà·~ë·xí×~mî÷ÙŸýÙ|Îç|¶ù—Hàµ^ëµøíßþmî÷Ú¯ýÚüÎïü¶y ×~í׿w~çw°Íý>û³?›ÏùœÏàAz~ðƒyQ½ôK¿4_ýÕ_ͶãÇséÒ%þ½Þﵜ«®z~nzÇqç>·àª«^­.Ðm­¸ø¤¸êªæºW|2çÿþÆ£W]õ‚ÌO°}Ó9Îþ탹êE÷ _õš<ê¥ÏðÿÉ÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«þGøìÏþl>çs>€ßú­ßâµ_ûµya$ðR/õRüõ_ÿ5÷ûìÏþl>çs>€ßú­ßâµ_ûµ¹ßgögó9Ÿó9<ýéOçÁ~0/Èîî.'Nœàµ^ëµøíßþm¾û»¿›[o½•äÁ~0ïýÞï Àk¿ökó;¿ó;Øæ_K~ðƒyúӟοä³?û³ùœÏù¾ë»¾‹÷~ï÷à·û·y×y>ë³>‹ÏþìÏæùùìÏþl>çs>€Ïú¬Ïâ³?û³y~>û³?›ÏùœÏà·~ë·xí×~mî÷ÙŸýÙ|Îç|¶ya~û·›×y×à­Þê­øéŸþiî÷Ú¯ýÚüÎïü¶y ×~í׿w~çw°Íý>û³?›ÏùœÏà³>ë³øìÏþlþ§ùèþh¾æk¾†¯÷{í窫žŸ›^ãqÜù‡Æ-¸êªdë† t[+.>鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®zÑ}ÂW½&zé3üò½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæªÿ>û³?›ÏùœÏ໾ë»xï÷~o^¿þë¿æe^æex«·z+~ú§šû}ög6Ÿó9ŸÀoýÖoñÚ¯ýÚÜï³?û³ùœÏù~ë·~‹×~í׿ùíßþm^çu^€÷z¯÷⻿û»ù×zë·~k~æg~€ßú­ßâµ_ûµyA~û·›ÏùœÏáµ_ûµy­×z-^ûµ_›—~é—æoþæo¸xñ"Çç…yí×~m~çw~€ßú­ßâµ_ûµøíßþm^çu^€Ïú¬Ïâ³?û³y~>û³?›ÏùœÏà³>ë³øìÏþlžŸÏþìÏæs>çsø­ßú-^ûµ_›û}ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ¼ ßýÝßÍû¼ÏûðYŸõY|ög6÷{í×~m~çw~Û<Ðk¿ökó;¿ó;Øæ~¿ýÛ¿Íë¼ÎëðVoõVüôOÿ4ÿ}ôG4ßýÝßÍ¥K—ø·z¿×þq®ºêù¹é5ÇøhÜ‚«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·æªÝ'|Õkò¨—>Ãÿ'ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®úá³?û³ùœÏùÞê­ÞŠŸþéŸæùìÏþl>çs>€ïú®ïâ½ßû½¹ßgögó9Ÿó9üÖoý¯ýÚ¯Íý>û³?›ÏùœÏà½Þë½øîïþn^÷~ï÷æ{¾ç{ø©Ÿú)Þú­ßš­¯þê¯æc>æcø¨ú(¾ú«¿šä£?ú£ùš¯ù~ê§~Š·~ë·æ½ßû½ùžïù¾ë»¾‹÷~ï÷æ¹õÖ[yÈCÂýls¿ßþíßæu^çuø¬Ïú,>û³?›çç³?û³ùœÏù>ë³>‹ÏþìÏæùùìÏþl>çs>€ßú­ßâµ_ûµ¹ßgögó9Ÿó9|Ög}ŸýÙŸÍ òÖoýÖüÌÏü õWÅK¿ôKs¿×~í׿w~çw°Í½ök¿6¿ó;¿€mî·»»Ë‰'8~ü8OúÓ9~ü8/Ègögó9Ÿó9¼ôK¿4ïõ^ïÅGôGó?$žŸ÷{í窫žŸ›^ãqÜù‡Æ-¸êªdë† t[+.>鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®zÑ}ÂW½&zé3üò½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæªÿ>û³?›ÏùœÏá~¿õ[¿Åk¿ökóÜvwwyÈCÂîî.ÇŽãÖ[oåøñãÜï³?û³ùœÏù~ë·~‹×~í׿~ŸýÙŸÍç|Îçp¿¿ú«¿â¥_ú¥yný×Í˼ÌËpìØ1vwwù·ØÝÝåĉ?~œ§?ýé?~œçvë­·ò2/ó2ìîîrìØ1n½õVŽ?Î_ÿõ_ó2/ó2?~œ§?ýé?~œççmÞæmøéŸþiÞë½Þ‹ïþîïæ~¿ýÛ¿Íë¼ÎëðYŸõY|ög6ÏÏgögó9Ÿó9|Ög}ŸýÙŸÍóóÙŸýÙ|Îç|¿õ[¿Åk¿öks¿ÏþìÏæs>çs8~ü8OúÓ9~ü8Ïí·û·y×yô që­·ò@¯ýÚ¯ÍïüÎï`›zí×~m~çw~Û<Ð{¿÷{ó=ßó=¼÷{¿7ßõ]ßÅó³»»ËCòvwwø­ßú-^ûµ_›ÿi$ñü¼ßkÿ8W]õüÜôãÎ?|4nÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛsÕ‹î¾ê5yÔKŸáÿ“ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6WýðÙŸýÙ|Îç|÷;~ü8?õS?Åk¿öks¿¿þë¿æ}Þç}øë¿þk¾ê«¾Šþèæ>û³?›ÏùœÏà·~ë·xí×~mî÷ÙŸýÙ|Îç|÷;~ü8¿õ[¿ÅK¿ôKs¿ßþíßæmÞæmØÝÝà§~ê§xë·~kþ­>ú£?š¯ùš¯à¥_ú¥ù®ïú.^ú¥_šûÝzë­¼ÍÛ¼ ý× Àg}ÖgñÙŸýÙÜï½ßû½ùžïù^ú¥_šïú®ïâ¥_ú¥¹ßîî.ó1Ãw÷wpìØ1n½õVŽ?Îý~û·›×y×à³>ë³øìÏþlžŸÏþìÏæs>çsø¬Ïú,>û³?›çç³?û³ùœÏù~ë·~‹×~í׿~ŸýÙŸÍç|Îçp¿—~é—æ»¾ë»xé—~iî÷Û¿ýÛ¼ÍÛ¼ »»»üÖoý¯ýگͽök¿6¿ó;¿€mèµ_ûµùßùló@·Þz+/ýÒ/Í¥K—xï÷~o¾ê«¾ŠãÇs¿[o½•·y›·á¯ÿú¯x­×z-~û·›ÿ‰$ñü¼ßkÿ8W]õüÜôãÎ?|4nÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛsÕ‹î¾ê5yÔKŸáÿ“ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6WýðÙŸýÙ|Îç|ÇŽãÒ¥K¼ôK¿4Çgww—¿þë¿æ~ïõ^ïÅw÷wóÜ>û³?›ÏùœÏà·~ë·xí×~mî÷ÙŸýÙ|Îç|ÇŽãÒ¥K¼ôK¿4Çgww—¿þë¿æ~ïõ^ïÅw÷wóïõÚ¯ýÚüÎïü÷{é—~iŽ?Àoÿöos¿×z­×â·û·y ÝÝ]^ûµ_›¿ù›¿á~/ýÒ/ÍñãÇøíßþmîwìØ1~û·›—~é—æ~û·›×y×à³>ë³øìÏþlžŸÏþìÏæs>çsø¬Ïú,>û³?›çç³?û³ùœÏù~ë·~‹×~í׿~ŸýÙŸÍç|ÎçpìØ1.]ºÀk¿ök°»»Ë_ÿõ_s¿¯úª¯â£?ú£yn¯ýÚ¯ÍïüÎï`›zí×~m~çw~Û<·ŸþéŸæ½ßû½¹té÷{í×~mî÷Û¿ýÛÜï¥^ê¥øíßþmŽ?ÎÿD’x~Þﵜ«®z~nzÇqç>·àª«^­.Ðm­¸ø¤¸êªæºW|2çÿþÆ£W]õ‚ÌO°}Ó9Îþ탹êE÷ _õš<ê¥ÏðÿÉ÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«þGøìÏþl>çs>€Ïú¬Ïbww—¯ùš¯á¹;vŒþèæ³?û³y~>û³?›ÏùœÏà·~ë·xí×~mî÷ÙŸýÙ|Îç|?õS?Åw÷wó3?ó3<·cÇŽñÕ_ýÕ¼÷{¿7ÿQ>û³?›ÏùœÏáù¨ú(>û³?›ãÇóÜvwwùìÏþl¾æk¾†äµ^ëµøîïþnüàóÜ~û·›×y×à³>ë³øìÏþlžŸÏþìÏæs>çsø¬Ïú,>û³?›çç³?û³ùœÏù~ë·~‹×~í׿~ŸýÙŸÍç|ÎçðS?õS|õW5¿ó;¿Ãs{ЃÄgögóÞïýÞÃÿ'ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®úá³?û³ùœÏù>ë³>‹ÏþìÏæ?Úgögó9Ÿó9üÖoý¯ýÚ¯ÍUÿ±>û³?›ÏùœÏà·~ë·xí×~m®ú·“Äóó~¯ýã\uÕósÓk<Ž;ÿðѸW]õ‚lÝpnkÅÅ'ÝÀUW½0×½â“9ÿ÷·0͸êªd~ò€í›ÎqöoÌU/ºOøª×äQ/}†ÿO¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\õ?Âgögó9Ÿó9|Ög}ŸýÙŸÍ´ÏþìÏæs>çsø­ßú-^ûµ_›«þc}ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ\õo'‰ççý^ûǹêªçç¦×xwþá£q ®ºêÙºáÝÖŠ‹Oº«®za®{Å'sþïoa<šqÕU/ÈüäÛ7ãìß>˜«^tŸðU¯É£^ú ÿŸ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹ê„ÏþìÏæs>çsø¬Ïú,>û³?›ÿhŸýÙŸÍç|Îçð[¿õ[¼ök¿6WýÇúìÏþl>çs>€ßú­ßâµ_ûµ¹êßNÏÏû½ösÕUÏÏM¯ñ8îüÃGã\uÕ ²uú­ŸtW]õÂ\÷ŠOæüßßÂx4㪫^ùɶo:ÇÙ¿}0W½è>á«^“G½ôþ?ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕÿŸýÙŸÍç|ÎçðYŸõY|ög6ÿÑ>û³?›ÏùœÏà·~ë·xí×~m®úõÙŸýÙ|Îç|¿õ[¿Åk¿öksÕ¿$žŸ÷{í窫žŸ›^ãqÜù‡Æ-¸êªdë† t[+.>鮺ꅹîŸÌù¿¿…ñhÆUW½ ó“lßt޳û`®zÑ}ÂW½&zé3üò½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæªÿ¾û»¿›ïþîïà½ßû½yï÷~oþ£}÷w7ßýÝß ÀWõWóÒ/ýÒ\õ뻿û»ùîïþn¾ú«¿š—~é—æª;I™ó ãÑŒ«®zAæ'ؾégÿöÁ\õ¢û„¯zMõÒgøÿä{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~Þﵜ«®z~nzÇqç>·àª«^­.Ðm­¸ø¤¸êªæºW|2çÿþÆ£W]õ‚ÌO°}Ó9Îþ탹êE÷ _õš<ê¥ÏðÿÉ÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñü¼ßkÿ8W]õüÜôãÎ?|4nÁUW½ [7\ ÛZqñI7pÕU/Ìu¯ødÎÿý-ŒG3®ºê™Ÿ<`û¦sœýÛsÕ‹î¾ê5yÔKŸáÿ“ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâùy¿×þq®ºêù¹é5ÇøhÜ‚«®zA¶n¸@·µââ“nફ^˜ë^ñÉœÿû[f\uÕ 2?yÀöMç8û·æªÝ'|Õkò¨—>Ãÿ'ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóó~¯ýã\uÕósÓk<Ž;ÿðѸW]õ‚lÝpnkÅÅ'ÝÀUW½0×½â“9ÿ÷·0͸êªd~ò€í›ÎqöoÌU/ºOøª×äQ/}†ÿO¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰ççý^ûǹêªçç¦×xwþá£q ®ºêÙºáÝÖŠ‹Oº«®za®{Å'sþïoa<šqÕU/ÈüäÛ7ãìß>˜«^tŸðU¯É£^ú ÿŸ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!Ÿð Ÿ@×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áò Ÿð t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!Ÿð Ÿ@×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áò Ÿð t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!Ÿð Ÿ@×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áò Ÿð t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!Ÿð Ÿ@×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áò Ÿð t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!Ÿð Ÿ@×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áò Ÿð t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!Ÿð Ÿ@×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áò Ÿð t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>á>®ë¸êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïå5^ã5xÈCÂUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_È'|Â'ÐuW]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼Æk¼yÈC¸êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—×x×à!yW]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!Ÿð Ÿ@×u\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷ò¯ñ<ä!᪫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/ä>á躎«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ã5^ƒ‡<ä!\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|Â'|]×qÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßËk¼Ækð‡<„«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿Oø„O ë:®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{y×x ò‡pÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áò Ÿð t]ÇUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯ñ¯ÁCò®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâù±ÍUW=?_ø…_ÈÇüÇÓ÷=W]õ‚üùŸÿ9÷ÜsoþæoÎUW½0ßð ßÀ;½Ó;qúôi®ºêyÊSžÂÿñóîïþî\uÕ ó½ßû½¼ú«¿:}èC¹êªäž{îá§ú§ùàþ`®ºê…ù™Ÿùô ñÒ/ýÒ\uÕUW]õ|!Ûæª«®ºê(IÍUW½ OyÊSøã?þcÞýÝß«®za¾÷{¿—WõWç¡}(W]õ‚ÜsÏ=üôOÿ4üÁÌUW½0?ó3?Ãô ^ú¥_š«®ºêª«ž/dÛ\uÕUWý%‰çÇ6W]õü|á~!ÿñOß÷\uÕ òçþçÜsÏ=¼ù›¿9W]õÂ|Ã7|ïôNïÄéÓ§¹êªä)Oy üÇÌ»¿û»sÕU/Ì÷~ï÷òê¯þê<ô¡媫^{ŸþéŸæƒ?øƒ¹êªæg~ægxЃÄK¿ôKsÕUW]uÕó…l›«®ºêªÿ¡$ñüØæª«žŸ/üÂ/äã?þãéûž«®zAþüÏÿœ{7ó7窫^˜oø†oàÞé8}ú4W]õ‚<å)Oáÿøy÷ww®ºê…ùÞïý^^ýÕ_‡>ô¡\uÕ rÏ=÷ðÓ?ýÓ|ð0W]õÂüÌÏü zЃxé—~i®ºêª«®z¾msÕUW]õ?”$žÛ\uÕóó…_ø…|üÇ<}ßsÕU/ÈŸÿùŸsÏ=÷ðæoþæ\uÕ ó ßð ¼Ó;½§OŸæª«^§<å)üñÿ1ïþîïÎUW½0ßû½ßË«¿ú«óЇ>”«®zAî¹ç~ú§šþà檫^˜Ÿù™ŸáAz/ýÒ/ÍUW]uÕUϲm®ºêª«þ‡’Äóc›«®z~¾ð ¿ÿø§ï{®ºêùó?ÿsî¹çÞüÍßœ«®za¾á¾wz§wâôéÓ\uÕ ò”§<…?þã?æÝßýݹêªæ{¿÷{yõWuúЇrÕU/È=÷ÜÃOÿôOóÁüÁ\uÕ ó3?ó3<èAâ¥_ú¥¹êª«®ºêùB¶ÍUW]uÕÿP’x~lsÕUÏÏ~áòñÿñô}ÏUW½ þçÎ=÷ÜÛ¿ù›sÕU/Ì7|Ã7ðNïôNœ>}š«®zAžò”§ðÇüǼû»¿;W]õÂ|ï÷~/¯þê¯ÎCúP®ºê¹çž{øéŸþi>øƒ?˜«®za~æg~†=èA¼ôK¿4W]uÕUW=_ȶ¹êª«®úJÏm®ºêùùÂ/üB>þã?ž¾ï¹êªäÏÿüϹçž{xó7s®ºê…ù†oøÞéÞ‰Ó§OsÕU/ÈSžòþøÿ˜w÷w窫^˜ïýÞïåÕ_ýÕyèCÊUW½ ÷Üs?ýÓ?ÍðsÕU/ÌÏüÌÏð =ˆ—~é—æª«®ºêªç Ù6W]uÕUÿCIâßëÁ~0ýÑÍG}ÔGñ/¹õÖ[ù˜ù~ú§š«—~é—æ«¾ê«xí×~mþ%¿ýÛ¿ÍÇ|ÌÇð×ý×ü[½õ[¿5_õU_Ńü`þ%Ÿó9ŸÃw÷wsë­·òoqüøq>ú£?šÏú¬Ïâ_²»»ËÇ|ÌÇðÓ?ýÓìîîòoñÒ/ýÒ|Ög}oýÖoÍ¿äG~äGø˜ùî¾ûnþ­Þú­ßšÏú¬Ïâ¥_ú¥ù—üôOÿ4ó1í·ÞÊ¿ÅñãÇyï÷~o>ë³>‹ãÇóÂìîîò9Ÿó9|÷w7»»»ü[<øÁæ£?ú£ù¨ú(þ%·Þz+ó1ÃOÿôOóoõÒ/ýÒ|ÕW}¯ýگͿä·û·ù˜ùþú¯ÿš«·~ë·æ»¾ë»8~ü8ÿ’×y×áOþäOX.—ü[<øÁæ½ßû½ù¬Ïú,þ%»»»|ÌÇ| ?ýÓ?Íîî.ÿ/ýÒ/Íg}ÖgñÖoýÖüKþú¯ÿšù˜á·û·ù·zë·~k>ë³>‹—~é—æ_ò5_ó5|õW5·Þz+ÿÇç½ßû½ù¬Ïú,Ž?Î ³»»Ëç|ÎçðÝßýÝìîîòoñà?˜þèæ£>ê£ø—üþïÿ>ïøŽïÈÝwßÍ¿ÕK¿ôKóU_õU¼ök¿6ÿ’ŸþéŸæs>çsøë¿þkþ­Þû½ß›¯úª¯âøñãüK>çs>‡ïþîïæÖ[oåßâÁ~0ïýÞïÍg}Ögñ/ÙÝÝåc>æcøéŸþivwwù·xé—~i>ë³>‹·~ë·æ_ò×ý×¼Ïû¼ý×Í¿Õ[¿õ[óU_õU<øÁæ_ò¶oû¶üÒ/ý«ÕŠ‹ãÇóÞïýÞ|Ög}Çç…ÙÝÝås>çsøîïþnvwwù·xðƒÌGôGóQõQüKn½õV>æc>†ŸþéŸæßêµ_ûµùª¯ú*^ú¥_šÉOÿôOó9Ÿó9üõ_ÿ5ÿÇç­ßú­ùª¯ú*Ž?οäs>çsøîïþnn½õVþ-üàóÞïýÞ|Ög}ÿ’g<ã¼þë¿>OyÊSø·zé—~i¾ê«¾Š×~í׿_òÛ¿ýÛ|ÌÇ| ý×Í¿Õ[¿õ[óU_õU<øÁæ_ò5_ó5|õW5·Þz+ÿÇç½ßû½ù¬Ïú,Ž?Î ³»»Ëç|ÎçðÝßýÝìîîòoñà?˜þèæ£>ê£ø—üõ_ÿ5Ÿó9ŸÃOÿôOóoõÚ¯ýÚ|ÕW}/ýÒ/Í¿äS>åSøú¯ÿzø·8~ü8oýÖoÍW}ÕWqüøqþ%Ÿó9ŸÃw÷wsë­·òoñà?˜÷~ï÷æ³>ë³ø—Üzë­|ÌÇ| ?ýÓ?Í¿ÕK¿ôKóU_õU¼ök¿6ÿ’ßþíßæc>æcøë¿þkþ­Þú­ßš¯úª¯âÁ~0ÿ’¯ùš¯á«¿ú«¹õÖ[ù·8~ü8ïýÞïÍg}Ögqüøq^˜g<ã¼þë¿>·ÝvÃ0ðoñà?˜¯úª¯â­ßú­ù—üõ_ÿ5Ÿó9ŸÃOÿôOs¿ãÇóÞïýÞ|ÕW}W]uÕUÿƒ!Ûæª«®ºê(IüGùª¯ú*>ú£?šæe^æeøë¿þkþ½Ž?Î_ýÕ_ñà?˜äÖ[oåe^æeØÝÝåßë¥_ú¥ù«¿ú+^˜¯þê¯æc>æcøðYŸõY|ög6/ÌÛ¼ÍÛðÓ?ýÓüGø­ßú-^ûµ_›äÏxyÈC°Í¿×ƒü`þê¯þŠãÇó‚üöoÿ6¯ó:¯Ã„·~ë·æ§~ê§xa>ú£?š¯ùš¯á?ÂOýÔOñÖoýÖ¼0/ó2/Ã_ÿõ_óïuüøqþê¯þŠ?øÁ¼ ý×Í˼ÌËðá¥_ú¥ù«¿ú+^˜·y›·á§ú§ùðYŸõY|ög6/Ìë¼ÎëðÛ¿ýÛüGø­ßú-^ûµ_›dww—‡<ä!ìîîòïõà?˜¿ú«¿âøñã¼ ?ýÓ?ÍÛ¼ÍÛðá­ßú­ù©Ÿú)^˜þèæk¾ækøðS?õS¼õ[¿5/È3žñ þð‡3Mÿ^Çç¯þê¯xðƒÌ ò×ý×¼Ì˼ ÿ^ûµ_›ßú­ßâ…ùìÏþl>çs>‡ÿŸõYŸÅgögó¼Î뼿ýÛ¿Í„¿ú«¿â¥_ú¥yAn½õV^æe^†ÝÝ]þ½üàóWõW?~œäS>åSøâ/þbþ#|ÔG}_ýÕ_Í óÑýÑ|Í×| ÿ~ê§~Š·~ë·æÙÝÝåe^æe¸õÖ[ù÷:~ü8OúÓ9~ü8/È_ÿõ_ó2/ó2üGxí×~m~ë·~‹æ³?û³ùœÏùþ#|ÕW}ýÑÍ sâÄ vwwùðWõW¼ôK¿4/È­·ÞÊ˼Ì˰»»Ë¿×ƒü`žþô§óÂüôOÿ4oó6oÄú¨â«¿ú«yaÞæmÞ†ŸþéŸæ?ÂOýÔOñÖoýÖ¼ »»»¼Ì˼ ·Þz+ÿ^ÇçéO:Ççùê¯þj>æc>†ÿ¯ýÚ¯ÍoýÖoñÂ|ög6Ÿó9ŸÃ„¯úª¯â£?ú£ya^æe^†¿þë¿æ?Â_ýÕ_ñÒ/ýÒ¼ ·Þz+/ó2/Ãîî.ÿ^/ýÒ/Í_ýÕ_ñÂüôOÿ4oó6oÄú¨â«¿ú«yan¸áî¾ûnþ#üÖoý¯ýÚ¯Í ²»»Ë˼ÌËpë­·òü|ÔG}_ýÕ_ÍUW]uÕÿPȶ¹êª«®úJÿQ^ëµ^‹ßþíßæùíßþm^çu^‡ÿ(ŸõYŸÅgögó‚|ög6Ÿó9ŸÃ”¿ú«¿â¥_ú¥yA^ûµ_›ßùßá?ƒü`žþô§ó‚ìîîrâÄ þ£|ÔG}_ýÕ_Í ò‘ù‘|Ý×}ÿQ~ê§~Š·~ë·æyï÷~o¾ç{¾‡ÿ(¶yaNœ8Áîî.ÿ^ëµ^‹ßþíßæùíßþm^çu^‡ÿ(_õU_ÅGôGó‚|ög6Ÿó9ŸÃ”¿ú«¿â¥_ú¥yA¶··988à?ƒü`žþô§ó‚ìîîrâÄ þ£|ÔG}_ýÕ_Í òÝßýݼÏû¼ÿQ~ë·~‹×~í׿yë·~k~æg~†ÿ(¶ya$ñåµ^ëµøíßþm^¯þê¯æc>æcøòU_õU|ôG4/Ègögó9Ÿó9üGyúӟ΃ü`^—y™—á¯ÿú¯ùðÒ/ýÒüÕ_ý/È­·ÞÊCòþ£|Ög}ŸýÙŸÍ òÕ_ýÕ|ÌÇ| ÿQ~ë·~‹×~í׿¹á†¸ûî»ùb›FÿQÞê­ÞŠŸþéŸæùíßþm^çu^‡ÿ(ßõ]ßÅ{¿÷{ó‚|ôG4_ó5_Ô§?ýé<øÁæy™—yþú¯ÿšÿ/ýÒ/Í_ýÕ_ñ‚üþïÿ>¯ñ¯Á”Ïú¬Ïâ³?û³yA¾ú«¿šù˜á?ÊoýÖoñÚ¯ýÚ¼ ¯ýÚ¯ÍïüÎïðÅ6/Œ$þ£¼Õ[½?ýÓ?Í òÛ¿ýÛ¼Îë¼ÿQ¾ë»¾‹÷~ï÷æyÙ—}Yþê¯þŠÿ(/^äøñã¼ yÈC¸õÖ[ùðZ¯õZüöoÿ6/È_ÿõ_ó2/ó2üGù¬Ïú,>û³?›ä³?û³ùœÏùþ£üÖoý¯ýÚ¯Í òÚ¯ýÚüÎïüÿŽ?ÎÅ‹ya$ñå½Þë½øîïþn^ŸþéŸæmÞæmxAŽ?ÎÅ‹¹êª«®ú Ù6W]uÕUÿCIâ?Êk½ÖkñÛ¿ýÛ¼ ¿ýÛ¿Íë¼Îëðå³>ë³øìÏþl^ÏþìÏæs>çsøòWõW¼ôK¿4/Èk¿ökó;¿ó;üGxЃÄ­·ÞÊ ²»»Ë‰'øòQõQ|õW5/È[¿õ[ó3?ó3üGù©Ÿú)Þú­ßšä½ßû½ùžïùþ£Øæ…9~ü8—.]â?Âk½ÖkñÛ¿ýÛ¼ ¿ýÛ¿Íë¼Îëðå«¾ê«øèþh^ÏþìÏæs>çsøòWõW¼ôK¿4/ˆ$þ£<èAâÖ[oåÙÝÝåĉüGù¨ú(¾ú«¿š仿û»yŸ÷yþ£üÖoý¯ýÚ¯Í òÖoýÖüÌÏü ÿQlóÂHâ?Êk½ÖkñÛ¿ýÛ¼ _ýÕ_ÍÇ|ÌÇðå«¾ê«øèþh^ÏþìÏæs>çsøòô§??øÁ¼ /ýÒ/ÍßüÍßðá¥^ê¥øë¿þk^[o½•‡<ä!üGù¬Ïú,>û³?›ä«¿ú«ù˜ùþ£üÖoý¯ýÚ¯Í ræÌÎ;ÇÛ¼0’øòVoõVüôOÿ4/Èoÿöoó:¯ó:üGù®ïú.Þû½ß›ä£?ú£ùš¯ùþ£<ýéOçÁ~0/ÈK¿ôKó7ó7üGx©—z)þú¯ÿšä‡ø‡y—wyþ£|Ög}ŸýÙŸÍ òÕ_ýÕ|ÌÇ| ÿQ~ë·~‹×~í׿yí×~m~çw~‡ÿÇŽcww—FÿQÞê­ÞŠŸþéŸæùíßþm^çu^‡ÿ(ßõ]ßÅ{¿÷{ó‚Ü|óÍÜqÇüG¹xñ"ÇçyðƒÌ3žñ þ#¼Ök½¿ýÛ¿Í ò×ý×¼Ì˼ ÿQ>ë³>‹ÏþìÏæùìÏþl>çs>‡ÿ(¿õ[¿Åk¿ökó‚¼ök¿6¿ó;¿Ã„cÇŽ±»»Ë #‰ÿ(ïõ^ïÅw÷wó‚üôOÿ4oó6oà rìØ1vww¹êª«®ú Ù6W]uÕUÿCIâ?ÊW}ÕWñÑýѼ0/ýÒ/ÍßüÍßðïuìØ1þú¯ÿš?øÁ¼ ·Þz+/ýÒ/Í¥K—ø÷z©—z)þú¯ÿšæ«¿ú«ù˜ùþ#|Ög}ŸýÙŸÍ óÖoýÖüÌÏü ÿ~ë·~‹×~í׿ùäOþd¾äK¾„ÿzЃøë¿þkŽ?Î òÛ¿ýÛ¼Îë¼ÿÞê­ÞŠŸþéŸæ…ùèþh¾æk¾†ÿ?õS?Å[¿õ[ó¼ôK¿4ó7ÿױcÇøë¿þküàó‚üõ_ÿ5/ó2/Ä×z­×â·û·ya$ñå³>ë³øìÏþl^˜×~í׿w~çwøðWõW¼ôK¿4/Èîî.~ðƒ¹téÿ^zЃøë¿þkŽ?Î òÓ?ýÓ¼ÍÛ¼ ÿÞë½Þ‹ïþîïæ…ùèþh¾æk¾†ÿ?õS?Å[¿õ[ó‚üüÏÿú£?šä·û·y×yþ£üÕ_ý/ýÒ/Í rë­·òÒ/ýÒ\ºt‰¯—z©—â¯ÿú¯ya~ú§š·y›·á?ÂG}ÔGñÕ_ýÕ¼0oýÖoÍÏüÌÏðá§~ê§xë·~k^ÝÝ]^ú¥_šg<ãü{;vŒ[o½•ãÇó‚¼Ì˼ ý×Í„×z­×â·û·ya>û³?›ÏùœÏá?ÂW}ÕWñÑýѼ0/ýÒ/ÍßüÍßðá¯þê¯xé—~i^[o½•—~é—æÒ¥Kü{½ÔK½ý×Í óÓ?ýÓ¼ÍÛ¼ ÿ>ê£>Нþê¯æ…‘Ä”ßú­ßâµ_ûµyAvwwyé—~ižñŒgðü|Ög}ŸýÙŸÍUW]uÕÿPȶ¹êª«®úJÿ^zЃøèþh>ú£?šÉ­·ÞÊGôGó3?ó3ü[½ÔK½_ýÕ_Ík¿ökó/ùíßþm>ú£?š¿ù›¿áßê­Þê­øê¯þjüàó/ùê¯þj¾ú«¿šg<ãü[;vŒþèæ³?û³ù—ìîîòÑýÑüôOÿ4—.]âßâ¥^ê¥øìÏþlÞú­ßšæ³?û³ùœÏùþ½Þê­ÞŠÏþìÏæ¥_ú¥ù—üôOÿ4ýÑÍ3žñ þ-Ž;Æ{¿÷{óÙŸýÙ?~œfww—ÏþìÏæ»¿û»¹téÿzЃøèþh>ú£?šÉ­·ÞÊGôGó3?ó3ü[½ÔK½_ýÕ_Ík¿ökó/ùíßþm>ú£?š¿ù›¿áßê½Þë½øê¯þjŽ?Î #‰¯=èA¼÷{¿7ŸýÙŸÍ¿dww—þèæ§ú§¹téÿ/õR/ÅgögóÖoýÖüKþú¯ÿšÏþìÏæg~ægø·z«·z+>û³?›—~é—æ_òÕ_ýÕ|õW5ÏxÆ3ø·8vìïýÞïÍgögsüøq^˜ÝÝ]>û³?›ïþîïæÒ¥Kü[<èAâ£?ú£ùèþh^˜ßþíßæu^çuø÷z©—z)¾ú«¿š×~í׿_òÓ?ýÓ|ög6ó7ÿűcÇxë·~k¾ú«¿šãÇó/ùìÏþl¾û»¿›g<ãü[<èAâ½ßû½ùìÏþlþ%»»»|ôG4?ýÓ?Í¥K—ø·x©—z)>û³?›·~ë·æ_ò×ý×¼÷{¿7ó7ÿÕ[½Õ[ñÕ_ýÕ<øÁæ…yí×~m~çw~‡cÇŽñÞïýÞ|ög6Çç…ÙÝÝå³?û³ùîïþn.]ºÄ¿Åƒô >ú£?šþèæ_rë­·òÑýÑüÌÏü ÿV¯õZ¯ÅWõWóÒ/ýÒüK~ú§šÏþìÏæoþæoø·8vìoýÖoÍWõWsüøqþ%ŸýÙŸÍw÷wóŒg<ƒ‹=èA¼÷{¿7ŸýÙŸÍ óÛ¿ýÛ¼Îë¼ÿ^/õR/ÅWõWóÚ¯ýÚüK~û·›þèæoþæoø·z«·z+¾ú«¿š?øÁüK¾ú«¿š¯þê¯æÏxÿÇŽã½ßû½ùìÏþlŽ?Î ³»»ËgögóÝßýÝ\ºt‰‹=èA|ög6ïýÞïÍ¿ä¯ÿú¯ùìÏþl~æg~†«·z«·â³?û³yé—~i^˜×~í׿w~çwø÷8vìoýÖoÍWõWsüøqþ%ŸýÙŸÍw÷wóŒg<ƒ‹=èA¼÷{¿7ŸýÙŸÍ¿dww—÷~ï÷æg~ægø·z©—z)¾ú«¿š×~í׿_òÛ¿ýÛ|ôG4ó7ÿÕ[½Õ[ñÕ_ýÕ<øÁæ_òÕ_ýÕ|õW5ÏxÆ3ø·8vìïýÞïÍgögsüøq^Iü{=èAâ«¿ú«yë·~kþ%ý×Ígögó3?ó3ÜïØ±c|ôG4ŸýÙŸÍUW]uÕÿ`ȶ¹êª«®úJÏm®ºê~ŸýÙŸÍç|ÎçðÜ>ë³>‹ÏþìÏæª«î'‰çÇ6W]u¿ßþíßæu^çuxn¯õZ¯ÅoÿöosÕU÷{í×~m~çw~‡çö[¿õ[¼ök¿6W]ðÛ¿ýÛ¼Îë¼Ïíµ^ëµøíßþm®ºê~¯ýÚ¯ÍïüÎïðÜ~ë·~‹×~í׿ª«î'‰çÇ6W]uÕUW=dÛ\uÕUWý%‰çÇ6W]u¿ÏþìÏæs>çsxnŸõYŸÅgögsÕU÷“Äóc›«®ºßoÿöoó:¯ó:<·×z­×â·û·¹êªû½ök¿6¿ó;¿Ãsû­ßú-^ûµ_›«®øíßþm^çu^‡çöZ¯õZüöoÿ6W]u¿×~í׿w~çwxn¿õ[¿Åk¿öksÕU÷“Äóc›«®ºêª«ž²m®ºêª«þ‡’Äóc›«®ºßgögó9Ÿó9<·Ïú¬Ïâ³?û³¹êªûIâù±ÍUWÝï·û·y×yžÛk½ÖkñÛ¿ýÛ\uÕý^ûµ_›ßùßá¹ýÖoý¯ýÚ¯ÍUWüöoÿ6¯ó:¯Ãs{­×z-~û·›«®ºßk¿ökó;¿ó;<·ßú­ßâµ_ûµ¹êªûIâù±ÍUW]uÕUÏÙ6W]uÕUÿCIâù±ÍUWÝï³?û³ùœÏùžÛg}ÖgñÙŸýÙ\uÕý$ñüØæª«î÷Û¿ýÛ¼Îë¼Ïíµ^ëµøíßþm®ºê~¯ýÚ¯ÍïüÎïðÜ~ë·~‹×~í׿ª«~û·›×y×á¹½Ök½¿ýÛ¿ÍUWÝïµ_ûµùßùžÛoýÖoñÚ¯ýÚ\uÕý$ñüØæª«®ºêªç€l›«®ºêªÿ¡$ñüØæª«î÷ÙŸýÙ|Îç|Ïí³>ë³øìÏþl®ºê~’x~lsÕU÷ûíßþm^çu^‡çöZ¯õZüöoÿ6W]u¿×~í׿w~çwxn¿õ[¿Åk¿öksÕU¿ýÛ¿Íë¼ÎëðÜ^ëµ^‹ßþíßæª«î÷Ú¯ýÚüÎïüÏí·~ë·xí×~m®ºê~’x~lsÕUW]uÕs@¶ÍUW]uÕÿP’x~lsÕU÷ûìÏþl>çs>‡çöYŸõY|ög6W]u?Iû³?›«®ºŸ$žÛ\uÕý~û·›×y×á¹½Ök½¿ýÛ¿ÍUWÝïµ_ûµùßùžÛoýÖoñÚ¯ýÚ\uÀoÿöoó:¯ó:<·×z­×â·û·¹êªû½ök¿6¿ó;¿Ãsû­ßú-^ûµ_›«®ºŸ$žÛ\uÕUW]õmsÕUW]õ?ÔñãǹtétìØ1vww¹êªûýôOÿ4oó6oÃsû¬Ïú,>û³?›«®ºßK¿ôKó7ó7<7Û\uÕý~û·›×y×á¹}ÔG}_ýÕ_ÍUWÝï­ßú­ù™ŸùžÛÓŸþtüàsÕU·Þz+yÈCxnoõVoÅOÿôOsÕU÷ûèþh¾æk¾†çö[¿õ[¼ök¿6W]u?I<·×z­×â·û·¹êª«®ºê9 Ûæª«®ºê¨þèæk¾ækx Ïú¬Ïâ³?û³¹êªûíîîòà?˜K—.q¿cÇŽñ×ý×<øÁ檫î÷Õ_ýÕ|ÌÇ| ôVoõVüôOÿ4W]u¿ÝÝ]^ú¥_šg<ã<Ð_ýÕ_ñÒ/ýÒ\uÕý~ú§š·y›·á^ëµ^‹ßþíßæª«èµ_ûµùßùè§~ê§xë·~k®ºê~¿ýÛ¿Íë¼Îëð@/õR/Å_ÿõ_sÕUôÖoýÖüÌÏü ôU_õU|ôG4W]uÕUW=dÛ\uÕUWýöÑýÑ|÷w7ýÑÍgögsÕUÏí¯ÿú¯ùìÏþl~æg~†—z©—â«¿ú«yí×~m®ºê¹}ög6ßýÝßÍîî.ïýÞïÍgögsüøq®ºên½õV>ú£?šŸù™Ÿá¥^ê¥øìÏþlÞú­ßš«®zn_ýÕ_ÍWõW³»»Ë[¿õ[óÕ_ýÕ?~œ«®z ÝÝ]>ú£?šŸþéŸæøñã|ôG4ýÑÍUW=·ŸþéŸæ³?û³ù›¿ùÞê­ÞНþê¯æÁ~0W]õ@»»»|ög6ßýÝßÍñãÇùèþh>ú£?š«®ºêª«ž²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«þøžïù¾û»¿›ßþíßàøñã¼ök¿6õQÅk¿öksÕUÏÏîî.yÈCØÝÝå·~ë·xí×~m®ºjww—ïùžïá§ú§ùíßþmî÷Ò/ýÒ¼ök¿6õQŃü`®ºê·û·ùžïù~û·›[o½€×~í׿­ßú­y«·z+üàsÕU/ÈGôGó5_ó5Øæªÿß~û·›ßùßáEñ =ˆ÷~ï÷æªÿ¿vwwùžïù~ú§šßþíßàøñã¼ök¿6oýÖoÍ{½×{qÕU¿ýÛ¿ÍïüÎïðoñ^ïõ^<øÁ檫®ºêÿ1dÛ\uÕUWý7ÚÝÝåmÞæmøíßþm^·~ë·æ»¾ë»8~ü8W]õ@oó6oÃOÿôOð[¿õ[¼ök¿6WýÿöÓ?ýÓ¼Ïû¼»»»¼0ŸýÙŸÍg}ÖgqÕÿ_ó1ÃWõWó‚?~œïú®ïâ­ßú­¹êªçöÛ¿ýÛ¼Îë¼÷³ÍUÿ¿½ök¿6¿ó;¿Ã‹âµ^ëµøíßþm®úÿé¯ÿú¯y×yvwwyA^ûµ_›Ÿú©Ÿâøñã\õÿÛgögó9Ÿó9ü[üÖoý¯ýÚ¯ÍUW]uÕÿcȶ¹êª«®úoô6oó6üôOÿ4zЃxï÷~o^ú¥_šßþíßæ»¿û»¹téïýÞïÍw}×wqÕU÷{Ÿ÷y¾û»¿›ûýÖoý¯ýÚ¯ÍUÿýöoÿ6¯ó:¯Ãý^ê¥^Š·~ë·æ¥_ú¥ùë¿þkþú¯ÿšŸù™Ÿá~ŸõYŸÅgögsÕÿ?ýÑÍ×|Í×p¿ú¨â¥_ú¥ÙÝÝå¯ÿú¯ùžïùî÷S?õS¼õ[¿5W]u¿ÝÝ]^æe^†[o½•ûÙæªÿßNœ8Áîî./Š×z­×â·û·¹êÿŸ¿þë¿æu^çuØÝÝàµ^ëµxë·~kŽ?Î_ÿõ_óÝßýÝ\ºt €·~ë·æ§~꧸êÿ·ÏþìÏæs>çsø·ø«¿ú+^ú¥_š«®ºêªÿÇmsÕUW]õß仿û»yŸ÷y^ëµ^‹ŸþéŸæøñãÜoww—×~í׿oþæoø­ßú-^ûµ_›«þÛÝÝå}Þç}øéŸþiè·~ë·xí×~m®úÿë!y·Þz+õQÅWõWóܾû»¿›÷yŸ÷á~OúÓyðƒÌUÿüöoÿ6¯ó:¯À±cÇøíßþm^ú¥_šúéŸþiÞæmÞ€?øÁ<ýéO窫î÷6oó6üôOÿ4d›«þÿºõÖ[yÈCÀ{½×{ñÝßýÝ\uÕsÛÝÝåe^æe¸õÖ[ø®ïú.Þû½ß›úë¿þk^ûµ_›K—.ð[¿õ[¼ök¿6W]õ¢xŸ÷y¾û»¿€÷z¯÷⻿û»¹êª«®úÙ6W]uÕUÿMò‡pë­·rìØ1n½õVŽ?Îs»õÖ[yÈCÀK¿ôKóWõW\õÿ×oÿöoó>ïó>Üzë­<·ßú­ßâµ_ûµ¹êÿ§ŸþéŸæmÞæmx­×z-~û·›ä£?ú£ùš¯ù>ê£>Нþê¯æªÿ?Þú­ßšŸù™Ÿ໾ë»xï÷~ožŸ÷~ï÷æ{¾ç{ø©Ÿú)Þú­ßš«®úîïþnÞç}Þ€cÇŽqéÒ%lsÕÿ_¿ýÛ¿Íë¼ÎëðU_õU|ôG4W]õÜ>û³?›ÏùœÏà³>ë³øìÏþlžŸïþîïæ}Þç}x¯÷z/¾û»¿›«®ú—|÷w7ïó>ïÀK½ÔKñ×ý×\uÕUW]²m®ºêª«þüôOÿ4oó6oÀ{½×{ñÝßýݼ oýÖoÍÏüÌÏðô§??øÁ\õÿËîî.oó6oÃoÿöos¿×z­×bww—¿ù›¿à·~ë·xí×~m®úÿé³?û³ùœÏù¾ë»¾‹÷~ï÷æùíßþm^çu^€×z­×â·û·¹êÿ'N°»» €m^¯þê¯æc>æcø¬Ïú,>û³?›«þ»õÖ[y™—yvwwù¨ú(þú¯ÿšßùßÀ6WýÿõÙŸýÙ|Îç|¿õ[¿Åk¿öksÕUÏí!y·Þz+ÇŽãÖ[oåøñãú£?šÏþìÏæµ_ûµùßù~ë·~‹×~í׿ªÿŸ>ú£?š¿þë¿fww—¯þê¯æµ_ûµyAvww9qâ¯õZ¯ÅoÿöosÕÿ/¿ýÛ¿ Àk¿ökó‚|ög6Ÿó9ŸÀg}ÖgñÙŸýÙ\õÿÛë¼ÎëðÛ¿ýÛ<èAâ¯ÿú¯yë·~k~çw~Û\õÿ×[¿õ[ó3?ó3Øæª«žÛ_ÿõ_ó2/ó2|ÔG}_ýÕ_ÍUWýGy×y~û·€ïú®ïâ½ßû½¹êª«®ºê2dÛ\uÕUWý7xí×~m~çw~€¿ú«¿â¥_ú¥yA~û·›×y×à£>ê£øê¯þj®úÿå·û·y×yÞë½Þ‹ÏþìÏæÁ~0¯ýÚ¯ÍïüÎïð[¿õ[¼ök¿6W]õ/ùíßþm^çu^€×z­×â·û·¹êªçö:¯ó:üöoÿ6õWÅK¿ôKsÕÿ_ŸýÙŸÍç|ÎçðWõW¼ôK¿4¯ýÚ¯ÍïüÎï`›«þÿzÈC­·ÞÊK½ÔKñ×ý×ÜïÖ[oàÁ~0WýÿöÝßýݼÏû¼?õS?Å[¿õ[sÕUÿ¾û»¿›÷yŸ÷àµ^ëµøíßþm®ºêª«®zdÛ\uÕUWý7x™—yþú¯ÿÛ¼0·Þz+yÈCx­×z-~û·›«þ¹õÖ[xðƒÌ½ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ\uÕ¿äu^çuøíßþm¾ê«¾Šþèæª«èk¾ækøèþh^ëµ^‹ßþíßæªÿ¿þú¯ÿš—y™—à³>ë³øìÏþl^ûµ_›ßùßÀ6Wýÿ% €÷z¯÷â½ßû½ùš¯ù~ú§šzë·~k>ë³>‹—~é—æªÿ>û³?›ÏùœÏà¯þê¯xé—~in½õV¾ç{¾‡ßþíßæ~/ýÒ/ÍG}ÔGñà?˜«®ú—ìîîò‡<„ÝÝ]þê¯þŠ—~é—æª«®ºêªgA¶ÍUW]uÕIÜÏ6ÿI¼Ök½¿ýÛ¿ÍUW¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ\uÕ óÛ¿ýÛ¼Îë¼ÇŽãÖ[oåøñã\õÿÛ­·ÞÊßüÍßð×ý×|÷w7·Þz+/õR/Åoÿöosüøq®úÿiww——y™—áÖ[oå¥^ê¥øë¿þkî÷Ú¯ýÚüÎïü¶¹êÿ§ßþíßæu^çu8~ü8»»»¼0ßõ]ßÅ{¿÷{sÕÿ/¯ýÚ¯ÍïüÎï`›¯ùš¯á£?ú£yA>û³?›Ïú¬Ï⪫^˜ÏþìÏæs>çsx¯÷z/¾û»¿›«®ºêª«ž²m®ºêª«þHâ~¶ù—Hà¥_ú¥ù«¿ú+®º àµ_ûµùßù~ë·~‹×~í׿ª«^¿þë¿æu^çuØÝÝà«¾ê«øèþh®ºê£?ú£ùš¯ùè¥_ú¥ù­ßú-Ž?ÎUÿ}ôG4_ó5_ñcÇøíßþm^ú¥_šû½ök¿6¿ó;¿€m®úÿé«¿ú«ù˜ùîwìØ1Þû½ß›—~é—æÁ~0¿ýÛ¿Íw÷wóŒg<ƒû}×w}ïýÞïÍUÿ¼ök¿6¿ó;¿ÀW}ÕWñ1ó1;vŒ—~é—àÖ[oåÏx÷û¨ú(¾ú«¿š«®z~vwwyÈCÂîî.OúÓyðƒÌUW]uÕUÏÙ6W]uÕUÿ $püøq.^¼È¿D÷³ÍUW¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ\uÕóó×ý×¼Îë¼»»»¼Õ[½?ýÓ?ÍUW¼ök¿6ý× À¥K—¸ßñãÇùª¯ú*Þû½ß›«þÿùéŸþiÞæmÞ€¯úª¯â£?ú£y ×~í׿w~çw°ÍUÿ?½÷{¿7ßó=ßÀK½ÔKñÛ¿ýÛ?~œçöÞïýÞ|Ï÷|ÇçéO:Ççªÿ^ûµ_›ßùßá¾ê«¾Šþèæ¾ú«¿šù˜á~¿õ[¿Åk¿öksÕUÏí«¿ú«ù˜ùÞë½Þ‹ïþîïæª«®ºêªçl›«®ºêªÿ’¸Ÿmþ%’¸Ÿm®º àµ_ûµùßù~ë·~‹×~í׿ª«žÛ_ÿõ_ó:¯ó:ìîîðR/õRüöoÿ6Ç窫žÛîî._ýÕ_Íç|Îçp¿ïú®ïâ½ßû½¹êÿÝÝ]ò‡°»»Ëk½ÖkñÛ¿ýÛ<·×~í׿w~çw°ÍUÿ?ýõ_ÿ5·Þz+ý×ÍGôGsüøq^×~í׿w~çwøª¯ú*>ú£?š«þxí×~m~çw~‡û}×w}ïýÞïÍóóÕ_ýÕ|ÌÇ| ¯ýÚ¯ÍoýÖoqÕUÏí!y·Þz+OúÓyðƒÌUW]uÕUÏÙ6W]uÕUÿ $q?ÛüK$ðR/õRüõ_ÿ5W]ðÚ¯ýÚüÎïü¿õ[¿Åk¿öksÕUôÛ¿ýÛ¼ÍÛ¼ »»»¼Ök½?ýÓ?Íñãǹêªæ§ú§y›·yŽ?ÎÓŸþtŽ?ÎUÿ?¼ÍÛ¼ ?ýÓ?ͱcÇøë¿þküàóÜ^ûµ_›ßùßÀ6W]õ/ùéŸþiÞæmÞ€×z­×â·û·¹êÿ‡×~í׿w~çwx©—z)þú¯ÿšæøñã\ºt €‹/rüøq®ºê~?ýÓ?ÍÛ¼ÍÛðR/õRüõ_ÿ5W]uÕUW=_ȶ¹êª«®úopüøq.]º€mþ%’x­×z-~û·›«®xí×~m~çw~€ßú­ßâµ_ûµ¹êªû}÷w7ïó>ïÃýÞë½Þ‹ïþîïæª«^T¯ýÚ¯ÍïüÎïðU_õU|ôG4Wýß÷Õ_ýÕ|ÌÇ| ?õS?Å[¿õ[óü¼ök¿6¿ó;¿€m®ºê_²»»Ë‰'8~ü8/^äªÿ^ûµ_›ßùßà³>ë³øìÏþl^˜×~í׿w~çwø­ßú-^ûµ_›«®ºß{¿÷{ó=ßó=|ÕW}ýÑÍUW]uÕUϲm®ºêª«þ¼ök¿6¿ó;¿ÀÅ‹9~ü8/Èoÿöoó:¯ó:¼Õ[½?ýÓ?ÍUW¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ\uÀÇ|ÌÇðÕ_ýÕÜï³>ë³øìÏþl®ºê_ã³?û³ùœÏù>ë³>‹ÏþìÏæªÿû^ûµ_›ßùßáßê·~ë·xí×~m®ºêù‘ÄýlsÕÿïýÞïÍ÷|Ï÷ðYŸõY|ög6/Ìgögó9Ÿó9üÖoý¯ýÚ¯ÍUWÝïĉìîîðô§??øÁ\uÕUW]õ|!Ûæª«®ºê¿ÁGôGó5_ó5üÖoý¯ýÚ¯Í òÓ?ýÓ¼ÍÛ¼ ŸõYŸÅgögsÕU¯ýÚ¯ÍïüÎïð[¿õ[¼ök¿6W]õ>ïó>|÷w7÷û®ïú.Þû½ß›«®øë¿þk.]º„m^ûµ_›æ§ú§y›·yÞë½Þ‹ïþîïæªÿû^ûµ_›ßùßáßê·~ë·xí×~m®úÿã¯ÿú¯xé—~i^˜ÝÝ]Nœ8ÀK½ÔKñ×ý×\õÿÃWõWó1ó1|Ög}ŸýÙŸÍ óÞïýÞ|Ï÷|¿õ[¿Åk¿öksÕU¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6W]uÕUW½@ȶ¹êª«®úoðÝßýݼÏû¼ŸõYŸÅgögó‚|ôG4_ó5_ÀOýÔOñÖoýÖ\uÀk¿ökó;¿ó;üÖoý¯ýÚ¯ÍUÿ¿½Ïû¼ßýÝß À±cÇøíßþm^ú¥_š«®øíßþm^çu^€·z«·â§ú§ya>û³?›ÏùœÏà³>ë³øìÏþl®ú¿ï»¿û»¹õÖ[ù—|÷w7ÏxÆ3ø¬Ïú,î÷ÞïýÞ<øÁæªÿûþú¯ÿš—y™—àøñã\¼x‘æ§ú§y›·yÞë½Þ‹ïþîïæªÿþú¯ÿš—y™—à­Þê­øéŸþi^˜—y™—á¯ÿú¯°ÍUWÝï³?û³ùœÏù>ë³>‹ÏþìÏæª«®ºêªÙ6W]uÕUÿ vww9qâ~ðƒyúÓŸÎó³»»ËCòvww9vì»»»\uÕý^ûµ_›ßùßà·~ë·xí×~m®úÿë«¿ú«ù˜ùŽ;ÆoÿöoóÒ/ýÒ\uÕývww9qâ÷»xñ"ÇçyÈC­·Þ ÀOýÔOñÖoýÖ\uÕý^ûµ_›ßùßÀ6Wýÿtüøq.]ºÀoýÖoñÚ¯ýÚ¼ ¯ó:¯Ãoÿöoð]ßõ]¼÷{¿7Wýÿñà?˜g<ã<ýéOçÁ~0ÏÏ­·ÞÊCò^ëµ^‹ßþíßæª«î÷ÖoýÖüÌÏü ¿õ[¿Åk¿öksÕUW]uÕ „l›«®ºêªÿ&ïýÞïÍ÷|Ï÷ðU_õU|ôG4Ïí£?ú£ùš¯ù>ë³>‹ÏþìÏæª«î÷Ú¯ýÚüÎïü¿õ[¿Åk¿öksÕÿOý×Í˼ÌËp¿¿ú«¿â¥_ú¥¹êªçöÞïýÞ|Ï÷|oýÖoÍOýÔOñü¼Ïû¼ßýÝß ÀK½ÔKñ×ý×\uÕ½ök¿6¿ó;¿€m®úÿé³?û³ùœÏù^ú¥_šßú­ßâøñã<·¯þê¯æc>æcxЃÄ­·ÞÊUÿ¿|õW5ó1ÀK¿ôKó[¿õ[?~œçö:¯ó:üöoÿ6ßõ]ßÅ{¿÷{sÕU÷;qâ»»»Øæª«®ºêª Ù6W]uÕUÿMn½õV^ú¥_šK—.ðÙŸýÙ|ÔG}ÇçÖ[oås>çsøîïþnô ñ×ý×?~œ«®ºßk¿ökó;¿ó;üÖoý¯ýÚ¯ÍUÿ?½÷{¿7ßó=ßÀñãÇyé—~i^/õR/ÅWõWsÕÿ·Þz+/ýÒ/Í¥K—xí×~m¾ê«¾Š—~é—à¯ÿú¯ùœÏù~ú§€cÇŽñÛ¿ýÛ¼ôK¿4W]õ@¯ýÚ¯ÍïüÎï`›«þÚÝÝåÁ~0—.]à¥_ú¥ùìÏþlÞê­Þ €[o½•ÏùœÏỿû»8vì¿ýÛ¿ÍK¿ôKsÕÿ?/ýÒ/ÍßüÍßðÒ/ýÒ|ÕW}¯ýÚ¯ À_ÿõ_ó>ïó>üõ_ÿ5¯õZ¯ÅoÿöosÕU$ €=èAÜzë­\uÕUW]õB!Ûæª«®ºê¿Ñw÷wó>ïó>¼0ÇŽã·û·yé—~i®ºê^ûµ_›ßùßà·~ë·xí×~m®úÿgww—'NðoñZ¯õZüöoÿ6Wýÿò×ý×¼ök¿6—.]â…9vìßýÝßÍ[¿õ[sÕUÏíµ_ûµùßùlsÕÿ_ý×Ík¿ökséÒ%^˜cÇŽñÕ_ýÕ¼÷{¿7Wýÿ´»»Ëk¿ökó7ó7¼0/õR/Åoÿöosüøq®ºê~ý×Í˼ÌËðZ¯õZüöoÿ6W]uÕUW½Pȶ¹êª«®úoö×ý×|ôG4¿ó;¿Ãs{­×z-¾û»¿›?øÁ\uÕs{í×~m~çw~€ßú­ßâµ_ûµ¹êÿŸßþíßæu^çuø·x­×z-~û·›«þÿÙÝÝå£?ú£ùžïùžŸ÷z¯÷â³?û³yðƒÌUW=?¯ýÚ¯ÍïüÎï`›«þÛÝÝå£?ú£ùžïùžŸ·z«·â«¿ú«yðƒÌUW}ög6_ýÕ_Í¥K—x cÇŽñÑýÑ|ôG4Ç窫è·û·y×yÞê­ÞŠŸþéŸæª«®ºêª Ù6W]uÕUÿCÜzë­Üzë­Üzë­<øÁæÁ~0~ðƒ¹êª«®ºêªÿ,»»»üõ_ÿ5·Þz+Ççøñã¼ök¿6W]uÕUÿV¿ýÛ¿ Àîî.Çç¥_ú¥9~ü8W]õÜþú¯ÿšÝÝ]vww9~ü8/ýÒ/Íñãǹꪫ®ºêª«þC Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«žå»¿û»yÆ3žÀ{½×{ñà?˜ÿJ_ýÕ_Í¥K—ø¨ú(Ž?ÎUW]uÕUW]uÕUW]õ¯‚l›«®ºêª«®ºêª«®ºê9¼Îë¼ÿU¾ê«¾Š—~é—æ¯ÿú¯ù˜ùî÷R/õR|õW5Wý×úéŸþiÞæmÞ€—z©—â¯ÿú¯ù¯öÕ_ýÕ|ÌÇ| oýÖoÍOýÔOqÕUW]uÕUW]uÕUWý« Ûæª«®ºêª«®ºêª«®z’ø¯ò[¿õ[¼ök¿6¿ýÛ¿Íë¼Îëp¿×z­×â·û·¹ê¿Îîî.yÈCØÝÝà·~ë·xí×~mþ;<øÁæÏx_õU_ÅGôGsÕUW]uÕUW]uÕUW½ÈmsÕUW]uÕUW]uÕUW=IüWù­ßú-^ûµ_›ßþíßæu^çu¸ßk½ÖkñÛ¿ýÛ\õ_çu^çuøíßþm^ëµ^‹ßþíßæ¿ËOÿôOó6oó6?~œ¿ú«¿âÁ~0W]uÕUW]uÕUW]uÕ‹Ù6W]uÕUW]uÕUW]uÕsÄ•ßú­ßâµ_ûµùíßþm^çu^‡û½Ök½¿ýÛ¿ÍUÿ5~ú§š·y›·á~OúÓyðƒÌ§×~í׿w~çwxë·~k~ê§~Š«®ºêª«®ºêª«®ºêE‚l›«®ºêª«®ºêª«®ºê9üöoÿ6/ªþèæoþæox ßú­ßâEõÒ/ýÒ?~œßþíßæu^çu¸ßk½ÖkñÛ¿ýÛ\õŸoww——y™—áÖ[oà½Þë½øîïþnþ»ýöoÿ6¯ó:¯Ãý~ë·~‹×~í׿ª«®ºêª«®ºêª«®ú!Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²ÍUÿ{|ög6Ÿó9ŸÃýžþô§óà?˜ÿ ^ûµ_›ßùßàÁ~0Oúӹꪫ®ºêª«®ºêª«þEȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßálsÕÿ»»»<ä!aww€÷z¯÷⻿û»ùŸâ·û·y×yî÷]ßõ]¼÷{¿7W]uÕUW]uÕUW]uÕ …l›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6WýïðÙŸýÙ|Îç|÷û­ßú-^ûµ_›ÿIüàóŒg<€?øÁ<ýéO窫®ºêª«®ºêª«®z¡msÕUW]uÕUW]uÕUWý›½ök¿6¿ó;¿ÃÙæªÿùvwwyÈCÂîî.zЃ¸õÖ[ùŸæ«¿ú«ù˜ùî÷]ßõ]¼÷{¿7W]uÕUW]uÕUW]uÕ „l›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6ÿZ·Þz+ßó=ßÃýô ñÞïýÞæcØÝÝå…ù®ïú.Þû½ß›úœÏù>û³?›æøñã|×w}oýÖoÍ¿Æ÷|Ï÷ðÙŸýÙÜzë­üKüàó]ßõ]¼ök¿6ÿÙ^æe^†¿þë¿æ~OúÓyðƒÌ‹êk¾ækøìÏþlvwwyQ¼÷{¿7ŸõYŸÅƒü`þµ^ú¥_š¿ù›¿á~õWÅK¿ôKsÕUW]uÕUW]uÕUW=_ȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùîg›÷yŸ÷ỿû»yQ}ÔG}_ýÕ_ Àë¼ÎëðÛ¿ýÛ¼¨~ê§~Š·~ë·æ_²»»Ëû¼ÏûðÓ?ýÓük½÷{¿7ßõ]ßÅ–¿þë¿æe^æe¸ßƒô n½õV^Tïó>ïÃw÷wó¯uüøq~ë·~‹—~é—æ_ã³?û³ùœÏùî÷^ïõ^|÷w7W]uÕUW]uÕUW]uÕó…l›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6ÿZ¿ýÛ¿Íë¼Îëp¿×z­×â·û·y~^ûµ_›ßùßá~_õU_ÅÇ|ÌÇð@zЃxðƒ ÀïüÎïðü<ýéOçs>çsøîïþnèµ^ëµØÝÝåoþæoxnÇçéO:ÇçÙÝÝåu^çuøë¿þkžÛK½ÔKñÚ¯ýÚ?~œ[o½•¿þë¿æoþæoxnïýÞïÍw}×wñŸá£?ú£ùš¯ùî÷^ïõ^|÷w7/ŠÏþìÏæs>çsxn¯õZ¯Åk¿ök°»»Ë_ÿõ_ó;¿ó;<·ãÇóô§?ãÇó¢úë¿þk^æe^†û=øÁæéO:W]uÕUW]uÕUW]uÕó…l›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6ÿZ¿ýÛ¿Íë¼Îëp¿×z­×â·û·y~^ûµ_›ßùßáùy¯÷z/>û³?›?øÁÜoww—þèæ{¾ç{x ?øÁÜzë­;vŒ¯þê¯æ½ßû½y ¿þë¿æ½ßû½ù›¿ù軾ë»xï÷~o^÷yŸ÷ỿû»y ×z­×â«¿ú«yé—~ižÛ_ÿõ_óÑýÑüÎïüôU_õU|ôG4ÿÑ^æe^†¿þë¿æ~?õS?Å[¿õ[ó/¹õÖ[yÈC½×{½_ýÕ_ÍñãÇyn·Þz+ýÑÍÏüÌÏð@õQÅWõWó¯!‰ú«¿ú+^ú¥_š«®ºêª«®ºêª«®ºêy Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çwxnßõ]ßÅ{¿÷{ó‚¼ök¿6¿ó;¿Ãs;vìý×̓ü`žŸÝÝ]üàséÒ%î÷VoõVüôOÿ4ÏÏOÿôOó6oó6<Ð{½×{ñÝßýÝüKÞû½ß›ïùžïá~Çç¯þê¯xðƒÌ”ÝÝ]Nœ8ÁýÕ_ý/ýÒ/Ϳ䫿ú«ù˜ùî÷Z¯õZüöoÿ6ÿ’÷~ï÷æ{¾ç{¸ßñãǹxñ"ÿ¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡z¯÷z/¾û»¿›æ§ú§y›·yžÛoýÖoñÚ¯ýÚ¼0ýÑÍ×|Í×p¿?øÁ<ýéOçùy™—yþú¯ÿšû½Ök½¿ýÛ¿Í‹bww——~é—æÏx÷û¬Ïú,>û³?›ÿ(¿ýÛ¿Íë¼Îëð@¶yQ¼÷{¿7ßó=ßÃý>ë³>‹ÏþìÏæ_rë­·ò‡<„ú«¿ú+^ú¥_šÕ{¿÷{ó=ßó=Üï­Þê­øéŸþi®ºêª«®ºêª«®ºêªçl›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6ÿZ¿ýÛ¿Íë¼Îëp¿×z­×â·û·y~^ûµ_›ßùßážþô§óà?˜fww—'Nð@/õR/Å_ÿõ_ó/ùéŸþiÞæmÞ†²Ísûíßþm^çu^‡ú­ßú-^ûµ_›Õw÷wó>ïó>Üïøñã\¼x‘ÿ(ŸýÙŸÍç|Îçp¿—z©—â¯ÿú¯yQ¼ök¿6¿ó;¿Ãý>ê£>Нþê¯æEñÙŸýÙ?~œ—~é—æøñã¼ôK¿4ÿ_ýÕ_ÍÇ|ÌÇp¿?øÁ<ýéO窫®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷R/õRüõ_ÿ5/ I<ÐG}ÔGñÕ_ýÕüK~û·›×y×álóÜ>û³?›ÏùœÏá~zЃ¸õÖ[ùרÝÝåĉ<ÐoýÖoñÚ¯ýÚüGxï÷~o¾ç{¾‡û½×{½ßýÝßÍ‹â³?û³ùœÏùîwüøq~ê§~Š×~í׿?Ûoÿöoó:¯ó:û³?›Åoÿöoó:¯ó:<··~ë·æ­ßú­y«·z+Ž?Άßþíßæu^çux ¿ú«¿â¥_ú¥¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍ‹Bô[¿õ[¼ök¿6ÿ’ßþíßæu^çux Û<7I<Ð{¿÷{óÞïýÞük}ôG4ý×ÍýÞë½Þ‹ïþîïæ?Â˼ÌËð×ý×Üï³>ë³øìÏþl^T¯ýÚ¯ÍïüÎïð‚¼ôK¿4oýÖoÍk¿ökóZ¯õZüG¹õÖ[yÈCÂýÖoý¯ýÚ¯ÍUW]uÕUW]uÕUW]õmsÕUW]uÕUW]uÕUWý›½ök¿6¿ó;¿ÃÙæ_ë·û·y×yî÷Z¯õZüöoÿ6ÏÏk¿ökó;¿ó;Üï³>ë³øìÏþl^’x ßú­ßâµ_ûµù—üöoÿ6¯ó:¯ÃÙæ¹Iâ?Ãk½ÖkñÛ¿ýÛüGÄ}Ög}ŸýÙŸÍ‹jww—?øÁ\ºt‰ÉñãÇyí×~mÞú­ßš·z«·âøñãü{Hâ~ë·~‹×~í׿ª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóÜ$ñŸáµ^ëµøíßþmþ#Hâ>ë³>‹ÏþìÏæ_cww—÷~ï÷æg~ægxQ?~œ·~ë·æ«¾ê«8~ü8ÿ’x ßú­ßâµ_ûµ¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍ‹Bô[¿õ[¼ök¿6ÿ’ßþíßæu^çux Û<Ð_ÿõ_ó2/ó2ügx­×z-~û·›ÿ’x ¯úª¯â£?ú£ù·¸õÖ[ùê¯þj~ú§šg<ã¼(Ž?ÎoýÖoñÒ/ýÒükIâ~ë·~‹×~í׿ª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóÜ$ñ@_õU_ÅK¿ôKóïuüøq^ú¥_šÿ’x Ïú¬Ïâ³?û³ù÷úë¿þk~û·›ŸþéŸæw~çwxaŽ?ÎoýÖoñÒ/ýÒükHâ~ë·~‹×~í׿ª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóÜ$ñ@ßõ]ßÅ{¿÷{ó?Ék¿ökó;¿ó;Üï³>ë³øìÏþlþ£ýöoÿ6?ýÓ?ÍOÿôOóŒg<ƒçö^ïõ^|÷w7/ªßþíßæu^çux ßú­ßâµ_ûµ¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍ‹Bô[¿õ[¼ök¿6ÿ’ßþíßæu^çux Û<·?øÁ<ãÏà~õQÅWõWó?Ék¿ökó;¿ó;Üï½Þë½øîïþnþ3ýöoÿ6oýÖoÍ¥K—x Û¼¨~û·›×y×áþê¯þŠ—~é—æª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóÜÞû½ß›ïùžïá~/ýÒ/Í_ýÕ_ñ¯õÝßýÝHâÁ~0zЃxðƒÌ”÷~ï÷æ{¾ç{¸ßk½ÖkñÛ¿ýÛ¼(vwwù›¿ùn½õVlóÞïýÞ¼¨¾û»¿›÷yŸ÷áþê¯þŠ—~é—æEñÛ¿ýÛ¼Îë¼d›«®ºêª«®ºêª«®ºêy Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼($ñ@¿õ[¿Åk¿ökó/ùíßþm^çu^‡²ÍsûîïþnÞç}Þ‡ú­ßú-^ûµ_›Õ­·ÞÊCòè«¾ê«øèþhþ#|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæ_òÓ?ýÓ¼ÍÛ¼ d›Õ_ÿõ_ó2/ó2<ÐoýÖoñÚ¯ýÚ¼(¾ú«¿šù˜á~zЃ¸õÖ[¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍ‹Bô[¿õ[¼ök¿6ÿ’ßþíßæu^çux Û<·ÝÝ]üàséÒ%î÷Ò/ýÒüÕ_ý/ª·y›·á§ú§y §?ýé<øÁæ?ÂOÿôOó6oó6û³?›…$è·~ë·xí×~mþ%¿ýÛ¿Íë¼Îëð@¶yAþú¯ÿš×~í×æÒ¥K<·—~é—æ­ßú­¹ß_ÿõ_óÛ¿ýÛìîîòܾ뻾‹÷~ï÷æ?ÚGôGó5_ó5Üï­Þê­øéŸþi^ýÑÍ×|Í×ðÜ^ú¥_š×~í׿øñãÜï¯ÿú¯ùíßþmvwwy ÷z¯÷⻿û»yQýôOÿ4oó6oÃýŽ;Æîî.W]uÕUW]uÕUW]uÕó…l›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6ÿZ¿ýÛ¿Íë¼Îëp¿×z­×â·û·y~^ûµ_›ßùßá~ŸõYŸÅgögó¢ÄýÖoý¯ýگͿä·û·y×yÈ6/Ì_ÿõ_óÞïýÞüÍßü ÿßõ]ßÅ{¿÷{óŸá¯ÿú¯y™—yîwüøq.^¼È‹ê½ßû½ùžïùþ-^ê¥^Šßþíßæøñ㼨>û³?›ÏùœÏá~õQÅWõWsÕUW]uÕUW]uÕUW=_ȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×áló/ÙÝÝå«¿ú«ùê¯þj.]ºÄ‹âµ^ëµøê¯þj^ú¥_šÿL~ðƒyÆ3žÁýþê¯þŠ—~é—æEõÙŸýÙ|õW5—.]âEqìØ1>û³?›þèæ_ëe^æeøë¿þkî÷WõW¼ôK¿4W]uÕUW]uÕUW]uÕó…l›«®ºêª«®ºêª«®ºêß컿û»¹õÖ[y ÏþìÏæ_ëÖ[o廿û»¹ßƒü`Þû½ß›ç绿û»¹õÖ[¹ßk¿ökóÚ¯ýÚ¼(>û³?›zï÷~oüàó/¹õÖ[ùîïþnè³?û³yQíîîòÓ?ýÓüôOÿ4ý×Í3žñ èµ^ëµxé—~iÞû½ß›—~é—æ¿ÂWõWó1ó1Üï£>ê£øê¯þjþ5vwwùéŸþi~ú§š[o½•¿ù›¿á^ê¥^Š—~é—æµ_ûµyë·~kŽ?οÖ_ÿõ_ó2/ó2ÜïAz·Þz+W]uÕUW]uÕUW]uÕ „l›«®ºêª«®ºêª«®ºêªÿ‡vwwyðƒÌ¥K—8~ü8/^äšþèæk¾æk¸ßw}×wñÞïýÞ\uÕUW]uÕUW]uÕU/²m®ºêª«®ºêª«®ºêª«þŸzï÷~o¾ç{¾‡û}×w}ïýÞïÍÿ$'Nœ`ww€cÇŽ±»»ËUW]uÕUW]uÕUW]õB!Ûæª«®ºêª«®ºêª«®ºêÿ©[o½•‡<ä!Üïµ_ûµù­ßú-þ§øîïþnÞç}Þ‡û}Ög}ŸýÙŸÍUW]uÕUW]uÕUW]õB!Ûæª«®ºêª«®ºêª«®ºêÿ±þèæk¾æk¸ßÓŸþtüàó?Áë¼ÎëðÛ¿ýÛ<èAâ¯ÿú¯9~ü8W]uÕUW]uÕUW]uÕ …l›«®ºêª«®ºêª«®ºêªÿÇvwwyðƒÌ¥K—x¯÷z/¾û»¿›ÿn¿ýÛ¿Íë¼Îëp¿ïú®ïâ½ßû½¹êª«®ºêª«®ºêª«þEȶ¹êª«®ºêª«®ºêª«®úî«¿ú«ù˜ùî÷ô§??øÁüwz×y~û·€—z©—â¯ÿú¯¹êª«®ºêª«®ºêª«^$ȶ¹êª«®ºêª«®ºêª«®ºŠ—~é—æoþæoxë·~k~ê§~Šÿ.¿ýÛ¿Íë¼Îëp¿ßú­ßâµ_ûµ¹êª«®ºêª«®ºêª«^$ȶ¹êª«®ºêª«®ºêª«®ºŠ¿þë¿æe^æe¸ßoýÖoñÚ¯ýÚüwxÈC­·Þ ÀG}ÔGñÕ_ýÕ\uÕUW]uÕUW]uÕU/2dÛ\uÕUW]uÕUW]uÕUW]uÙWõWó1ó1¼ök¿6¿õ[¿Åµïþîïæ}Þç}x©—z)þú¯ÿš«®ºêª«®ºêª«®ºê_Ù6W]uÕUW]uÕUW]uÕUW=ËWõW³»» ÀGôGsüøqþ+}÷w7·Þz+oýÖoÍK¿ôKsÕUW]uÕUW]uÕUWý« Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;øG‡§yõäX§BIEND®B`‚uv-0.9.17+ds1/assets/png/resolve-warm.png000066400000000000000000004403361520155276700201700ustar00rootroot00000000000000‰PNG  IHDR@è†{2ƒ@¥IDATxíà$I’$I‹ª™»GDDfffVUUUUwwwww÷ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌtwwwwWWUUUUffFFD„»›™ ÏLfWwuwwOÏÌÌÌÌL¢l›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«þG¹õÖ[yÆ3žÁ½ÔK½Çç_kww—¿ù›¿á¹=èAâÁ~0ÿ¿ó;¿Ã=èAâÁ~0Wý÷ûë¿þk.]ºÄ„=èA<øÁæªÿ·Þz+ÏxÆ3¸ß±cÇxé—~iþ7úë¿þk.]ºÄýô ñà?˜«®ºêª«®ºêª«®ú/„l›«®ºêª«®ºêª«þGùîïþnÞç}Þ‡ú­ßú-^ûµ_›­ïþîïæ}Þç}xnõQÅWõWó¯õ×ý×¼Ì˼ ô]ßõ]¼÷{¿7Wý÷{í×~m~çw~‡ÿ(Ççµ_ûµyë·~kÞë½Þ‹«þó|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæšŸþéŸæ­ßú­ya^ûµ_›ßùßá~ŸõYŸÅgögsÕUW]uÕUW]uÕUÿ…msÕUW]uÕUW]uÕÿ(»»»œ8q‚ú¬Ïú,>û³?›­·~ë·æg~ægxn~ðƒyúӟοÖWõWó1ó1<ÐÅ‹9~ü8Wý÷{í×~m~çw~‡ÿ /ýÒ/Íw}×wñÒ/ýÒ\õï³?û³ùœÏùî÷Z¯õZüöoÿ6ÿSÜzë­¼Ïû¼¿ýÛ¿m^˜×~í׿w~çw¸ßg}ÖgñÙŸýÙ\uÕUW]uÕUW]uÕ!dÛ\uÕUW]uÕUW]õ?ÎK¿ôKó7ó7Üïµ^ëµøíßþmþµNœ8Áîî.ÏÏÓŸþtüàó¯ñÖoýÖüÌÏü ÷{©—z)þú¯ÿš«þgxí×~m~çw~‡ÿ,Çç·~ë·xé—~i®úõÙŸýÙ|Îç|÷{­×z-~û·›ÿn»»»|Í×| ŸýÙŸÍýló¼ök¿6¿ó;¿Ãý>ë³>‹ÏþìÏæª«®ºêª«®ºêª«þ !Ûæª«®ºêª«®ºêªÿq>ú£?š¯ùš¯áló¯ñÛ¿ýÛ¼Îë¼/ÈW}ÕWñÑýÑükœ8q‚ÝÝ]î÷QõQ|õW5WýÏðÚ¯ýÚüÎïü÷{ЃÄ{¿÷{󯱻»Ë_ÿõ_ó;¿ó;û³?›ÏùœÏá~¯õZ¯Åoÿöoóßí·û·y×yÈ6/Ìk¿ökó;¿ó;Üï³>ë³øìÏþl®ºêª«®ºêª«®ºê¿²m®ºêª«®ºêª«®úç§ú§y›·yè¯þê¯xé—~i^TŸýÙŸÍç|Îçp¿·z«·âg~æg¸ß[½Õ[ñÓ?ýÓ¼¨þú¯ÿš—y™—á~ë·~‹×~í׿ªÿ^ûµ_›ßùßá~¯õZ¯Åoÿöoóoqë­·òÕ_ýÕ|Í×| Ïí³>ë³øìÏþl®úóÙŸýÙ|Îç|÷{­×z-~û·›ÿn¿ýÛ¿Íë¼Îëð@¶ya¾û»¿›[o½•û½ök¿6¯ýÚ¯ÍUW]uÕUW]uÕUWýB¶ÍUW]uÕUW]uÕUÿãìîîrâÄ è«¾ê«øèþh^T/ó2/Ã_ÿõ_s¿§?ýé<ä!áló¢úîïþnÞç}Þ‡²ÍUÿs¼ök¿6¿ó;¿Ãý^ëµ^‹ßþíßæß㻿û»yŸ÷yèøñã\¼x‘«þã|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæ¿Ûoÿöoó:¯ó:û³?›ÏùœÏá~¯õZ¯Åoÿöoóßí·û·y×yÈ6W]uÕUW]uÕUW]õ?²m®ºêª«®ºêª«®úé³?û³ùœÏùîwüøq.^¼È‹â»¿û»yŸ÷yî÷QõQ|õW5ýÑÍ×|Í×p¿ú¨â«¿ú«yQ<ä!áÖ[oå~_õU_ÅGôGsÕÿ¯ýÚ¯ÍïüÎïp¿×z­×â·û·ù÷úéŸþiÞæmÞ†ú¬Ïú,>û³?›«þc|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæ¿Ûoÿöoó:¯ó:û³?›ÏùœÏá~¯õZ¯Åoÿöoóßí·û·y×yÈ6ÿþú¯ÿšK—.ñÛ¿ýÛ¼ök¿6¯õZ¯Å†ÝÝ]þæoþ†[o½•[o½•—~é—æøñã¼Ök½W]uÕUW]uÕUWý¯ƒl›«®ºêª«®ºêª«þÇ:~ü8—.]â~ßõ]ßÅ{¿÷{ó/9qâ»»»ÜÏ6÷“Ä=ýéOçÁ~0/Ìw÷wó>ïó>ÜïAz·Þz+/Ì­·ÞÊç|ÎçðÛ¿ýÛÜzë­¼0~ðƒyí×~m>ë³>‹?øÁüK^ûµ_›ßùßá~¶øœÏù¾ú«¿šÝÝ]è½ßû½ù¬Ïú,üàðÚ¯ýÚüÎïü÷³ Àîî._ó5_Ãw÷wsë­·òÜŽ?Î{¿÷{óYŸõY?~œÚÝÝåc>æcøéŸþivwwynÇç£?ú£ù¬Ïú,þ³¼ök¿6¿ó;¿Ãý^ëµ^‹ßþíßæßë¯ÿú¯y™—yè³>ë³øìÏþl^TŸó9ŸÃOÿôOó×ý×¼ oýÖoÍG}ÔGñÚ¯ýÚükýõ_ÿ5_ó5_ÃOÿôO³»»Ë òÒ/ýÒ¼õ[¿5õQÅñãÇù×úë¿þk¾æk¾†ŸþéŸfww—ççøñã¼õ[¿5ŸõYŸÅƒü`^ŸýÙŸÍç|Îçp¿×z­×â·û·y~^ûµ_›ßùßá~¿õ[¿Åk¿ökó¢’ÄýÖoý¯ýÚ¯Íý>û³?›ÏùœÏáEõ[¿õ[¼ök¿6÷{í×~m~çw~‡û}Ög}ŸýÙŸÍ‹âÖ[oås>çsøéŸþivwwy~Ž?Îk¿ökóYŸõY¼ôK¿4/Šßþíßæu^çu¸ßg}ÖgñÙŸýÙüöoÿ6_ó5_ÃOÿôOóü?~œ·~ë·æ³>ë³xðƒÌUW]uÕUW]uÕUÿ+ Ûæª«®ºêª«®ºêªÿ±Þú­ßšŸù™Ÿá~ïõ^ïÅw÷wóÂüõ_ÿ5/ó2/ÃýÞê­ÞŠŸþéŸæ~oýÖoÍÏüÌÏp¿ïú®ïâ½ßû½yaÞû½ß›ïùžïá~ïõ^ïÅw÷wóüìîîò1ó1|÷w7ÿßõ]ßÅ{¿÷{ó¼ök¿6¿ó;¿ÃýlóÕ_ýÕ|ÌÇ| /ÈW}ÕWñÑýѼök¿6¿ó;¿Ãýló×ý×¼ÍÛ¼ ·Þz+ÿ’—~é—æ·~ë·8~ü8¿ýÛ¿ÍÛ¼ÍÛ°»»Ë¿äµ_ûµù©Ÿú)Ž?δ×~í׿w~çw¸ßk½ÖkñÛ¿ýÛü{ýôOÿ4oó6oÃ}Ög}ŸýÙŸÍ¿ä§ú§ù˜ùn½õV^T¯ýÚ¯ÍOýÔOqüøq^Ÿó9ŸÃgögó¯qüøq¾ë»¾‹·~ë·æE±»»Ëû¼ÏûðÓ?ýÓük|ôG4ŸõYŸÅñãÇya>û³?›ÏùœÏá~¯õZ¯Åoÿöoóü¼ök¿6¿ó;¿Ãý~ë·~‹×~í׿E%‰ú­ßú-^ûµ_›û}ög6Ÿó9ŸÃ‹ê·~ë·xí×~mî÷Ú¯ýÚüÎïü÷û¬Ïú,>û³?›fww—ù˜á»¿û»ù×xï÷~o¾ê«¾ŠãÇóÂüöoÿ6¯ó:¯Ãý>ë³>‹ÏþìÏæs>çsøìÏþl^Ç竾ê«xï÷~o®ºêª«®ºêª«®úÙ6W]uÕUW]uÕUWýõÕ_ýÕ|ÌÇ| ÷{é—~iþê¯þŠæ³?û³ùœÏùî÷U_õU|ôG4÷ûê¯þj>æc>†û½Õ[½?ýÓ?Í ó‡<„[o½•û}×w}ïýÞïÍsÛÝÝåu^çuøë¿þkþ=~ê§~Š·~ë·æyí×~m~çw~‡ûýÕ_ý/ó2/à óô§??øÁ¼ök¿6¿ó;¿Ãýþê¯þŠ×y×aww—Õk¿ökó[¿õ[üôOÿ4oó6oÿÆG}ÔGñÕ_ýÕüG{í×~m~çw~‡û½Ök½¿ýÛ¿Í¿×ë¼ÎëðÛ¿ýÛ<ÐoýÖoñÚ¯ýÚ¼0ßýÝßÍû¼Ïûðoqüøq~ë·~‹—~é—æ…ùèþh¾æk¾†«Ÿú©Ÿâ­ßú­yan½õV^çu^‡[o½•‹—~é—æ·~ë·8~ü8/Ègögó9Ÿó9Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßoýÖoñÚ¯ýÚ¼¨$ñ@¿õ[¿Åk¿öks¿ÏþìÏæs>çsxQýÖoý¯ýÚ¯Íý^ûµ_›ßùßá~ŸõYŸÅgögó‚ìîîò:¯ó:üõ_ÿ5ÿ/ýÒ/ÍOýÔOñà?˜ä·û·y×yî÷YŸõY<ãÏ໿û»ù×ú©Ÿú)Þú­ßš«®ºêª«®ºêª«þGC¶ÍUW]uÕUW]uÕUÿcýõ_ÿ5/ó2/Ã]¼x‘ãÇó‚¼Ì˼ ý×Íýþê¯þŠ—~é—æ~·Þz+yÈC¸ßñãǹxñ"/È­·ÞÊCòèâÅ‹?~œçö6oó6üôOÿ4ôR/õR|ôG4~ðƒyí×~mî÷Û¿ýÛüöoÿ6ßýÝßÍ3žñ èøñã\¼x‘äµ_ûµùßùî÷Ò/ýÒüõ_ÿ5/È[½Õ[ñÓ?ýÓÜïµ_ûµùßùî÷à?˜[o½•û½×{½oýÖoÍñãÇøéŸþi¾û»¿›K—.ñ@õQÅ÷|Ï÷°»» Àƒô >ú£?š—~é—`ww—¯þê¯æw~çwxnOúÓyðƒÌ¤×~í׿w~çw¸ßk½ÖkñÛ¿ýÛü{|÷w7ïó>ïÃ;vŒÝÝ]^˜ŸþéŸæmÞæmxnïõ^ïÅ[¿õ[óÚ¯ýÚ?~œ[o½•ßþíßæ»¿û»ùßùèøñã<ýéOçøñãú£?š×~í׿µ_ûµÙÝÝå¯ÿú¯ùéŸþi¾û»¿›K—.ñ@/ýÒ/Í_ýÕ_ñ‚|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæùyí×~m~çw~‡ûýÖoý¯ýگ͋Jô[¿õ[¼ök¿6÷ûíßþm~û·€[o½•ïùžïá>ë³>‹zï÷~oüàs¿×~í׿w~çw¸ßg}ÖgñÙŸýÙû³?›çç³?û³ùœÏùè³>ë³øìÏþlþ#½ök¿6¿ó;¿Ãý^ëµ^‹ßþíßæßbww—¯ùš¯á³?û³ynŸõYŸÅgögó‚Üzë­¼Ì˼ »»»ÜïØ±cüöoÿ6/ýÒ/Í òÝßýݼÏû¼ôÖoýÖüÔOýÏÏ[¿õ[ó3?ó3Üïµ^ëµøíßþm^˜ÏþìÏæs>çsx ïú®ïâ½ßû½y~ÞæmÞ†ŸþéŸæÞë½Þ‹¯þê¯æøñãú£?šççÖ[oå­ßú­ù›¿ùè§~ê§xë·~k®ºêª«®ºêª«®ú Ù6W]uÕUW]uÕUWýöÞïýÞ|Ï÷|÷û¬Ïú,>û³?›çç§ú§y›·yî÷^ïõ^|÷w7Ïí½ßû½ùžïùî÷QõQ|õW5ÏÏ{¿÷{ó=ßó=Üï£>ê£øê¯þjžÛ{¿÷{ó=ßó=ÜïAz·Þz+/ŠÝÝ]Nœ8Á}Ög}ŸýÙŸÍóóÚ¯ýÚüÎïüô =ˆ¿þë¿æøñãüK^ûµ_›ßùßáô ñ×ý×?~œä£?ú£ùš¯ùžÛg}ÖgñÙŸýÙ¼0/ýÒ/ÍßüÍßp¿×z­×â·û·ùôÚ¯ýÚüÎïü÷{é—~i¾ú«¿š¿þë¿æ¯ÿú¯ùíßþmn½õVžÛk½ÖkñÛ¿ýÛ¼0ïýÞïÍ÷|Ï÷p¿cÇŽñÛ¿ýÛ¼ôK¿4ÿ’ïþîïæ}Þç}x ßú­ßâµ_ûµyn’x ¿ú«¿â¥_ú¥ù—¼ök¿6¿ó;¿ÃýÞê­ÞŠŸþéŸæ¹ýöoÿ6¯ó:¯Ã½Õ[½?ýÓ?Í‹âµ_ûµùßùèéO:~ðƒynŸýÙŸÍç|Îçp¿×z­×â·û·y~^ûµ_›ßùßá~¿õ[¿Åk¿ökó¢’ÄýÖoý¯ýÚ¯ÍóóÛ¿ýÛ¼Îë¼d›æµ_ûµùßùî÷YŸõY|ög6ÏíÖ[oå!yôZ¯õZüöoÿ6/Š·~ë·æg~ægx ßú­ßâµ_ûµyn¿ýÛ¿Íë¼Îëðܾ뻾‹÷~ï÷æ…ùë¿þk^æe^†z¯÷z/¾û»¿›«®ºêª«®ºêª«þÇB¶ÍUW]uÕUW]uÕUÿ£}÷w7ïó>ïÃý^ëµ^‹ßþíßæùyï÷~o¾ç{¾‡ûýÔOýoýÖoÍsûéŸþiÞæmÞ†û½ôK¿4õWÅóó2/ó2üõ_ÿ5÷û­ßú-^ûµ_›çvâÄ vww¹ßw}×wñÞïýÞ¼¨^ûµ_›ßùßá~ïõ^ïÅw÷wóü¼ök¿6¿ó;¿Ã}×w}ïýÞïÍ‹âµ_ûµùßùè§~ê§xë·~k^˜ßþíßæu^çux cÇŽ±»»Ë¿ä³?û³ùœÏùî÷Z¯õZüöoÿ6ÿ‘^ûµ_›ßùßá?ËK½ÔKñÛ¿ýÛ?~œäÖ[oå!yô]ßõ]¼÷{¿7/ª÷~ï÷æ{¾ç{¸ß[½Õ[ñÓ?ýÓ<7Iê£>Нþê¯æ_òÓ?ýÓ¼ÍÛ¼ d›ÿH¯ýÚ¯ÍïüÎïðíØ±c|ôG4ŸýÙŸÍ¿ä³?û³ùœÏùîwìØ1vwwù×øéŸþiÞæmÞ†ºxñ"Çç$ñ@_õU_ÅGôGóEôYŸõY|ög6ÿïýÞïÍ÷|Ï÷p¿ãÇsñâEžÛgögó9Ÿó9Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßoýÖoñÚ¯ýÚ¼¨$ñ@¿õ[¿Åk¿ökóüüöoÿ6¯ó:¯ÃÙæ…yí×~m~çw~‡û}Ög}ŸýÙŸÍs;qâ»»»Üï£>ê£øê¯þjþ5>û³?›ÏùœÏálóÜ~û·›×y×á~ê§~Š·~ë·æEñÙŸýÙ|Îç|÷{­×z-~û·›«®ºêª«®ºêª«þÇB¶ÍUW]uÕUW]uÕUÿã=øÁæÏx÷û­ßú-^ûµ_›úë¿þk^æe^†û½ÔK½ý×Í òÚ¯ýÚüÎïü÷û®ïú.Þû½ß›úéŸþiÞæmÞ†û½Õ[½?ýÓ?͆ÏþìÏæs>çs¸ßk½ÖkñÛ¿ýÛê£8~ü8ÿ¿ýÛ¿Íë¼Îëð@OúÓyðƒÌ¿Æ_ÿõ_ó2/ó2<Ð_ýÕ_ñÒ/ýÒ<Ðgögó9Ÿó9Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßoýÖoñÚ¯ýÚ¼¨$ñ@¿õ[¿Åk¿ökóüüöoÿ6¯ó:¯ÃÙæ…yí×~m~çw~‡û}Ög}ŸýÙŸÍýõ_ÿ5/ó2/ÃýÕ_ý/ýÒ/Ϳƭ·ÞÊCòè·~ë·xí×~mè·û·y×yèéO:~ðƒyQ|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæª«®ºêª«®ºêªÿ±msÕUW]uÕUW]uÕÿxýÑÍ×|Í×p¿¯úª¯â£?ú£y ¯þê¯æc>æc¸ßg}ÖgñÙŸýÙ¼ ŸýÙŸÍç|Îçp¿÷z¯÷⻿û»y þèæk¾æk¸ßW}ÕWñÑýÑüGÙÝÝåoþæoøéŸþi~ú§š[o½•û½Ök½¿ýÛ¿ÍóóÚ¯ýÚüÎïü÷û¨ú(¾ú«¿šÕk¿ökó;¿ó;Üï³>ë³øìÏþl^’x ßú­ßâµ_ûµù—üöoÿ6¯ó:¯ÃÙæ?Òk¿ökó;¿ó;ÜïØ±c¼ôK¿4/È_ÿõ_séÒ%žÛG}ÔGñÙŸýÙ?~œ­'N°»»Ëý>ë³>‹ÏþìÏæ_ë¥_ú¥ù›¿ùî÷YŸõY|ög6ôÝßýݼÏû¼ÏÏk¿ökóÖoýÖ¼Ök½/ýÒ/Í¿Ögögó9Ÿó9ÜïØ±cìîîòo!‰úª¯ú*>ú£?šúìÏþl>çs>‡û½Ök½¿ýÛ¿ÍóóÚ¯ýÚüÎïü÷û­ßú-^ûµ_›•$è·~ë·xí×~mžŸßþíßæu^çux Û¼0¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³y ¯þê¯æc>æcx Ûü[?~œK—.q¿Ïú¬Ïâ³?û³y ßþíßæu^çux Û¼¨¾û»¿›÷yŸ÷á~¯õZ¯ÅoÿöosÕUW]uÕUW]uÕÿXȶ¹êª«®ºêª«®ºê¼ŸþéŸæmÞæm¸ß[½Õ[ñÓ?ýÓ<Ðk¿ökó;¿ó;Üï¯þê¯xé—~i^¿þë¿æe^æe¸ßƒü`žþô§ó@/ó2/Ã_ÿõ_s¿§?ýé<øÁæ_ë¯ÿú¯¹té¿ýÛ¿ Àoÿöosë­·rë­·ò‚¼Ök½¿ýÛ¿ÍóóÚ¯ýÚüÎïü÷û¬Ïú,>û³?›Õk¿ökó;¿ó;Üï³>ë³øìÏþl^’x ßú­ßâµ_ûµù—üöoÿ6¯ó:¯ÃÙæ?Òk¿ökó;¿ó;Üïµ^ëµøíßþm^˜ßþíßæ£?ú£ù›¿ùèµ_ûµù®ïú.üàó¯!‰zðƒ̃ü`þµþú¯ÿšÝÝ]î÷YŸõY|ög6Ïí¥_ú¥ù›¿ù^˜ãÇóÖoýÖ¼ök¿6oõVoÅñãÇù—|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæßâµ_ûµùßùî÷YŸõY|ög6ôÙŸýÙ|Îç|÷{­×z-~û·›ççµ_ûµùßùî÷[¿õ[¼ök¿6/*I<ÐoýÖoñÚ¯ýÚû³?›Õk¿ökó;¿ó;Üï³>ë³øìÏþl^’x ßú­ßâµ_ûµù—üöoÿ6¯ó:¯ÃÙæ?Òk¿ökó;¿ó;Üïµ^ëµøíßþm^ïýÞïÍ÷|Ï÷ð@Çç·~ë·xé—~i^ý×Í˼ÌËðŸáµ^ëµøíßþmžÛîî.oýÖoÍïüÎïð¢zë·~kÞú­ßš÷z¯÷âùèþh¾æk¾†û½Ök½¿ýÛ¿Í¿Åk¿ökó;¿ó;Üï³>ë³øìÏþlè³?û³ùœÏùî÷Z¯õZüöoÿ6ÏÏk¿ökó;¿ó;Üï·~ë·xí×~m^T’x ßú­ßâµ_ûµy~~û·›×y×áló¼ök¿6¿ó;¿Ãý>ë³>‹ÏþìÏæ>û³?›ÏùœÏá~¯õZ¯ÅoÿöoóoñÚ¯ýÚüÎïü÷{«·z+~ú§šúíßþm^çu^‡û½Ök½¿ýÛ¿Í‹ê·û·y×yî÷Z¯õZüöoÿ6W]uÕUW]uÕUWý…l›«®ºêª«®ºêª«þWxé—~iþæoþ†ûýÕ_ý/ýÒ/ ÀOÿôOó6oó6Üï½Þë½øîïþnþ%oýÖoÍÏüÌÏp¿ïú®ïâ½ßû½øéŸþiÞæmÞ†û½×{½ßýÝßÍ ³»»Ë×|Í×ðÙŸýÙük¼ÔK½Ççw~çw¸ßk½ÖkñÛ¿ýÛë³>‹ÏþìÏæE!‰ú­ßú-^ûµ_›Éoÿöoó:¯ó:ë³øìÏþl^T¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³yQHâ~ë·~‹×~í׿_òÛ¿ýÛ¼Îë¼d›ÿH¯ýÚ¯ÍïüÎïp¿×z­×â·û·yQíîîòÚ¯ýÚüÍßü ôÒ/ýÒüÖoýÇç…ùíßþm^çu^‡z©—z)Ž?ο×K¿ôKóÕ_ýÕüKþú¯ÿšŸþéŸæ§ú§ù›¿ù^ßõ]ßÅ{¿÷{ó@ïýÞïÍ÷|Ï÷p¿×z­×â·û·ù·xí×~m~çw~‡û}Ög}ŸýÙŸÍ}ög6Ÿó9ŸÃý^ëµ^‹ßþíßæùyí×~m~çw~‡ûýÖoý¯ýگ͋Jô[¿õ[¼ök¿6ÏÏoÿöoó:¯ó:çs¸ßk½ÖkñÛ¿ýÛü[¼ök¿6¿ó;¿Ãý>ë³>‹ÏþìÏæ>û³?›ÏùœÏá~¯õZ¯Åoÿöoóü¼ök¿6¿ó;¿Ãý~ë·~‹×~í׿E%‰ú­ßú-^ûµ_›çç·û·y×yÈ6/Ìk¿ökó;¿ó;Üï³>ë³øìÏþlè³?û³ùœÏùî÷Z¯õZüöoÿ6ÿ¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³y ßþíßæu^çu¸ßk½ÖkñÛ¿ýÛ¼¨~û·›×y×á~¯õZ¯ÅoÿöosÕUW]uÕUW]uÕÿXȶ¹êª«®ºêª«®ºê IÜï¥_ú¥ù«¿ú+n½õVò‡p¿—z©—â¯ÿú¯yQ½ôK¿4ó7Ãý.^¼ÈñãÇ‘Äý^ê¥^Š¿þë¿æùíßþm^çu^‡z©—z)¾û»¿›—~é—æEñÖoýÖüÌÏü ÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ª×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼($ñ@¿õ[¿Åk¿ökó/ùíßþm^çu^‡²Í¤×~í׿w~çw¸ßk½ÖkñÛ¿ýÛük}õW5ó1Ãsû©Ÿú)Þú­ßšFô]ßõ]¼÷{¿7ÿÜzë­|÷w7_ýÕ_Í¥K—x Ïú¬Ïâ³?û³¹ßgögó9Ÿó9Üïøñã\¼x‘ I<ÐW}ÕWñÑýÑ<Ðgögó9Ÿó9Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼¨$ñ@¿õ[¿Åk¿ökóüüöoÿ6¯ó:¯ÃÙæ…yí×~m~çw~‡û}Ög}ŸýÙŸÍ}õW5ó1ÃÙæßâĉìîîr¿Ïú¬Ïâ³?û³y ßþíßæu^çu¸ßk½ÖkñÛ¿ýÛ¼¨~û·›×y×á~¯õZ¯ÅoÿöosÕUW]uÕUW]uÕÿXȶ¹êª«®ºêª«®ºê·~ë·æg~æg¸ßÅ‹ùéŸþiÞç}Þ‡û}ÔG}_ýÕ_Í‹ê£?ú£ùš¯ùî÷[¿õ[¼Îë¼÷û¬Ïú,>û³?›ä½ßû½ùžïùèéO:~ðƒyQ½ök¿6¿ó;¿Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍ‹êµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóéµ_ûµùßùî÷Z¯õZüöoÿ6ÿ¯ýÚ¯ÍïüÎïð@ÇçéO:Ççyé—~iþæoþ†û½×{½ßýÝßÍÿ$ý×Ík¿ökséÒ%î÷Z¯õZüöoÿ6÷ûíßþm^çu^‡zúӟ΃ü`þ5þú¯ÿš—y™—á~ë·~‹×~í׿>û³?›ÏùœÏá~¯õZ¯Åoÿöoóü¼ök¿6¿ó;¿Ãý>ë³>‹ÏþìÏæEqë­·ò‡<„ú­ßú-^ûµ_›çç·û·y×yÈ6/Ìk¿ökó;¿ó;Üï³>ë³øìÏþlè¯ÿú¯y™—yè¯þê¯xé—~iþ5n½õVò‡ð@¿õ[¿Åk¿ökó@¿ýÛ¿Íë¼Îëp¿×z­×â·û·yQýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæª«®ºêª«®ºêªÿ±msÕUW]uÕUW]uÕÿ_ýÕ_ÍÇ|ÌÇp¿ßú­ßâ§ú§ùš¯ùî÷[¿õ[¼ök¿6/ªŸþéŸæmÞæm¸ßW}ÕW±»»Ëç|Îçp¿ßú­ßâµ_ûµyA^ûµ_›ßùßá~¯õZ¯Åoÿöoó¯!‰z­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ª×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼($ñ@¿õ[¿Åk¿ökó/ùíßþm^çu^‡²Í¤×~í׿w~çw¸ßk½ÖkñÛ¿ýÛü[Üzë­¼ôK¿4—.]âÞë½Þ‹ïþîïæùèþh¾æk¾†û?~œ§?ýé?~œÕîî.yÈCxðƒÌñãÇyðƒÌ{½×{ñÚ¯ýÚÜû»ùßùn½õV~û·›Ïú¬Ïâ³?û³yQ½÷{¿7ßó=ßÃý^ëµ^‹ßþíßæ$ñ@_õU_ÅGôGó¯ñÑýÑ|Í×| d›çöÙŸýÙ|Îç|÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ŠŸþéŸæmÞæmx ßú­ßâµ_ûµy~~û·›×y×áló¼ök¿6¿ó;¿Ãý>ë³>‹ÏþìÏæ¹Iâ>ê£>Нþê¯æ_ã³?û³ùœÏùèâÅ‹?~œúíßþm^çu^‡û½Ök½¿ýÛ¿Í‹ê·û·y×yî÷Z¯õZüöoÿ6W]uÕUW]uÕUWý…l›«®ºêª«®ºêª«þ×øë¿þk^æe^†û}ÕW}ßó=ßÃ_ÿõ_s?ÛükIâ~ïõ^ïÅîî.?ó3?À±cÇØÝÝå…yðƒÌ3žñ î÷^ïõ^|÷w7/ª¯þê¯æc>æcx ×z­×â·û·y~^ûµ_›ßùßá~ŸõYŸÅgögó¢zí×~m~çw~‡û}Ög}ŸýÙŸÍ‹Bô[¿õ[¼ök¿6ÿ’ßþíßæu^çux ÛüGzí×~m~çw~‡û½Ök½¿ýÛ¿Í¿ÕWõWó1ó1<·ßú­ßâµ_ûµy~~û·›×y×á¾ê«¾ŠþèæEõÙŸýÙ|Îç|ôWõW¼ôK¿4÷ûèþh¾æk¾†û½ôK¿4õWÅ‹ê£?ú£ùš¯ùî÷Z¯õZüöoÿ6ôÚ¯ýÚüÎïü÷{ðƒÌ_ýÕ_qüøq^»»»<ä!aww—û½Õ[½?ýÓ?ÍsûìÏþl>çs>‡û½Ök½¿ýÛ¿ÍóóÚ¯ýÚüÎïü÷{«·z+~ú§šÅÛ¼ÍÛðÓ?ýÓ<ÐoýÖoñÚ¯ýÚë³>‹ÏþìÏæE!‰ú­ßú-^ûµ_›Éoÿöoó:¯ó:ú£?š¯ùš¯ážþô§óà?˜æ§ú§y›·yžÛoýÖoñÚ¯ýÚïó>|÷w7ô]ßõ]¼÷{¿7Ïí·û·y×yî÷Z¯õZüöoÿ6/ªßþíßæu^çu¸ßk½ÖkñÛ¿ýÛ\uÕUW]uÕUW]õ?²m®ºêª«®ºêª«®ú_å­ßú­ù™ŸùžŸ¯úª¯â£?ú£ù×úê¯þj>æc>†çç«¾ê«øèþh^˜÷~ï÷æ{¾ç{x ïú®ïâ½ßû½ya~û·›·y›·aww—çöZ¯õZüöoÿ6ÏÏk¿ökó;¿ó;Üï³>ë³øìÏþl^T¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³yQHâ~ë·~‹×~í׿_òÛ¿ýÛ¼Îë¼d›ÿH¯ýÚ¯ÍïüÎïp¿×z­×â·û·ù÷øë¿þk^æe^†çöYŸõY|ög6ÏÏOÿôOó6oó6<ÐñãÇù­ßú-^ú¥_šäÖ[oåmÞæmøë¿þk軾ë»xï÷~ožÛƒü`žñŒgp¿ãÇó[¿õ[¼ôK¿4/ÌGôGó5_ó5<Ð_ýÕ_ñÒ/ýÒ<·×~í׿w~çwx ÷~ï÷滾ë»xaÞç}Þ‡ïþîïæ^ëµ^‹ßþíßæùùìÏþl>çs>‡û½Ök½¿ýÛ¿ÍóóÓ?ýÓ¼ÍÛ¼ ôÒ/ýÒüÖoýÇçùùîïþnÞç}Þ‡çç·~ë·xí×~mžŸßþíßæu^çux ßú­ßâµ_ûµyA^ûµ_›ßùßá~ŸõYŸÅgögóü¼õ[¿5?ó3?ý÷{¿7ßõ]ßÅ ó>ïó>|÷w7ôZ¯õZüöoÿ6ÏÏoÿöoó:¯ó:Üïµ^ëµøíßþm^T¿ýÛ¿Íë¼Îëp¿×z­×â·û·¹êª«®ºêª«®ºê,dÛ\uÕUW]uÕUW]õ¿Êw÷wó>ïó>û³?›÷z¯÷âøñã<Ðoÿöoó=ßó=|÷w7/ȃü`žþô§óü¼ök¿6¿ó;¿Ãý>ë³>‹ÏþìÏæEõÚ¯ýÚüÎïü÷û¬Ïú,>û³?›…$è·~ë·xí×~mþ%¿ýÛ¿Íë¼Îëð@¶ùôÚ¯ýÚüÎïü÷{­×z-~û·›¯ÏþìÏæs>çsxnõWÅK¿ôKóü|ôG4_ó5_Ãs{ï÷~oÞû½ß›×z­×â~ý×ÍÏüÌÏðÕ_ýÕìîîò@ïõ^ïÅw÷wóüüôOÿ4oó6oÃsûèþhÞë½Þ‹—~é—æ~æg~†ïþîïæ§ú§y ×z­×â·û·y~vwwyðƒÌ¥K—x ?øÁ|ög6oõVoÅñãÇØÝÝåg~ægøìÏþln½õVèØ±cÜzë­?~œçç³?û³ùœÏùî÷Z¯õZüöoÿ6/ȃü`žñŒgð@~ðƒùìÏþlüàóZ¯õZüÎïü·Þz+_ýÕ_Í_ÿõ_s¿—z©—âoþæo¸ßoýÖoñÚ¯ýÚ¼ ’xn¯ýÚ¯ÍñãÇÙÝÝ嫾ê«xé—~iî÷Ú¯ýÚüÎïü÷û¬Ïú,>û³?›çgww——~é—æÏxôà?˜ÏþìÏæ­Þê­8~ü8»»»üÌÏü ŸýÙŸÍ­·ÞÊ;vŒßþíßæ¥_ú¥y~~û·›×y×á~¯õZ¯Åoÿöoó¢úíßþm^çu^‡û½Ök½¿ýÛ¿ÍUW]uÕUW]uÕUÿc!Ûæª«®ºêª«®ºêªÿUn½õVò‡ðÜô që­·òoõà?˜g<ã<Ѓô n½õV^ýÑÍ×|Í×ðü¼ôK¿4Çgww—¿þë¿æ¹½ÔK½ŸýÙŸÍÛ¼ÍÛð@/^äøñã<·×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼¨^ûµ_›ßùßá~ŸõYŸÅgögó¢ÄýÖoý¯ýگͿä·û·y×yÈ6ÿ‘^ûµ_›ßùßá~¯õZ¯Åoÿöoóïµ»»ËK¿ôKóŒg<ƒzé—~iþê¯þŠä½ßû½ùžïùþ­^ê¥^Šßþíßæøñã¼ ïýÞïÍ÷|Ï÷ðoõR/õRüöoÿ6Ççùë¿þk^ûµ_›K—.ñoqìØ1~û·›—~é—æùìÏþl>çs>‡û½Ök½¿ýÛ¿Í òÓ?ýÓ¼ÍÛ¼ ÿZïõ^ïŃü`>çs>‡ûýÖoý¯ýÚ¯Í òÒ/ýÒüÍßü /Èw}×wñÞïýÞÜïµ_ûµùßùî÷YŸõY|ög6/È_ÿõ_óÚ¯ýÚ\ºt‰‹cÇŽñÛ¿ýÛ¼ôK¿4/Èoÿöoó:¯ó:Üïµ^ëµøíßþm^T¿ýÛ¿Íë¼Îëp¿×z­×â·û·¹êª«®ºêª«®ºê,dÛ\uÕUW]uÕUW]õ¿Îƒü`žñŒgð@ïõ^ïÅw÷wóoõÞïýÞ|Ï÷|ôQõQ|õW5/ª÷~ï÷æ{¾ç{ø×ø¨ú(>û³?›ãÇsüøq.]ºÄý¾ë»¾‹÷~ï÷æ¹½ök¿6¿ó;¿Ãý>ë³>‹ÏþìÏæEõÚ¯ýÚüÎïü÷û¬Ïú,>û³?›…$è·~ë·xí×~mþ%¿ýÛ¿Íë¼Îëð@¶ùôÚ¯ýÚüÎïü÷{­×z-~û·›ÿ¿ýÛ¿Íë¼ÎëðÜ>ë³>‹ÏþìÏæùìÏþl>çs>‡­ú¨â«¿ú«yQ|ôG4_ó5_ÿÖ[½Õ[ñÝßýÝ?~œÉ­·ÞÊ{¿÷{ó;¿ó;ük¼Ök½?ýÓ?ÍñãÇya>û³?›ÏùœÏá~¯õZ¯ÅoÿöoóÂ|÷w7ïó>ïËê³>ë³øìÏþl>û³?›ÏùœÏá~¿õ[¿Åk¿ökó‚üöoÿ6¯ó:¯Ã òYŸõY|ög6÷{í×~m~çw~‡û}Ög}ŸýÙŸÍ ³»»Ë[¿õ[ó;¿ó;ük¼Ök½ßýÝß̓ü`^˜ßþíßæu^çu¸ßk½ÖkñÛ¿ýÛ¼¨~û·›×y×á~¯õZ¯ÅoÿöosÕUW]uÕUW]uÕÿXȶ¹êª«®ºêª«®ºê÷~ï÷æ{¾ç{x Ÿú©Ÿâ­ßú­ù·úîïþnÞç}Þ‡ú©Ÿú)Þú­ßšïþîïæ³?û³yÆ3žÁ rìØ1Þú­ßšÏþìÏæÁ~0÷{ï÷~o¾ç{¾‡û½ök¿6¿õ[¿Ås{í×~m~çw~‡û}Ög}ŸýÙŸÍ‹êµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóéµ_ûµùßùî÷Z¯õZüöoÿ6ÿQÞû½ß›ïùžïáŽ?Î_ýÕ_ñà?˜äÖ[oå³?û³ùéŸþi.]ºÄ óVoõV|ôG4¯ýگͿÆoÿöoóÝßýÝ|Ï÷|ÿ’·z«·â£?ú£yí×~mþµ¾û»¿›¯þê¯æoþæoxaÞê­ÞŠþèæµ_ûµyQ|ög6Ÿó9ŸÃý^ëµ^‹ßþíßæ_rë­·òÙŸýÙ|Ï÷|/Èk½ÖkñÙŸýÙ¼ök¿6ŸýÙŸÍç|Îçp¿ßú­ßâµ_ûµyaþú¯ÿš÷~ï÷æoþæoxnoõVoÅOÿôOs¿×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼(~û·›þèæoþæoxaÞê­ÞŠþèæµ_ûµyQüöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæEõÛ¿ýÛ¼Îë¼÷{­×z-~û·›«®ºêª«®ºêª«þÇB¶ÍUW]uÕUW]uÕUWý'øë¿þkþú¯ÿš[o½•û?~œ×~í׿¥_ú¥¹êÿ§¿þë¿æ·û·ÙÝÝå^ûµ_›—~é—æøñãü{ýöoÿ6ý×Íîî.÷;~ü8/ýÒ/ÍK¿ôKsüøqþ½vwwùíßþmþú¯ÿšzí×~m^ú¥_šãÇó_iww—ßþíßæ¯ÿú¯¹ßK¿ôKóÒ/ýÒ<øÁæ?Ê­·ÞÊ­·ÞÊýŽ?ÎK¿ôKómww—ßþíßæ¯ÿú¯y ×~í׿¥_ú¥9~ü8W]uÕUW]uÕUW]õB Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUWýµ»»Ëç|ÎçðÝßýݼ÷{¿7_õU_ÅUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕ l›«®ºêªÿ¡>ú£?š¯ùš¯á>ê£>Нþê¯æª«®ºêª«®ºêª«®ºêª«®ºêª«®ºê@¶ÍUW]uÕÿP'Nœ`ww—:~ü8/^䪫þ­vww9wîøÃ¹êª¯axžÀK¾äKrÕUÿþú¯ÿšñ§ÖÊUWý{=ñ‰Oä†n`{{›«®ú÷ºãŽ;¨µrÝu×qÕUÿ^çÏŸgoo‡<ä!\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóó~¯ýã|ûo½W]õoñ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)ñÁb±àª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«looó*¯ò*\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰ççý^ûÇùößz;®ºêßâ Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏÏû½öóí¿õv\uÕ¿Åžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ÄG|‹Å‚«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲½½Í«¼Ê«pÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žŸ÷{íçÛëí¸êª‹'<á üõ_ÿ5ïüÎïÌUWý{íîîòÝßýÝ|ôG4W]õáK¿ôKùˆø‹W]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_e{{›Wy•W᪫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Iâ#X,\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•íím^åU^…«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñü¼ßkÿ8ßþ[oÇUWý[<á Oà¯ÿú¯yçw~g®ºêßkww—ïþîïæ£?ú£¹êªÿ_ú¥_ÊG|ÄG°X,¸êª¯ïùžïáµ^ëµxðƒÌUWý{ýê¯þ*ÛÛۼʫ¼ W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6W]uÕUÿCIâùy¿×þq¾ý·ÞŽ«®ú·xžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóó~¯ýã|ûo½W]õoñ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)ñÁb±àª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«looó*¯ò*\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰ççý^ûÇùößz;®ºêßâ Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏÏû½öóí¿õv\uÕ¿Åžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ÄG|‹Å‚«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲½½Í«¼Ê«pÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUWý¯ò×ý×\ºt €×z­×â…ùßùŽ;ÆK¿ôKpë­·òŒg<€=èA<øÁæ…ÙÝÝåoþæoxЃăü`þ«Hâùy¿×þq¾ý·ÞŽ«®ú·xžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêªÿU^ûµ_›ßùßÀ6/Œ$^ëµ^‹ßþíßà§ú§y›·yÞë½Þ‹ïþîïæ…ùèþh¾æk¾€Ÿú©Ÿâ­ßú­ù¯"‰ççý^ûÇùößz;®ºêßâ Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«þWyí×~m~çw~Û¼0’x­×z-~û·›û=øÁæÏx/^äøñã¼ 'Nœ`ww—cÇŽ±»»Ë%Iâ#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«þWyí×~m~çw~Û¼0’x­×z-~û·›ûÝzë­<ä!à­Þê­øéŸþižŸ÷~ï÷æ{¾ç{ø«¿ú+^ú¥_šÿJ’x~Þﵜoÿ­·ãª«þ-žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/å#>â#X,\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•íím^åU^…«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºê•×~í׿w~çw°Í # €×z­×â·û·y —~é—æoþæoxúӟ΃ü`hww—'NðR/õRüõ_ÿ5ÿÕ$ñü¼ßkÿ8ßþ[oÇUWý[<á Oà¯ÿú¯yçw~g®ºêßkww—ïþîïæ£?ú£¹êªÿ_ú¥_ÊG|ÄG°X,¸êª¯ïùžïáµ^ëµxðƒÌUWý{ýê¯þ*ÛÛۼʫ¼ W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6W]uÕÿ*¯ýÚ¯ÍïüÎï`›F¯õZ¯Åoÿöoó@ßýÝßÍû¼ÏûðU_õU|ôG4ôÝßýݼÏû¼_õU_ÅGôGóïµ»»Ëç|ÎçðÝßýÝìîîòoõ~¯ýã|ûo½W]õoñ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)ñÁb±àª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«looó*¯ò*\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUÿ«¼ök¿6¿ó;¿€m^I¼Ök½¿ýÛ¿Ííîîòà?˜K—.ñÒ/ýÒüÕ_ýô:¯ó:üöoÿ6/^äøñãü{}ôG4_ó5_ÿ×û½öóí¿õv\uÕ¿Åžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ÄG|‹Å‚«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲½½Í«¼Ê«pÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUWý¯òÚ¯ýÚüÎïü¶ya$ðZ¯õZüöoÿ6Ïí½ßû½ùžïùžþô§óà?€[o½•‡<ä!¼Õ[½?ýÓ?Í„'N°»»Ë¿×û½öóí¿õv\uÕ¿Åžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ÄG|‹Å‚«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲½½Í«¼Ê«pÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUWý¯òÚ¯ýÚüÎïü¶yAvww9qâ¯õZ¯ÅoÿöoóÜ~û·›×y×à£>ê£øê¯þj¾ú«¿šù˜à§~ê§xë·~kþ#?~œK—.ñïõ~¯ýã|ûo½W]õoñ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)ñÁb±àª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«looó*¯ò*\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUÿ«¼ök¿6¿ó;¿€m^ßþíßæu^çux­×z-~û·›ççÁ~0ÏxÆ3xðƒÌÓŸþtò‡pë­·ò =ˆ[o½•ÿ(ýÑÍ×|Í×ðïõ~¯ýã|ûo½W]õoñ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)ñÁb±àª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«looó*¯ò*\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUÿ«¼ök¿6¿ó;¿€m^ßþíßæu^çux­×z-~û·›çç³?û³ùœÏùþê¯þ €—y™—à£>ê£øê¯þjþ#}ôG4ßýÝßÍ¥K—ø·z¿×þq¾ý·ÞŽ«®ú·xžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêªÿU^ûµ_›ßùßà·~ë·xí×~mžŸÏþìÏæs>çsx­×z-~û·›ççÖ[oå!yõQÀ×|Í×ðô§??øÁüw‘Äóó~¯ýã|ûo½W]õoñ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)ñÁb±àª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«looó*¯ò*\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUÿ«|ôG4_ó5_Àg}ÖgñÙŸýÙ<·ÝÝ]ò‡°»» Àk½ÖkñÛ¿ýÛ¼ oýÖoÍÏüÌÏðà?€[o½•—z©—â¯ÿú¯ùï$‰ççý^ûÇùößz;®ºêßâ Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«þWùéŸþiÞæmÞ†û}õW5õQÅý~û·›ù˜á¯ÿú¯¹ßk½ÖkñÛ¿ýÛ¼ ßýÝßÍû¼Ïûð@ßõ]ßÅ{¿÷{óßIÏÏû½öóí¿õv\uÕ¿Åžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ÄG|‹Å‚«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲½½Í«¼Ê«pÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUWý¯óÒ/ýÒüÍßü ÷{ðƒ̃ü`n½õVn½õVÞë½Þ‹[o½•ßùßáµ^ëµøíßþm^˜ãÇséÒ%îwñâEŽ?Î'Iú£?š«®úð¥_ú¥|ÄG|‹Å‚«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲½½Í«¼Ê«pÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUWý¯õ×ý×üôOÿ4·Þz+ÇçÁ~0oýÖo̓ü`þú¯ÿšÝÝ]Ž?ÎK¿ôKóÂìîîò×ý×<øÁæÁ~0ÿÝ$ñü¼ßkÿ8ßþ[oÇUWý[<á Oà¯ÿú¯yçw~g®ºêßkww—ïþîïæ£?ú£¹êªÿ_ú¥_ÊG|ÄG°X,¸êª¯ïùžïáµ^ëµxðƒÌUWý{ýê¯þ*ÛÛۼʫ¼ W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6W]uÕUÿCIâùy¿×þq¾ý·ÞŽ«®ú·xžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóó~¯ýã|ûo½W]õoñ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)ñÁb±àª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«looó*¯ò*\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰ççý^ûÇùößz;®ºêßâ Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏÏû½öóí¿õv\uÕ¿Åžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ÄG|‹Å‚«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲½½Í«¼Ê«pÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žŸ÷{íçÛëí¸êª‹'<á üõ_ÿ5ïüÎïÌUWý{íîîòÝßýÝ|ôG4W]õáK¿ôKùˆø‹W]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_e{{›Wy•W᪫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Iâ#X,\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•íím^åU^…«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñü¼ßkÿ8ßþ[oÇUWý[<á Oà¯ÿú¯yçw~g®ºêßkww—ïþîïæ£?ú£¹êªÿ_ú¥_ÊG|ÄG°X,¸êª¯ïùžïáµ^ëµxðƒÌUWý{ýê¯þ*ÛÛۼʫ¼ W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6W]uÕUÿCIâùy¿×þq¾ý·ÞŽ«®ú·xžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”øˆ`±XpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶··y•Wy®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóó~¯ýã|ûo½W]õoñ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)ñÁb±àª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«looó*¯ò*\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰ççý^ûÇùößz;®ºêßâ Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGøÒ/ýR>â#>‚ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏm®ºêßê Oxý×Í;¿ó;sÕUÿ^»»»|÷w7ýÑÍUWýGø’/ù>ò#?’ÅbÁUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏÏû½ösÕUÿV‹Ó{l^·Ë¹¿¿…«®ú÷ªó3/ýtîþãGqÕUÿn|õÇs÷?’œ W]õïuÍK?K·^Ãzw“«®ú÷:þð»i«Žý;NsÕUÿ^›×]dvü O¸‰«þ÷ùößz;®ºêª«þ@¶ÍUW]uÕÿP’x~Þﵜ«®ú·ZœÞcóº]Îýý-\uÕ¿Wœyé§s÷?Š«®úpã«?ž»ÿø‘äT¸êª¯k^úé\ºõÖ»›\uÕ¿×ñ‡ßM[uìßqš«®ú÷Ú¼î"³ã‡\xÂM\õ¿Ï·ÿÖÛqÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\uÕ¿Õâô›×írîïo᪫þ½ê|àÌK?»ÿøQ\uÕ„_ýñÜýÇ$§ÂUWý{]óÒOçÒ­×°ÞÝ䪫þ½Ž?ünÚªcÿŽÓ\uÕ¿×æu™?äÂnâªÿ}¾ý·ÞŽ«®ºêªÿmsÕUW]õ?”$žŸ÷{í窫þ­§÷ؼn—s W]õïUçg^úéÜýÇ⪫þ#Üøêçî?~$9®ºêßëš—~:—n½†õî&W]õïuüáwÓVûwœæª«þ½6¯»Èìø!žpWýïóí¿õv\uÕUWý/€l›«®ºêªÿ¡$ñü¼ßkÿ8W]õoµ8½Çæu»œûû[¸êª¯:8óÒOçî?~W]õáÆWçs>‡­×~í׿³>ë³xí×~mžÛoÿöoó:¯ó:ük½õ[¿5_õU_Ńü`^ßþíßæu^çux —~é—æ¯þê¯ø×x™—yþú¯ÿšû½Ök½¿ýÛ¿ÍÿD’x~Þﵜ«®ú·ZœÞcóº]Îýý-\uÕ¿Wœyé§s÷?Š«®úpã«?ž»ÿø‘äT¸êª¯k^úé\ºõÖ»›\uÕ¿×ñ‡ßM[uìßqš«®ú÷Ú¼î"³ã‡\xÂM\õ¿Ï·ÿÖÛqÕUW]õ¿²m®ºêÿ©ÏþìÏæs>çsø·ú©Ÿú)Þú­ßšúíßþm^çu^‡‹ãÇó]ßõ]¼õ[¿5ÏÏoÿöoó:¯ó:<·§?ýé<øÁæEqë­·ò‡<„z­×z-~û·›ÿ‰$ñü¼ßkÿ8W]õoµ8½Çæu»œûû[¸êª¯:8óÒOçî?~W]õáÆWp楟ÎÝü(®ºê?¯þxîþãG’S᪫þ½®yé§séÖkXïnrÕUÿ^Ç~7mÕ±Çi®ºêßkóº‹ÌŽrá 7qÕÿ>ßþ[oÇUW]uÕÿȶ¹êªÿ§>û³?›ÏùœÏà³>ë³øìÏþlþ%ý×Ík¿ökséÒ%¾ê«¾Šþèæ~¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿ’[o½•·~ë·æoþæoxðƒÌÓŸþtžÛoÿöoó:¯ó:¼ÔK½ó7ÀK¿ôKóWõW¼(^æe^†¿þë¿æAzÏxÆ3x­×z-~û·›ÿ‰$ñü¼ßkÿ8W]õoµ8½Çæu»œûû[¸êª¯:8óÒOçî?~W]õáÆWçsx­×z-~û·›ûýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛ¼(þú¯ÿš×~í×æÒ¥K|Ög}ŸýÙŸÍýöoÿ6¯ó:¯Àk½Ökqüøq~æg~€§?ýé<øÁæ…¹õÖ[yÈCÀG}ÔGñ5_ó5¼Ök½¿ýÛ¿ÍÿD’x~Þﵜ«®ú·ZœÞcóº]Îýý-\uÕ¿Wœyé§s÷?Š«®úpã«?ž»ÿø‘äT¸êª¯k^úé\ºõÖ»›\uÕ¿×ñ‡ßM[uìßqš«®ú÷Ú¼î"³ã‡\xÂM\õ¿Ï·ÿÖÛqÕUW]õ¿²m®ºêÿ©ÏþìÏæs>çsø¬Ïú,>û³?›Åoÿöoó:¯ó:?~œ‹/r¿ßþíßæu^çux­×z-~û·›Õw÷wó>ïó>?~œ‹/ò@¿ýÛ¿Íë¼ÎëðZ¯õZ¼÷{¿7ïó>ïÀW}ÕWñÑýѼ0_ýÕ_ÍÇ|ÌÇpìØ1~ú§š×y×àµ^ëµøíßþmþ'’Äóó~¯ýã\uÕ¿Õâô›×írîïo᪫þ½ê|àÌK?»ÿøQ\uÕ„_ýñÜýÇ$§ÂUWý{]óÒOçÒ­×°ÞÝ䪫þ½Ž?ünÚªcÿŽÓ\uÕ¿×æu™?äÂnâªÿ}¾ý·ÞŽ«®ºêªÿmsÕUÿO}ög6Ÿó9ŸÀg}ÖgñÙŸýÙ¼(~û·›×y×á~¶¹ßoÿöoó:¯ó:¼Ök½¿ýÛ¿Í¿ÆñãǹtéõWÅK¿ôKs¿ßþíßæu^çux­×z-~û·›ãÇséÒ%^ú¥_š¿ú«¿â…yÈC­·ÞÊ{½×{ñÞïýÞ¼Î뼯õZ¯Åoÿöoó?‘$žŸ÷{í窫þ­§÷ؼn—s W]õïUçg^úéÜýÇ⪫þ#Üøêçî?~$9®ºêßëš—~:—n½†õî&W]õïuüáwÓVûwœæª«þ½6¯»Èìø!žpWýïóí¿õv\uÕUWý/€l›«®úê³?û³ùœÏù>ë³>‹ÏþìÏæEñÝßýݼÏû¼¯õZ¯Åoÿöos¿ßþíßæu^çux­×z-~û·›·~ë·æg~ægøª¯ú*>ú£?šûýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛ¼÷{¿7ßó=ßÀÓŸþtüàóüüõ_ÿ5/ó2/À_ýÕ_±»»Ëë¼ÎëðZ¯õZüöoÿ6ÿIâùy¿×þq®ºêßjqzÍëv9÷÷·pÕUÿ^u>p楟ÎÝü(®ºê?¯þxîþãG’S᪫þ½®yé§séÖkXïnrÕUÿ^Ç~7mÕ±Çi®ºêßkóº‹ÌŽrá 7qÕÿ>ßþ[oÇUW]uÕÿȶ¹êªÿ§>û³?›ÏùœÏà³>ë³øìÏþlþ%»»»¼Ì˼ ·Þz+õQÅWõWs¿ßþíßæu^çux­×z-~û·›ÏþìÏæs>çsø¬Ïú,>û³?›ûýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛüôOÿ4oó6oÀW}ÕWñÑýÑçs>€?øÁ<øÁæùë¿þkvwwy ¯úª¯â£?ú£y ßþíßæu^çux­×z-~û·›ÏþìÏæs>çsx­×z-~û·›ûýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛ¼÷{¿7ßó=ßÀÓŸþtüàó@ý×Í˼ÌËðWõW¼ôK¿4¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6ÿIâùy¿×þq®ºêßjqzÍëv9÷÷·pÕUÿ^u>p楟ÎÝü(®ºê?¯þxîþãG’S᪫þ½®yé§séÖkXïnrÕUÿ^Ç~7mÕ±Çi®ºêßkóº‹ÌŽrá 7qÕÿ>ßþ[oÇUW]uÕÿȶ¹êªÿ§>û³?›ÏùœÏáßâ«¾ê«øèþhžÛoÿöoó:¯ó:¼Ök½¿ýÛ¿Í¿Ægögó9Ÿó9¼Ök½¿ýÛ¿Íý~û·›×y×àµ^ëµøíßþm~ú§š·y›·à«¾ê«øèþhè£?ú£ùš¯ùô që­·ðÛ¿ýÛ¼Î뼯õZ¯Åoÿöoó_aww—ÏùœÏỿû»ÙÝÝåßêý^ûǹꪫÅé=6¯ÛåÜßßÂUWý{ÕùÀ™—~:wÿñ£¸êªÿ7¾úã¹ûIN…«®ú÷ºæ¥ŸÎ¥[¯a½»ÉUWý{øÝ´UÇþ§¹êª¯Íë.2;~È…'ÜÄUÿû|ûo½W]uÕUÿ Ûæª«þŸúìÏþl>çs>‡ÅK½ÔKñà?˜×~í׿½ßû½9~ü8ÏÏoÿöoó:¯ó:¼Ök½¿ýÛ¿Í¿Ægögó9Ÿó9¼Õ[½?ýÓ?Íý~û·›×y×àµ^ëµøíßþmîwüøq.]ºÄK¿ôKóWõW<ÐCòn½õV>ê£>Нþê¯à·û·y×y^ëµ^‹ßþíßæ¿ÂGôGó5_ó5ü{½ßkÿ8W]õoµ8½Çæu»œûû[¸êª¯:8óÒOçî?~W]õáÆWp楟ÎÝü(®ºê?¯þxîþãG’S᪫þ½®yé§séÖkXïnrÕUÿ^Ç~7mÕ±Çi®ºêßkóº‹ÌŽrá 7qÕÿ>ßþ[oÇUW]uÕÿȶ¹êªÿ§>û³?›ÏùœÏà³>ë³øìÏþlþ½~û·›×y×àµ^ëµøíßþm^T·Þz+yÈC¸ßÓŸþtüàs¿ßþíßæu^çux­×z-~û·›:~ü8—.]â¥_ú¥ù«¿ú+Nœ8Áîî._õU_ÅGôGs¿ßþíßæu^çux­×z-~û·›ÿ ýÑÍ×|Í×ðïõ~¯ýã\uÕ¿Õâô›×írîïo᪫þ½ê|àÌK?»ÿøQ\uÕ„_ýñÜýÇ$§ÂUWý{]óÒOçÒ­×°ÞÝ䪫þ½Ž?ünÚªcÿŽÓ\uÕ¿×æu™?äÂnâªÿ}¾ý·ÞŽ«®ºêªÿmsÕUÿO}ög6Ÿó9ŸÀg}ÖgñÙŸýÙü{ýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛ¼¨Þû½ß›ïùžïà­Þê­øéŸþiè·û·y×y^ëµ^‹ßþíßæÞû½ß›ïùžïàéO:ý×ÍÛ¼ÍÛðô§??øÁÜï·û·y×y^ëµ^‹ßþíßæ¿ÊGôGóÝßýÝ\ºt‰«÷{í窫þ­§÷ؼn—s W]õïUçg^úéÜýÇ⪫þ#Üøêçî?~$9®ºêßëš—~:—n½†õî&W]õïuüáwÓVûwœæª«þ½6¯»Èìø!žpWýïóí¿õv\uÕUWý/€l›«®úê³?û³ùœÏù>ë³>‹ÏþìÏæßë·û·y×y^ëµ^‹ßþíßæEñÓ?ýÓ¼ÍÛ¼ ÷û­ßú-^ûµ_›úíßþm^çu^€×z­×â·û·y ŸþéŸæmÞæmøª¯ú*þú¯ÿšïùžïá¥^ê¥øë¿þkè·û·y×y^ëµ^‹ßþíßæ"Içsø¬Ïú,>û³?›¯ßþíßæu^çuxQ;vŒ·~ë·æ½ßû½yí×~m^˜ßþíßæu^çux­×z-~û·›ççÁ~0ÏxÆ3ø®ïú.Þû½ß›çöÛ¿ýÛ¼Î뼯õZ¯Åoÿöoó?‘$žŸ÷{í窫þ­§÷ؼn—s W]õïUçg^úéÜýÇ⪫þ#Üøêçî?~$9®ºêßëš—~:—n½†õî&W]õïuüáwÓVûwœæª«þ½6¯»Èìø!žpWýïóí¿õv\uÕUWý/€l›«®úêÖ[oåÖ[oàÁ~0~ðƒù÷ÚÝÝå¯ÿú¯yQ<øÁæÁ~0/ªÝÝ]þú¯ÿ€ãÇóÒ/ýÒp楟ÎÝü(®ºê?¯þxîþãG’S᪫þ½®yé§séÖkXïnrÕUÿ^Ç~7mÕ±Çi®ºêßkóº‹ÌŽrá 7qÕÿ>ßþ[oÇUW]uÕÿȶ¹êª«®úJÏÏû½ösÕUÿV‹Ó{l^·Ë¹¿¿…«®ú÷ªó3/ýtîþãGqÕUÿn|õÇs÷?’œ W]õïuÍK?K·^Ãzw“«®ú÷:þð»i«žý;NqÕUÿ^›×]dvü O¸‰«þ÷ùößz;®ºêª«þ@¶ÍUW]uÕÿP’x~Þﵜ«®ú·ZœÞcóº]Îýý-\uÕ¿Wœyé§s÷?Š«®úpã«?ž»ÿø‘äT¸êª¯k^úé\ºõÖ»›\uÕ¿×ñ‡ßM[õìßqŠ«®ú÷Ú¼î"³ã‡\xÂM\õ¿Ï·ÿÖÛqÕUW]õ¿²m®ºêª«þ‡’Äóó~¯ýã\uÕ¿Õâô›×írîïo᪫þ½ê|àÌK?»ÿøQ\uÕ„_ýñÜýÇ$§ÂUWý{]óÒOçÒ­×°ÞÝ䪫þ½Ž?ünÚªgÿŽS\uÕ¿×æu™?äÂnâªÿ}¾ý·ÞŽ«®ºêªÿmsÕUW]õ?”$žŸ÷{í窫þ­§÷ؼn—s W]õïUçg^úéÜýÇ⪫þ#Üøêçî?~$9®ºêßëš—~:—n½†õî&W]õïuüáwÓV=ûwœâª«þ½6¯»Èìø!žpWýïóí¿õv\uÕUWý/€l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Iê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾äK¾„ú¨b>ŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Iê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾äK¾„ú¨b>ŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Iê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾äK¾„ú¨b>ŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Iê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾äK¾„ú¨b>ŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Iê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾äK¾„ú¨b>ŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿ÕŸøDþú¯ÿšwz§w⪫þ½.]ºÄ÷|Ï÷ð‘ù‘\uÕ„/û²/ã#>â#˜Ïç\uÕ¿×÷~ï÷ò¯ñ<ä!᪫þ½~õW•^ù•_™«®ú÷ú›¿ùn½õVÞê­ÞŠ«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Iê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾äK¾„ú¨b>ŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Iê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾äK¾„ú¨b>ŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Iê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾äK¾„ú¨b>ŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Iê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾äK¾„ú¨b>ŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Iê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾äK¾„ú¨b>ŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñü¼ßkÿ8W]õoµ8½Çæu»œûû[ø¯ö¨—:Í'|õkqÕÿ»»»|÷w7ýÑÍUWýGø’/ù>ê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~Þﵜ«®ú·ZœÞcóº]Îýý-üW{ÔKæ¾úµ¸êÿŽÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žŸ÷{í窫þ­§÷ؼn—s ÿÕõR§ù„¯~-®ú¿cww—ïþîïæ£?ú£¹êªÿ_ò%_ÂG}ÔG1ŸÏ¹êª¯ïùžïáµ^ëµxðƒÌUWý{ýʯü ÇŽã•_ù•¹êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏÏû½ösÕUÿV‹Ó{l^·Ë¹¿¿…ÿjz©Ó|ÂW¿Wýß±»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕÿŸó9ŸÃ¿ÆK¿ôKóZ¯õZ?~œ«þ÷’Äóó~¯ýã\uÕ¿Õâô›×írîïoá¿Ú£^ê4ŸðÕ¯ÅUÿwìîîòÝßýÝ|ôG4W]õáK¾äKø¨ú(æó9W]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ù•_áØ±c¼ò+¿2W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6W]õ¿€$þ-Þû½ß›¯úª¯âøñãüoð×ý×¼Ïû¼õWÅU ‰ççý^ûǹꪫÅé=6¯ÛåÜßßµG½Ôi>á«_‹«þïØÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêIü[½ôK¿4¿õ[¿ÅñãÇùŸì³?û³ùœÏùlsHâùy¿×þq®ºêßjqzÍëv9÷÷·ð_íQ/ušOøê×âªÿ;vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ú_@÷û­ßú-^ûµ_›çç¯ÿú¯ÙÝÝ廿û»ùžïùî÷^ïõ^|÷w7ÿ“½ök¿6¿ó;¿€m®IŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUÿ Hâ~¿õ[¿Åk¿ökó/ùê¯þj>æc>†û=ýéOçÁ~0ÿS½ök¿6¿ó;¿€m®IŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUÿ Hâ~¿õ[¿Åk¿ökó¢xðƒÌ3žñ ¾ê«¾Šþèæª×~í׿w~çw°ÍU ‰ççý^ûǹꪫÅé=6¯ÛåÜßßµG½Ôi>á«_‹«þïØÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêIÜï·~ë·xí×~m^ïýÞïÍ÷|Ï÷ðYŸõY|ög6ÏÏîî.?ó3?í·ÞÊ_ÿõ_óà?˜ãÇóÚ¯ýÚ¼Ök½/ªÝÝ]~æg~†[o½•¿þë¿æÁ~0~ðƒy­×z-^ú¥_šçç¯ÿú¯¹téýÑÍ_ÿõ_ðÛ¿ýÛ;vŒ—~é—àÖ[oåÏx¯õZ¯À÷|Ï÷ðÓ?ýÓ¼ôK¿4¯õZ¯ÅK¿ôKó7ó7;vŒ—~é—æ_ò;¿ó;;vŒ—~é—æIŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUÿ Hâ~¿õ[¿Åk¿ökó¢øìÏþl>çs>€Ïú¬Ïâ³?û³ynŸó9ŸÃWõW³»»ËóóÚ¯ýÚ|ÕW}/ýÒ/Í ó9Ÿó9|õW5»»»çs>Û¼Ì˼ ý×Í}ÕW}ó1Àñãǹxñ"/ÌOÿôOó6oó6|ÔG}_ýÕ_Íÿ$’x~Þﵜ«®ú·ZœÞcóº]Îýý-üW{ÔKæ¾úµ¸êÿŽÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«þÄý~ë·~‹×~í׿EñÚ¯ýÚüÎïü_õU_ÅGôGó@ïó>ïÃw÷ws¿=èA<øÁà¯ÿú¯¹téÇ竾ê«xï÷~ožŸ·y›·á§ú§¹ßƒô üàð×ý×\ºt €—~é—æ§~ê§xðƒÌý^ûµ_›ßùßáùy­×z-~û·€ÏþìÏæs>çsø¬Ïú,>çs>‡çvñâEÞû½ß›Ÿù™Ÿà§~ê§xë·~k^÷~ï÷æ{¾ç{xúӟ΃ü`þ'‘Äóó~¯ýã\uÕ¿Õâô›×írîïoá¿Ú£^ê4ŸðÕ¯ÅUÿwìîîòÝßýÝ|ôG4W]õáK¾äKø¨ú(æó9W]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ù•_áØ±c¼ò+¿2W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6W]õ¿€$î÷[¿õ[¼ök¿6ÿ’ßþíßæu^çu¸ß_ýÕ_ñÒ/ýÒÜï£?ú£ùš¯ùŽ;ÆOÿôOóÚ¯ýÚÜoww—¯þê¯æs>çs8~ü8¿õ[¿ÅK¿ôKó@ýÑÍ×|Í×ð =ˆïþîïæµ_ûµ¹ßîî.ýÑÍ÷|Ï÷ðÒ/ýÒüÕ_ýÏíµ_ûµùßùlóÜ>û³?›ÏùœÏá~ÇŽã£?ú£yí×~mn½õVþú¯ÿš¯þê¯æ§ú§y›·yÞë½Þ‹ïþîïæùÙÝÝåĉ¼ÔK½ý×Íÿ4’x~Þﵜ«®ú·ZœÞcóº]Îýý-üW{ÔKæ¾úµ¸êÿŽÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«þÄý~ë·~‹×~í׿…ùžïù>ú£?šÝÝ]^ëµ^‹ßþíßæ~·Þz+yÈC8vì¿ýÛ¿ÍK¿ôKóü|ög6Ÿó9ŸÀ{½×{ñÝßýÝÜïÖ[oå!yÇŽã¯ÿú¯yðƒÌóóÖoýÖüÌÏü ßõ]ßÅ{¿÷{ó@¯ýÚ¯ÍïüÎï`›çöÙŸýÙ|Îç|÷û­ßú-^ûµ_›ççøñã\ºt €‹/rüøqžÛw÷wó>ïó>|×w}ïýÞïÍÿ4’x~Þﵜ«®ú·ZœÞcóº]Îýý-üW{ÔKæ¾úµ¸êÿŽÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«þÄý^ú¥_šãÇó‚üöoÿ6tìØ1þú¯ÿš?øÁÜï«¿ú«ù˜ù>ê£>Нþê¯æ…9~ü8—.]àâÅ‹?~€þèæk¾ækø¬Ïú,>û³?›äÖ[oå!yoõVoÅOÿôOó@¯ýÚ¯ÍïüÎï`›çöÙŸýÙ|Îç|/õR/Å_ÿõ_ó‚|ôG4_ó5_Àw}×wñÞïýÞ<·×y×á·û·¸xñ"ÇçIŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUÿ Hâßâ¥^ê¥øîïþn^ú¥_šzí×~m~çw~€ßú­ßâµ_ûµyaÞû½ß›ïùžïà§~ê§xë·~k^ûµ_›ßùßà¯þê¯xé—~i^˜ãÇséÒ%ló@¯ýÚ¯ÍïüÎï`›çöÙŸýÙ|Îç|õQÅWõWó‚üõ_ÿ5/ó2/À[½Õ[ñÓ?ýÓ<Э·ÞÊCòÞë½Þ‹ïþîïæ¿Âîî.Ÿó9ŸÃw÷w³»»Ë¿Õû½ösÕUÿV‹Ó{l^·Ë¹¿¿…ÿjz©Ó|ÂW¿Wýß±»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕÿ’xQ½Ök½/ýÒ/Ík¿ökóÖoýÖæcø©Ÿú)Þú­ßšÿ ýÑÍ×|Í×ðïõ~¯ýã\uÕ¿Õâô›×írîïoá¿Ú£^ê4ŸðÕ¯ÅUÿwìîîòÝßýÝ|ôG4W]õáK¾äKø¨ú(æó9W]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ù•_áØ±c¼ò+¿2W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6W]õ¿€$î÷[¿õ[¼ök¿6ÿ’¸Ÿmþ%¿ýÛ¿Íë¼ÎëðYŸõY|ög6’ø·ú­ßú-^ûµ_›û½ök¿6¿ó;¿€mžÛgögó9Ÿó9üÔOýoýÖoÍ óÕ_ýÕ|ÌÇ| _õU_ÅGôGs¿‡<ä!Üzë­<èAâÖ[oå¿Ê‰'ØÝÝåßëý^ûǹꪫÅé=6¯ÛåÜßßµG½Ôi>á«_‹«þïØÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêIÜï·~ë·xí×~mþ=$q?ÛüK~û·›×y×à³>ë³øìÏþl$pìØ1^ú¥_š¯þê¯æ¥_ú¥¹ßk¿ökó;¿ó;Øæ¹}ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ¼0»»»œ8q€—~é—æ¯þê¯øë¿þk^æe^€ú¨â«¿ú«ù¯rüøq.]ºÄ¿×û½ösÕUÿV‹Ó{l^·Ë¹¿¿…ÿjz©Ó|ÂW¿Wýß±»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕÿ’¸ßoýÖoñÚ¯ýÚü{<øÁæÏx¶ù—|ög6Ÿó9ŸÀg}ÖgñÙŸýÙHâ~¶ù÷xí×~m~çw~Û<·ÏþìÏæs>çsø­ßú-^ûµ_›É{¿÷{ó=ßó=üÕ_ý/ýÒ/ÍGôGó5_ó5<ýéOçÁ~0ÿU>ú£?š¯ùš¯áßëý^ûǹꪫÅé=6¯ÛåÜßßµG½Ôi>á«_‹«þïØÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêIÜï·~ë·xí×~mþ=^ûµ_›ßùßà·~ë·xí×~m^˜÷~ï÷æ{¾ç{ø©Ÿú)Þú­ß€—~é—æoþæoxúӟ΃ü`þ­^ûµ_›ßùßÀ6Ïí³?û³ùœÏù~ë·~‹×~í׿_òÓ?ýÓ¼ÍÛ¼ õQÅWõWó‡<„[o½•—z©—â¯ÿú¯ù¯öÑýÑ|÷w7—.]âßêý^ûǹꪫÅé=6¯ÛåÜßßµG½Ôi>á«_‹«þïØÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêIÜï·~ë·xí×~mþ=¾ú«¿šù˜à£>ê£øê¯þj^˜'N°»» ÀÓŸþtüàðÑýÑ|Í×| ŸõYŸÅgögó‚ìîîò‡<„ãÇóà?˜ßú­ßâ^ûµ_›ßùßÀ6Ïí³?û³ùœÏù~ë·~‹×~í׿Eñà?˜g<ã¼ôK¿4ßõ]ßÅ˼ÌËð]ßõ]¼÷{¿7ÿ“Iâùy¿×þq®ºêßjqzÍëv9÷÷·ð_íQ/ušOøê×âªÿ;vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ú_@÷û­ßú-^ûµ_›[o½•‡<ä!?~œßú­ßâ¥_ú¥y~>û³?›ÏùœÏàµ^ëµøíßþmîwë­·ò‡<€ãÇóWõW<øÁæùyŸ÷y¾û»¿€·z«·â§ú§y ×~í׿w~çw°ÍsûìÏþl>çs>€ßú­ßâµ_ûµyQ|ôG4_ó5_À{½×{ñ=ßó=\¼x‘ãÇó?™$žŸ÷{í窫þ­§÷ؼn—s ÿÕõR§ù„¯~-®ú¿cww—ïþîïæ£?ú£¹êªÿ_ò%_ÂG}ÔG1ŸÏ¹êª¯ïùžïáµ^ëµxðƒÌUWý{ýʯü ÇŽã•_ù•¹êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êªÿ$q¿ßú­ßâµ_ûµù÷úèþh¾æk¾€ãÇóS?õS¼ök¿6÷ÛÝÝåk¾ækøìÏþlŽ;ÆoÿöoóÒ/ýÒ<Ð{¿÷{ó=ßó=?~œŸú©Ÿâµ_ûµ¹ßîî._ó5_Ãgögs¿¿ú«¿â¥_ú¥y ×~í׿w~çwøìÏþlÞê­Þ €—~é—à³?û³ùœÏù~ë·~‹×~í׿Eqë­·ò‡<„z¯÷z/¾û»¿›ÿé$ñü¼ßkÿ8W]õoµ8½Çæu»œûû[ø¯ö¨—:Í'|õkqÕÿ»»»|÷w7ýÑÍUWýGø’/ù>ê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUWý/ ‰ûýÖoý¯ýگ̈́÷~ï÷æ{¾ç{¸ßƒü`üàð×ý×ìîîpìØ1¾ú«¿š÷~ï÷æ¹íîîòÚ¯ýÚüÍßü ÷{ðƒ̃ü`þú¯ÿšÝÝ]î÷]ßõ]¼÷{¿7Ïí£?ú£ùš¯ùèøñã\¼x€ÏþìÏæs>çsø­ßú-^ûµ_›ÕK¿ôKó7ó7Üï§~ê§xë·~kþ§“Äóó~¯ýã\uÕ¿Õâô›×írîïoá¿Ú£^ê4ŸðÕ¯ÅUÿwìîîòÝßýÝ|ôG4W]õáK¾äKø¨ú(æó9W]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ù•_áØ±c¼ò+¿2W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6W]õ¿€$î÷[¿õ[¼ök¿6ÿQ>û³?›¯þê¯æÒ¥K窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«þøíßþmî÷Ò/ýÒ?~œÿH»»»üôOÿ4·Þz+ý×̓ü`Ž?Î[¿õ[óÒ/ýÒ¼¨vwwùéŸþin½õVþú¯ÿšãÇóà?˜—~é—æ­ßú­yQüôOÿ4ý× Àƒü`Þú­ßšãÇsë­·rë­·ðÒ/ýÒ?~œÕ_ÿõ_ó2/ó2|ÔG}_ýÕ_Íÿ’x~Þﵜ«®ú·ZœÞcóº]Îýý-üW{ÔKæ¾úµ¸êÿŽÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ú?ï³?û³ùœÏùžþô§óà?˜ÿ $ñü¼ßkÿ8W]õoµ8½Çæu»œûû[ø¯ö¨—:Í'|õkqÕÿ»»»|÷w7ýÑÍUWýGø’/ù>ê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]õÞCòn½õV^ëµ^‹ßþíßæ IŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUWýŸö>ïó>|÷w7ßõ]ßÅ{¿÷{ó¿…$žŸ÷{í窫þ­§÷ؼn—s ÿÕõR§ù„¯~-®ú¿cww—ïþîïæ£?ú£¹êªÿ_ò%_ÂG}ÔG1ŸÏ¹êª¯ïùžïáµ^ëµxðƒÌUWý{ýʯü ÇŽã•_ù•¹êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«þOùë¿þkÞç}Þ‡ãÇsë­·rë­·ðR/õRüõ_ÿ5ÿ›Hâùy¿×þq®ºêßjqzÍëv9÷÷·ð_íQ/ušOøê×âªÿ;vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêÿI<бcÇøíßþm^ú¥_šÿM$ñü¼ßkÿ8W]õoµ8½Çæu»œûû[ø¯ö¨—:Í'|õkqÕÿ»»»|÷w7ýÑÍUWýGø’/ù>ê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]õÎK¿ôKó7ó7¼Õ[½ŸýÙŸÍK¿ôKó¿$žŸ÷{í窫þ­§÷ؼn—s ÿÕõR§ù„¯~-®ú¿cww—ïþîïæ£?ú£¹êªÿ_ò%_ÂG}ÔG1ŸÏ¹êª¯ïùžïáµ^ëµxðƒÌUWý{ýʯü ÇŽã•_ù•¹êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏÏû½ösÕUÿV‹Ó{l^·Ë¹¿¿…ÿjz©Ó|ÂW¿Wýß±»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰ççý^ûǹꪫÅé=6¯ÛåÜßßµG½Ôi>á«_‹«þïØÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóó~¯ýã\uÕ¿Õâô›×írîïoá¿Ú£^ê4ŸðÕ¯ÅUÿwìîîòÝßýÝ|ôG4W]õáK¾äKø¨ú(æó9W]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ù•_áØ±c¼ò+¿2W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6W]uÕUÿCIâùy¿×þq®ºêßjqzÍëv9÷÷·ð_íQ/ušOøê×âªÿ;vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñü¼ßkÿ8W]õoµ8½Çæu»œûû[ø¯ö¨—:Í'|õkqÕÿ»»»|÷w7ýÑÍUWýGø’/ù>ê£>Šù|ÎUWý{}Ï÷|¯õZ¯Åƒü`®ºêßëW~åW8vì¯üʯÌUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—|É—ðQõQÌçs®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ò+¿Â±cÇxåW~e®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾äK¾„ú¨b>ŸsÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷ú•_ùŽ;Æ+¿ò+sÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð%_ò%|ÔG}óùœ«®ú÷úžïù^ëµ^‹?øÁ\uտׯüʯpìØ1^ù•_™«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ù’/á£>꣘Ïç\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~åW~…cÇŽñʯüÊ\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|É—| õQÅ|>窫þ½¾ç{¾‡×z­×âÁ~0W]õïõ+¿ò+;vŒW~åW檫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IÃUW}Ï÷|¯õZ¯Åƒü`®ºêßëW~åWØÙÙáU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏÏû½ösÕUÿV‹Ó{l^·Ë¹¿¿…«®ú÷ªó3/ýtîþãGqÕUÿn|õÇs÷?’œ ÿVŸðU¯É£^ú W]õ=ßó=¼Ök½~ðƒ¹êª¯_ù•_agg‡Wy•W᪫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Ië³x ïþîïæÏxŸõYŸÅÿe’x~Þﵜ«®ú·ZœÞcóº]Îýý-\uÕ¿Wœyé§s÷?Š«®úpã«?ž»ÿø‘äTø·ú„¯zMõÒg¸êªïùžïáµ^ëµxðƒÌUWý{ýʯü ;;;¼Ê«¼ W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6WýŸòÙŸýÙ|Îç|¿õ[¿Åk¿öksÕÿ>¯ýÚ¯ÍïüÎï`›zí×~m~çw~Ûü_&‰ççý^ûǹꪫÅé=6¯ÛåÜßßÂUWý{ÕùÀ™—~:wÿñ£¸êªÿ7¾úã¹ûIN…«Oøª×äQ/}†«®úžïù^ëµ^‹?øÁ\uտׯüʯ°³³Ã«¼Ê«pÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕÿ)ŸýÙŸÍç|Îçð[¿õ[¼ök¿6WýïóÚ¯ýÚüÎïü¶y ×~í׿w~çw°Íÿe’x~Þﵜ«®ú·ZœÞcóº]Îýý-\uÕ¿Wœyé§s÷?Š«®úpã«?ž»ÿø‘äTø·ú„¯zMõÒg¸êªïùžïáµ^ëµxðƒÌUWý{ýʯü ;;;¼Ê«¼ W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6WýŸòÝßýÝ|÷w7_ýÕ_ÍK¿ôKsÕÿ>ýÑÍ_ÿõ_ðÛ¿ýÛ<Ðk¿ökó;¿ó;Øæÿ2Içs>€ßú­ßâw~çwøìÏþlžŸ÷~ï÷櫾ê«8~ü8Ïí³?û³ùœÏù~ë·~‹×~í׿~ŸýÙŸÍç|Îç`›ù˜á«¿ú«y~>ú£?š¯úª¯â?Êç|ÎçðÕ_ýÕìîîòü¼õ[¿5ßõ]ßÅñãÇy~>æc>†¯þê¯æyí×~m¾ê«¾Š—~é—æ¹ýöoÿ6¯ó:¯ÀoýÖo±»»Ëû¼Ïû°»»Ës{í×~m~ê§~ŠãÇóÝßýÝ|ÌÇ| »»»<·÷~ï÷滾ë»xnŸýÙŸÍç|ÎçpñâEÞç}Þ‡ŸþéŸæùùèþh¾ê«¾Šççµ_ûµùßùló@¯ýÚ¯ÍïüÎï`›çç¯ÿú¯ù˜ù~û·›ççøñã|ÕW}ïýÞïÍÿd’x~Þﵜ«®ú·ZœÞcóº]Îýý-\uÕ¿Wœyé§s÷?Š«®úpã«=ž»ÿô‘äXø·ú„¯zMõÒg¸êªïùžïáµ^ëµxðƒÌUWý{ýê¯þ*ÛÛۼʫ¼ W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6WýðÙŸýÙ|Îç|/ýÒ/Í_ÿõ_ðR/õR¼õ[¿5·Þz+?ýÓ?Í¥K—xé—~i~ë·~‹ãÇó@ŸýÙŸÍç|Îçð[¿õ[¼ök¿6÷ûìÏþl>çs>€÷~ï÷滿û»x«·z+^ú¥_š[o½•ŸþéŸæÒ¥K¼ôK¿4¿õ[¿ÅñãÇù÷x›·y~ú§šû½ÔK½Çàw~çw¸ßK¿ôKó[¿õ[?~œûíîîò:¯ó:üõ_ÿ5÷{©—z)Ž?ÀïüÎïp¿ãÇó[¿õ[¼ôK¿4ôÛ¿ýÛ¼Îë¼õQÅ×|Í×pìØ1^ú¥_š¿þë¿æÒ¥KÜï½Þë½xí×~mÞç}Þ‡û½Ök½·Þz+ÏxÆ3¸ßg}ÖgñÙŸýÙ<Ðgögó9Ÿó9¼õ[¿5?ýÓ? À[½Õ[ñÒ/ýÒüõ_ÿ5?ó3?ÃýÞû½ß›ïú®ïâ¹½ök¿6¿ó;¿€mèµ_ûµùßùlóÜþú¯ÿš×y×aww—û½Ök½»»»üÍßü ÷{ï÷~o¾ë»¾‹ÿ©$ñü¼ßkÿ8W]õoµ8½Çæu»œûû[¸êª¯:8óÒOçî?~W]õáÆW{û³?›ÏùœÏá~ÇŽã§ú§yí×~mîwë­·òÖoýÖüÍßü ŸõYŸÅgögóoõÙŸýÙ|Îç|zЃøîïþn^ûµ_›ûÝzë­¼õ[¿5ó7Àg}ÖgñÙŸýÙÜïmÞæmøéŸþi^ê¥^Šïþîïæ¥_ú¥¹ßîî.ýÑÍ÷|Ï÷püøqžþô§süøqî÷Û¿ýÛ¼Îë¼ôU_õU|ôG4÷ûîïþnÞç}Þ‡:vìßýÝßÍ[¿õ[s¿þèæk¾ækxðƒÌÓŸþtè³?û³ùœÏùîwìØ1~û·›—~é—æ~·Þz+oýÖoÍßüÍßð]ßõ]¼÷{¿7ôÚ¯ýÚüÎïü¶y ×~í׿w~çw°Ííîîò‡<„ÝÝ]>ê£>ŠÏþìÏæøñãÜï·û·yë·~k.]ºÀOýÔOñÖoýÖüO$‰ççý^ûǹꪫÅé=6¯ÛåÜßßÂUWý{ÕùÀ™—~:wÿñ£¸êªÿ7¾Úã¹ûOIŽ…«Oøª×äQ/}†«®úžïù^ëµ^‹?øÁ\uտׯþ꯲½½Í«¼Ê«pÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕÿŸýÙŸÍç|Îçp¿Ÿú©Ÿâ­ßú­yn»»»¼ôK¿4ÏxÆ3xúӟ΃ü`î÷ÙŸýÙ|Îç|¿õ[¿Åk¿öks¿ÏþìÏæs>çs¸ßoýÖoñÚ¯ýÚ<·ÝÝ]üàséÒ%Ž?ÎÓŸþtŽ?οÖîî.yÈCØÝÝ娱cüõ_ÿ5~ðƒyn»»»<øÁæÒ¥K?~œ‹/ðÛ¿ýÛ¼Îë¼ÇŽãÖ[oåøñãæc>€¯úª¯â£?ú£¹ßgögó9Ÿó9üÖoý¯ýÚ¯Íý>û³?›ÏùœÏà­Þê­øéŸþi^þèæk¾ækø®ïú.Þû½ß›­ïþîïæ}Þç}ø¨ú(¾ú«¿šä½ßû½ùë¿þkŽ?Îw÷wóà?˜÷~ï÷æ{¾ç{ø®ïú.Þû½ß›dww—'Npüøq.^¼Èý~û·›×y×àØ±cìîîòü|ög6Ÿó9ŸÀ[½Õ[ñÓ?ýÓçs>€÷z¯÷⻿û»yAÞû½ß›ïùžïà§~ê§xë·~kî÷Ú¯ýÚüÎïü¶y ×~í׿w~çw°Í8q‚ÝÝ].^¼ÈñãÇyAÞû½ß›ïùžïà§~ê§xë·~kþ§‘Äóó~¯ýã\uÕ¿Õâô›×írîïo᪫þ½ê|àÌK?»ÿøQ\uÕ„_íñÜý§$Ç¿Õ'|Õkò¨—>ÃUW}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹ê„ÏþìÏæs>çsøª¯ú*>ú£?šäÖ[oå!y¯õZ¯Åoÿöos¿ÏþìÏæs>çsø­ßú-^ûµ_›û}ög6Ÿó9ŸÀOýÔOñÖoýÖ¼ ¿ýÛ¿Íë¼ÎëðVoõVüôOÿ4ÿZoýÖoÍÏüÌÏð[¿õ[¼ök¿6ÿyÈC¸õÖ[¸xñ"Çç…yí×~m~çw~€¿ú«¿â¥_ú¥øíßþm^çu^€×z­×â·û·y~>û³?›ÏùœÏà³>ë³øìÏþlžŸÏþìÏæs>çsø­ßú-^ûµ_›û}ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ¼ ?ýÓ?ÍÛ¼ÍÛðYŸõY|ög6÷{í×~m~çw~Û<Ðk¿ökó;¿ó;Øæ~¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6/Ìw÷wó>ïó>|Ög}ŸýٟͶÝÝ]>çs>‡ïþîïfww—«÷{í窫þ­§÷ؼn—s W]õïUçg^úéÜýÇ⪫þ#Üøjçî?}$9þ­>á«^“G½ô®ºê{¾ç{x­×z-üàsÕUÿ^¿ú«¿Êöö6¯ò*¯ÂUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUÿ#|ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ¼0’xðƒÌÓŸþtî÷ÙŸýÙ|Îç|¿õ[¿Åk¿öks¿ÏþìÏæs>çsø«¿ú+^ú¥_šF¯õZ¯ÅoÿöoðÝßýÝ<ãÏàyЃÄ{¿÷{ðÚ¯ýÚüÎïü/^äøñãükHàØ±cìîîò/ùìÏþl>çs>€Ÿú©Ÿâ­ßú­øíßþm^çu^€÷z¯÷⻿û»y~>û³?›ÏùœÏà³>ë³øìÏþlžŸÏþìÏæs>çsø­ßú-^ûµ_›û}ög6Ÿó9ŸÀÅ‹9~ü8/È_ÿõ_ó2/ó2¼Ök½¿ýÛ¿Íý^ûµ_›ßùßÀ6ôÚ¯ýÚüÎïü¶¹ßgögó9Ÿó9¼ök¿6¯ýÚ¯Í së­·òÝßýݼÖk½¿ýۿͶþèæk¾ækø÷z¿×þq®ºêßjqzÍëv9÷÷·pÕUÿ^u>p楟ÎÝü(®ºê?¯öxîþÓG’cáßê¾ê5yÔKŸáª«¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«looó*¯ò*\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\õ?Âgögó9Ÿó9üÖoý¯ýÚ¯Í #‰ûÙæ~ŸýÙŸÍç|Îçð[¿õ[¼ök¿6÷ûìÏþl>çs>ÛüK$ðà?˜§?ýé¼ök¿6¿ó;¿Ã òZ¯õZüöoÿ6/ó2/Ã_ÿõ_`›-I¼Ök½¿ýÛ¿Í¿ä³?û³ùœÏù>ë³>‹ÏþìÏà·û·y×y>ë³>‹ÏþìÏæùùìÏþl>çs>€Ïú¬Ïâ³?û³y~>û³?›ÏùœÏà·~ë·xí×~mî÷ÙŸýÙ|Îç|¶ù—Hàµ^ëµøíßþmî÷Ú¯ýÚüÎïü¶y ×~í׿w~çw°Íý>û³?›ÏùœÏáßâµ^ëµøíßþmþ³8q‚ÝÝ]þ½Þﵜ«®ú·ZœÞcóº]Îýý-\uÕ¿Wœyé§s÷?Š«®úpã«=ž»ÿô‘äXø·ú„¯zMõÒg¸êªïùžïáµ^ëµxðƒÌUWý{ýê¯þ*ÛÛۼʫ¼ W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6WýðÙŸýÙ|Îç|¿õ[¿Åk¿ökóÂHâ~¶¹ßgögó9Ÿó9üÖoý¯ýÚ¯Íý>û³?›ÏùœÏÀ6ÿI¼ÔK½ý× Àk¿ökó;¿ó;¼ ¯õZ¯Åoÿöoðà?˜g<ãØæ_K¯õZ¯Åoÿöoó/ùìÏþl>çs>€Ïú¬Ïâ³?û³øíßþm^çu^€Ïú¬Ïâ³?û³y~>û³?›ÏùœÏà³>ë³øìÏþlžŸÏþìÏæs>çsø­ßú-^ûµ_›û}ög6Ÿó9Ÿ€mþ%’x­×z-~û·›û½ök¿6¿ó;¿€mèµ_ûµùßùls¿ÏþìÏæs>çsxЃăü`^T/ýÒ/ÍWõWóŸíøñã\ºt‰¯÷{í窫þ­§÷ؼn—s W]õïUçg^úéÜýÇ⪫þ#Üøjçî?}$9þ­>á«^“G½ô®ºê{¾ç{x­×z-üàsÕUÿ^¿ú«¿Êöö6¯ò*¯ÂUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUÿ#|ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ¼0’x©—z)þú¯ÿšû}ög6Ÿó9ŸÀoýÖoñÚ¯ýÚÜï³?û³ùœÏùžþô§óà?˜dww—'NðZ¯õZüöoÿ6ßýÝßÍ­·ÞÊ òà?˜÷~ï÷àµ_ûµùßùló¯% €?øÁ<ýéOç_òÙŸýÙ|Îç|ßõ]ßÅ{¿÷{ðÛ¿ýÛ¼Î뼟õYŸÅgögóü|ög6Ÿó9ŸÀg}ÖgñÙŸýÙçs>Û¼0¿ýÛ¿Íë¼ÎëðVoõVüôOÿ4÷{í×~m~çw~Û<Ðk¿ökó;¿ó;Øæ~ŸýÙŸÍç|ÎçðYŸõY|ög6ÿÓ|ôG4_ó5_ÿ×û½ösÕUÿV‹Ó{l^·Ë¹¿¿…«®ú÷ªó3/ýtîþãGqÕUÿn|µÇs÷Ÿ>’ ÿVŸðU¯É£^ú W]õ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_e{{›Wy•W᪫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæªÿ>û³?›ÏùœÏ໾ë»xï÷~o^¿þë¿æe^æex«·z+~ú§šû}ög6Ÿó9ŸÀoýÖoñÚ¯ýÚÜï³?û³ùœÏù~ë·~‹×~í׿ùíßþm^çu^€÷z¯÷⻿û»ù×zë·~k~æg~€ßú­ßâµ_ûµyA~û·›ÏùœÏáµ_ûµy­×z-^ûµ_›—~é—æoþæo¸xñ"Çç…yí×~m~çw~€ßú­ßâµ_ûµøíßþm^çu^€Ïú¬Ïâ³?û³y~>û³?›ÏùœÏà³>ë³øìÏþlžŸÏþìÏæs>çsø­ßú-^ûµ_›û}ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ¼ ßýÝßÍû¼ÏûðYŸõY|ög6÷{í×~m~çw~Û<Ðk¿ökó;¿ó;Øæ~¿ýÛ¿Íë¼ÎëðVoõVüôOÿ4ÿ}ôG4ßýÝßÍ¥K—ø·z¿×þq®ºêßjqzÍëv9÷÷·pÕUÿ^u>p楟ÎÝü(®ºê?¯öxîþÓG’cáßê¾ê5yÔKŸáª«¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«looó*¯ò*\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\õ?Âgögó9Ÿó9¼Õ[½?ýÓ?Í òÙŸýÙ|Îç|ßõ]ßÅ{¿÷{s¿ÏþìÏæs>çsø­ßú-^ûµ_›û}ög6Ÿó9ŸÀ{½×{ñÝßýݼ ïýÞïÍ÷|Ï÷ðS?õS¼õ[¿5ÿZ_ýÕ_ÍÇ|ÌÇðQõQ|õW5/ÈGôGó5_ó5üÔOýoýÖoÍ{¿÷{ó=ßó=|×w}ïýÞïÍ rë­·ò‡<„ûÙæ~¿ýÛ¿Íë¼ÎëðYŸõY|ög6ÏÏgögó9Ÿó9|Ög}ŸýÙŸÍóóÙŸýÙ|Îç|¿õ[¿Åk¿öks¿ÏþìÏæs>çsø¬Ïú,>û³?›ä­ßú­ù™Ÿùþê¯þŠ—~é—æ~¯ýÚ¯ÍïüÎï`›zí×~m~çw~ÛÜoww—'Npüøqžþô§süøq^ÏþìÏæs>çsxé—~iÞë½Þ‹þèæIçs>‡ûýÖoý¯ýÚ¯ÍsÛÝÝå!y»»»;vŒ[o½•ãÇs¿ÏþìÏæs>çsø­ßú-^ûµ_›û}ög6Ÿó9ŸÃýþê¯þŠ—~é—æ¹ýõ_ÿ5/ó2/À±cÇØÝÝåßbww—'Npüøqžþô§süøqžÛ­·ÞÊ˼Ì˰»»Ë±cǸõÖ[9~ü8ý×Í˼ÌËpüøqžþô§süøqžŸ·y›·á§ú§x¯÷z/¾û»¿›ûýöoÿ6¯ó:¯Àg}ÖgñÙŸýÙû³?›ÏùœÏàøñã<ýéOçøñã<·ßþíßæu^çuxЃÄ­·Þʽök¿6¿ó;¿€mèµ_ûµùßùló@ïýÞïÍ÷|Ï÷ðÞïýÞ|×w}ÏÏîî.yÈCØÝÝà·~ë·xí×~mþ§‘Äóó~¯ýã\uÕ¿Õâô›×írîïo᪫þ½ê|àÌK?»ÿøQ\uÕ„_íñÜý§$Ç¿Õ'|Õkò¨—>ÃUW}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹ê„ÏþìÏæs>çs¸ßñãÇù©Ÿú)^ûµ_›ûýõ_ÿ5ïó>ïÃ_ÿõ_ðU_õU|ôG4ôÙŸýÙ|Îç|¿õ[¿Åk¿öks¿ÏþìÏæs>çs¸ßñãÇù­ßú-^ú¥_šûýöoÿ6oó6oÃîî.?õS?Å[¿õ[óoõÑýÑ|Í×| /ýÒ/Íw}×wñÒ/ýÒÜïÖ[oåmÞæmøë¿þk>ë³>‹ÏþìÏæ~ïýÞïÍ÷|Ï÷ðÒ/ýÒ|×w}/ýÒ/Íývwwù˜ù¾û»¿€cÇŽqë­·rüøqî÷Û¿ýÛ¼Î뼟õYŸÅgögóü|ög6Ÿó9ŸÀg}ÖgñÙŸýÙçs>‡û½ôK¿4ßõ]ßÅK¿ôKs¿ßþíßæmÞæmØÝÝà·~ë·xí×~mèµ_ûµùßùló@¯ýÚ¯ÍïüÎï`›ºõÖ[yé—~i.]ºÀ{¿÷{óU_õU?~œûÝzë­¼ÍÛ¼ ý× Àk½ÖkñÛ¿ýÛüO$‰ççý^ûǹꪫÅé=6¯ÛåÜßßÂUWý{ÕùÀ™—~:wÿñ£¸êªÿ7¾Úã¹ûOIŽ…«Oøª×äQ/}†«®úžïù^ëµ^‹?øÁ\uտׯþ꯲½½Í«¼Ê«pÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕÿŸýÙŸÍç|ÎçpìØ1.]ºÀK¿ôKsüøqvwwùë¿þkî÷^ïõ^|÷w7Ïí³?û³ùœÏù~ë·~‹×~í׿~ŸýÙŸÍç|ÎçpìØ1.]ºÀK¿ôKsüøqvwwùë¿þkî÷^ïõ^|÷w7ÿ^¯ýÚ¯ÍïüÎïp¿—~é—æøñãüöoÿ6÷{­×z-~û·›ÚÝÝåµ_ûµù›¿ùî÷Ò/ýÒ?~€ßþíßæ~ÇŽã·û·yé—~iè·û·y×y>ë³>‹ÏþìÏæùùìÏþl>çs>€Ïú¬Ïâ³?û³y~>û³?›ÏùœÏà·~ë·xí×~mî÷ÙŸýÙ|Îç|ÇŽãÒ¥K¼ök¿6»»»üõ_ÿ5÷ûª¯ú*>ú£?šçöÚ¯ýÚüÎïü¶y ×~í׿w~çw°ÍsûéŸþiÞû½ß›K—.q¿×~í׿~¿ýÛ¿Íý^ê¥^ŠßþíßæøñãüO$‰ççý^ûǹꪫÅé=6¯ÛåÜßßÂUWý{ÕùÀ™—~:wÿñ£¸êªÿ7¾Úã¹ûOIŽ…«Oøª×äQ/}†«®úžïù^ëµ^‹?øÁ\uտׯþ꯲½½Í«¼Ê«pÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕÿŸýÙŸÍç|ÎçðYŸõYìîîò5_ó5<·cÇŽñÑýÑ|ög6ÏÏgögó9Ÿó9üÖoý¯ýÚ¯Íý>û³?›ÏùœÏà§~ê§øîïþn~æg~†çvìØ1¾ú«¿š÷~ï÷æ?Êgögó9Ÿó9¼ õQÅgögsüøqžÛîî.ŸýÙŸÍ×|Í×ð‚¼Ök½ßýÝß̓ü`žÛoÿöoó:¯ó:|Ög}ŸýÙŸÍóóÙŸýÙ|Îç|ŸõYŸÅgögóü|ög6Ÿó9ŸÀoýÖoñÚ¯ýÚÜï³?û³ùœÏù~ê§~Нþê¯æw~çwxnzЃøìÏþlÞû½ß›ççµ_ûµùßùló@¯ýÚ¯ÍïüÎï`›çç¯ÿú¯ùèþh~çw~‡ä­Þê­øîïþnŽ?ÎÿT’x~Þﵜ«®ú·ZœÞcóº]Îýý-\uÕ¿Wœyé§s÷?Š«®úpã«=ž»ÿô‘äXø·ú„¯zMõÒg¸êªïùžïáµ^ëµxðƒÌUWý{ýê¯þ*ÛÛۼʫ¼ W]õïõ×ý×Üzë­¼õ[¿5W]uÕUW]õŸÙ6WýðÙŸýÙ|Îç|ŸõYŸÅgögó×ý×üôOÿ4ý×ÍK¿ôKsüøqÞû½ß›ãÇó‚Üzë­Üzë­¼ôK¿4Çç~ŸýÙŸÍç|Îçð[¿õ[¼ök¿6ý×ÍOÿôOó×ý×¼ôK¿4~ðƒyë·~kŽ?δ[o½•ßþíßæÖ[oå·û·yé—~iüàóÖoýÖ<øÁæ_rë­·òÛ¿ýÛÜzë­üõ_ÿ5/ýÒ/ÍñãÇyë·~küàó‚ìîîò×ý×<øÁæÁ~0ÏÏ­·ÞÊ­·Þ Àƒü`üàóüÜzë­Üzë­¼ôK¿4Çç~ŸýÙŸÍç|Îçð[¿õ[¼ök¿6¿ýÛ¿Íoÿöoó×ý×¼ôK¿4~ðƒyï÷~o^˜¿þë¿fww€×~í׿þú¯ÿšÝÝ]^ûµ_›æ¯ÿú¯ùéŸþivwwùë¿þk^ûµ_›ãÇóÖoýÖ<øÁæ:Içs>€Ïú¬Ïâ³?û³ùöÙŸýÙ|Îç|¿õ[¿Åk¿öksÕ¬ÏþìÏæs>çsø­ßú-^ûµ_›«þí$ñü¼ßkÿ8W]õoµ8½Çæu»œûû[¸êª¯:8óÒOçî?~W]õáÆW{ë³>‹ÏþìÏæ?Úgögó9Ÿó9üÖoý¯ýÚ¯ÍUÿ±>û³?›ÏùœÏà·~ë·xí×~m®ú·“Äóó~¯ýã\uÕ¿Õâô›×írîïo᪫þ½ê|àÌK?»ÿøQ\uÕ„_íñÜý§$Ç¿Õ'|Õkò¨—>ÃUW}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹ê„ÏþìÏæs>çsø¬Ïú,>û³?›ÿhŸýÙŸÍç|Îçð[¿õ[¼ök¿6WýÇúìÏþl>çs>€ßú­ßâµ_ûµ¹êßNÏÏû½ösÕUÿV‹Ó{l^·Ë¹¿¿…«®ú÷ªó3/ýtîþãGqÕUÿn|µÇs÷Ÿ>’ ÿVŸðU¯É£^ú W]õ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_e{{›Wy•W᪫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæªÿ>û³?›ÏùœÏà³>ë³øìÏþlþ£}ög6Ÿó9ŸÀoýÖoñÚ¯ýÚ\õë³?û³ùœÏù~ë·~‹×~í׿ª;Içs>€Ïú¬Ïâ³?û³ùöÙŸýÙ|Îç|¿õ[¿Åk¿öksÕ¬ÏþìÏæs>çsø­ßú-^ûµ_›«þí$ñü¼ßkÿ8W]õoµ8½Çæu»œûû[¸êª¯:8óÒOçî?~W]õáÆW{ÃUW}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏÏû½ösÕUÿV‹Ó{l^·Ë¹¿¿…«®ú÷ªó3/ýtîþãGqÕUÿn|µÇs÷Ÿ>’ ÿVŸðU¯É£^ú W]õ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_e{{›Wy•W᪫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IÃUW}Ï÷|¯õZ¯Åƒü`®ºêßëWõWÙÞÞæU^åU¸êª¯¿þë¿æÖ[oå­ßú­¹êª«®ºêªÿ$ȶ¹êª«®úJÏÏû½ösÕUÿV‹Ó{l^·Ë¹¿¿…«®ú÷ªó3/ýtîþãGqÕUÿn|µÇs÷Ÿ>’ ÿVŸðU¯É£^ú W]õ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_e{{›Wy•W᪫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(IüÃ?œ ®ºêßë{¾ç{x­×z-üàsÕUÿ^¿ú«¿ÊÖÖ¯úª¯ÊUWý{ýõ_ÿ5·Þz+oýÖoÍUW]uÕUWý'A¶ÍUW]uÕÿP’x~lsÕUÿVOxÂøë¿þkÞùß™«®ú÷ÚÝÝ廿û»ùèþh®ºê?—~é—òáþálllpÕUÿ^ßó=ßÃk½Ökñà?˜«®ú÷úÕ_ýU¶¶¶xÕW}U®ºêßë¯ÿú¯¹õÖ[yë·~k®ºêª«®ºê? ²m®ºêª«þ‡’Äóc›«®ú·zžÀ_ÿõ_óÎïüÎ\uÕ¿×îî.ßýÝßÍGôGsÕUÿ¾ôK¿”ÿðgccƒ«®ú÷úžïù^ëµ^‹?øÁ\uտׯþ꯲µµÅ«¾ê«rÕUÿ^ý×Í­·ÞÊ[¿õ[sÕUW]uÕUÿImsÕUW]õ?”$žÛ\uÕ¿Õžðþú¯ÿšw~çw檫þ½vwwùîïþn>ú£?š«®úð¥_ú¥|ø‡8\uÕ¿×÷|Ï÷ðZ¯õZ<øÁ檫þ½~õW•­­-^õU_•«®ú÷úë¿þkn½õVÞú­ßš«®ºêª«®úO‚l›«®ºêªÿ¡$ñüØæª«þ­žð„'ð×ý×¼ó;¿3W]õïµ»»Ëw÷wóÑýÑ\uÕ„/ýÒ/åÃ?üÃÙØØàª«þ½¾ç{¾‡×z­×âÁ~0W]õïõ«¿ú«lmmñª¯úª\uÕ¿×_ÿõ_së­·òÖoýÖ\uÕUW]uÕdÛ\uÕUWý%‰çÇ6W]õoõ„'<¿þë¿æßù¹êª¯ÝÝ]¾û»¿›þèæª«þ#|é—~)þáÎÆÆW]õïõ=ßó=¼Ök½~ðƒ¹êª¯_ýÕ_ekk‹W}ÕW媫þ½þú¯ÿš[o½•·~ë·æª«®ºêª«þ“ Ûæª«®ºê(Iú£?šú¨âEqë­·ò1ó1üôOÿ4ÿoýÖoÍg}ÖgñÒ/ýÒ¼(~ú§šù˜áÖ[oåßêøñã¼÷{¿7ŸõYŸÅñãÇù—ìîîò9Ÿó9|÷w7»»»ü[=øÁ櫾ê«xë·~k^ý×Íç|ÎçðÓ?ýÓü{¼õ[¿5_õU_Ńü`^oû¶oË/ýÒ/±Z­ø·:~ü8ïýÞïÍg}Ögqüøqþ%»»»|ÌÇ| ?ýÓ?Íîî.ÿV/ýÒ/Íg}ÖgñÖoýÖ¼(~û·›ù˜á¯ÿú¯ù÷xë·~k¾ê«¾Š?øÁ¼(>çs>‡ïþîïæÖ[oåßêÁ~0ïýÞïÍg}Ögñ¢¸õÖ[ù˜ù~ú§š—~é—æ«¾ê«xí×~m^Ÿò)ŸÂ×}Ý×qxxÈ¿ÕñãÇyë·~k¾ê«¾ŠãÇó¢øœÏù¾ú«¿šÝÝ]þ­üàóÑýÑ|ÔG}/Š[o½•ù˜á§ú§ù÷xí×~m¾ê«¾Š—~é—æEñÓ?ýÓ|Îç|ý×Í¿ÕñãÇyï÷~o>ë³>‹ãÇó/ÙÝÝås>çsøîïþnvwwù·zðƒÌg}ÖgñÞïýÞ¼(~äG~„ù˜áî»ïæßã­ßú­ù¬Ïú,^ú¥_šÅ×|Í×ðÕ_ýÕÜzë­ü[?~œ÷~ï÷æ³>ë³8~ü8ÿ’ÝÝ]>æc>†ŸþéŸfww—«—~é—æ³>ë³xë·~k^¿ýÛ¿ÍÇ|ÌÇð×ý×ü{¼õ[¿5_õU_Ńü`^Ÿó9ŸÃw÷wsë­·òoõà?˜÷~ï÷æ³>ë³xQ<ãÏàU^åU¸ûî»ù÷xé—~i¾ê«¾Š×~í׿EñÛ¿ýÛ|ÌÇ| ý×Í¿ÕñãÇyë·~k¾ê«¾ŠãÇó¢øœÏù¾û»¿›[o½•«?øÁ|ôG4õQÅ‹âÖ[oåc>æcøéŸþiþ=^ûµ_›¯úª¯â¥_ú¥yQüôOÿ4Ÿó9ŸÃ_ÿõ_óouüøqÞú­ßš¯úª¯âøñãüKžñŒgð6oó6üýßÿ=ã8òoõà?˜þèæ£>ê£xQüõ_ÿ5Ÿó9ŸÃOÿôOóïñÖoýÖ|Ög}/ýÒ/Í‹âk¾ækøê¯þjn½õVþ­Ž?Î{¿÷{óYŸõY?~œÉîî.ó1ÃOÿôO³»»Ë¿ÕK¿ôKóYŸõY¼õ[¿5/Š¿þë¿æ}Þç}øë¿þkþ=Þú­ßš¯úª¯âÁ~0/Š·}Û·å—ù—Y.—ü[=øÁæ½ßû½ù¬Ïú,^»»»|ÌÇ| ßýÝßÍ¿ÇK¿ôKóU_õU¼ök¿6/Šßþíßæc>æcøë¿þkþ=Þû½ß›¯úª¯âøñã¼(>çs>‡ïþîïæÖ[oåßêÁ~0ýÑÍG}ÔGqÕUW]õ¿²m®ºêª«þ‡’ĤŸú©Ÿâ­ßú­yavwwy™—yn½õVþ#?~œ§?ýé?~œæ·û·y×yþ£¼õ[¿5?õS?Å¿ämÞæmøéŸþiþ£üÖoý¯ýÚ¯Í ³»»ËCòvwwùðà?˜¿ú«¿âøñã¼0Ÿò)ŸÂñóå£>ê£øê¯þjþ%¯ó:¯Ãoÿöoóå¯þê¯xé—~i^˜[o½•—y™—aww—ÿ/ýÒ/Í_ýÕ_ñ/ùê¯þj>æc>†ÿ(ŸõYŸÅgögó/y™—yþú¯ÿšÿÇç¯þê¯xðƒÌ ó#?ò#¼ó;¿3ÿQ^ûµ_›ßú­ßâ_òÙŸýÙ|Îç|ÿQ¾ë»¾‹÷~ï÷æ…ÙÝÝåe^æe¸õÖ[ùpüøqžþô§süøq^˜ßþíßæu^çuøòÖoýÖüÔOýÿ’þèæk¾ækøòS?õS¼õ[¿5/Ì3žñ ò‡`›ÿ~ðƒù«¿ú+Ž?Î óÓ?ýÓ¼ÍÛ¼ ÿQ>ê£>Нþê¯æ_ò6oó6üôOÿ4ÿQþê¯þŠ—~é—æ…¹õÖ[y™—yvwwùðÒ/ýÒüÕ_ýÿ’¯þê¯æc>æcøòYŸõY|ög6ÿ’ –Ë%ÿŽ?Î_ýÕ_ñà?˜æ¯ÿú¯y™—yþ£¼ök¿6¿õ[¿Å¿ä³?û³ùœÏùþ£|ÕW}ýÑÍ¿äe^æeøë¿þkþ#?~œ§?ýé?~œæ·û·y×yþ£¼õ[¿5?õS?Å¿äe_öeù«¿ú+þ£üÔOýoýÖoÍ ³»»ËCòvwwùðà?˜¿ú«¿âøñã¼0?ýÓ?ÍÛ¼ÍÛðå£>ê£øê¯þjþ%oó6oÃOÿôOóå·~ë·xí×~m^˜[o½•—y™—aww—ÿ/ýÒ/Í_ýÕ_ñ/y›·y~ú§šÿ(ŸõYŸÅgögó/y×y~û·›ÿ(õWÅK¿ôKóÂÜzë­<ä!á?Êk¿ökó[¿õ[üK>û³?›ÏùœÏá?ÊW}ÕWñÑýÑ\uÕUWý/„l›«®ºêªÿ¡$ñéµ^ëµøíßþm^˜ßþíßæu^çuøô]ßõ]¼÷{¿7/Ì{¿÷{ó=ßó=üG²Í¿Dÿ‘Þë½Þ‹ïþîïæ…ùîïþnÞç}Þ‡ÿH¿õ[¿Åk¿ökóœ8q‚ÝÝ]þ£?~œ‹/òÂìîîrâÄ þ#}Ög}ŸýÙŸÍ óÕ_ýÕ|ÌÇ| ÿ‘þê¯þŠ—~é—æ…y™—yþú¯ÿšÿ(/ýÒ/Í_ýÕ_ñÂüõ_ÿ5/ó2/äÏú¬Ïâ³?û³ya^çu^‡ßþíßæ?ÒÅ‹9~ü8/ÌCòn½õVþ£¼Ök½¿ýÛ¿Í óÛ¿ýÛ¼Îë¼ÿ‘¾ë»¾‹÷~ï÷æ…yï÷~o¾ç{¾‡ÿH¶ù—Hâ?Ò[½Õ[ñÓ?ýÓ¼0Ÿò)ŸÂñóé·~ë·xí×~m^˜×~í׿w~çwørüøq.^¼È ³»»Ë‰'øôQõQ|õW5/ÌWõWó1ó1üGú­ßú-^ûµ_›æµ_ûµùßùþ£¼ôK¿4õWÅ ó#?ò#¼ó;¿3ÿ‘>ë³>‹ÏþìÏæ…ùìÏþl>çs>‡ÿHOúÓyðƒÌ ó‡<„[o½•ÿ(¯õZ¯ÅoÿöoóÂüöoÿ6¯ó:¯Ã¤ïú®ïâ½ßû½ya>ú£?š¯ùš¯á?’mþ%’øôVoõVüôOÿ4/ÌOÿôOó6oó6üGú©Ÿú)Þú­ßšæ­ßú­ù™Ÿùþ£?~œ‹/ò/‘Ĥú¨â«¿ú«ya¾ú«¿šù˜á?ÒoýÖoñÚ¯ýÚ¼0'Nœ`ww—ÿ(~ðƒyúÓŸÎ së­·ò‡<„ÿHŸõYŸÅgögóÂ|ög6Ÿó9ŸÃ¤§?ýé<øÁæ…yÈC­·ÞÊ”×z­×â·û·¹êª«®ú_Ù6W]uÕUÿCIâ?Òk½ÖkñÛ¿ýÛ¼0¿ýÛ¿Íë¼Îëð黾ë»xï÷~o^˜÷~ï÷æ{¾ç{ød›‰$þ#½×{½ßýÝßÍ óÝßýݼÏû¼ÿ‘~ë·~‹×~í׿…9qâ»»»üG9vì»»»¼0»»»œ8q‚ÿHŸõYŸÅgögóÂ|õW5ó1ä¿ú«¿â¥_ú¥ya^ûµ_›ßùßá?ÊK½ÔKñ×ý×¼0ý×Í˼ÌËðé³>ë³øìÏþl^˜×y×á·û·ùtñâEŽ?Î óà?˜g<ãüGy­×z-~û·›æ·û·y×yþ#}×w}ïýÞïÍ óÞïýÞ|Ï÷|ÿ‘ló/‘Ĥ·z«·â§ú§yaÞùß™ù‘á?ÒoýÖoñÚ¯ýÚ¼0¯ýÚ¯ÍïüÎïð娱cìîîòÂìîîrâÄ þ#}ÔG}_ýÕ_Í óÕ_ýÕ|ÌÇ| ÿ‘~ë·~‹×~í׿…yí×~m~çw~‡ÿ(/õR/Å_ÿõ_óÂüðÿ0ïò.ï¤Ïú¬Ïâ³?û³ya>û³?›ÏùœÏá?ÒÓŸþtüàóÂ<øÁæÏxÿQ^ëµ^‹ßþíßæ…ùíßþm^çu^‡ÿHßõ]ßÅ{¿÷{óÂ|ôG4_ó5_Ã$ÛüK$ñé­Þê­øéŸþi^˜ŸþéŸæmÞæmøôS?õS¼õ[¿5/Ì[¿õ[ó3?ó3üG9vì»»»üK$ñé£>ê£øê¯þj^˜¯þê¯æc>æcøô[¿õ[¼ök¿6/̉'ØÝÝå?ʃô n½õV^˜[o½•‡<ä!üGú¬Ïú,>û³?›æ³?û³ùœÏùþ#=ýéOçÁ~0/ÌK¿ôKó7ó7üGy­×z-~û·›«®ºêªÿ…msÕUW]õ?”$þ#ýÔOýoýÖoÍ ³»»ËK¿ôKóŒg<ƒÿÇŽãÖ[oåøñã¼0¿ýÛ¿Íë¼Îëðå½Þë½øîïþnþ%oýÖoÍÏüÌÏðå·~ë·xí×~m^˜ÝÝ]üàséÒ%þ#<èAâ¯ÿú¯9~ü8/ÌK¼ÄKð÷ÿ÷üGù¨ú(¾ú«¿šÉk¿ökó;¿ó;üGù«¿ú+^ú¥_šæÖ[oå¥_ú¥¹téÿ^ê¥^Š¿þë¿æ_òÕ_ýÕ|ÌÇ| ÿQ¾ê«¾Šþèæ_òÒ/ýÒüÍßü ÿŽ;Æ_ÿõ_óà?˜æƒ?øƒù–oùþ£¼Ök½¿ýÛ¿Í¿ä³?û³ùœÏùþ£üÔOýoýÖoÍ ³»»ËK¿ôKóŒg<ƒÿÇŽãÖ[oåøñã¼0¿ýÛ¿Íë¼Îëðå­Þê­øéŸþiþ%ýÑÍ×|Í×ðå·~ë·xí×~m^˜OþäOæK¾äKøò =ˆ¿þë¿æøñã¼0?ýÓ?ÍÛ¼ÍÛðå£>ê£øê¯þjþ%oýÖoÍÏüÌÏðå¯þê¯xé—~i^˜[o½•—~é—æÒ¥KüGx©—z)þú¯ÿšÉWõWó1ó1üGù¬Ïú,>û³?›æ·û·y×yþ£;vŒ¿þë¿æÁ~0/Ì_ÿõ_ó2/ó2üGy­×z-~û·›Égögó9Ÿó9üGùª¯ú*>ú£?šÉK¿ôKó7ó7üG8vì·Þz+Çç…ùíßþm^çu^‡ÿ(oõVoÅOÿôOó/‘ĤŸú©Ÿâ­ßú­yavwwyé—~ižñŒgðáAzý×ÍñãÇya~ú§š·y›·á?ÊG}ÔGñÕ_ýÕüKÞú­ßšŸù™Ÿá?ÊoýÖoñÚ¯ýÚ¼0·Þz+/ýÒ/Í¥K—øðR/õRüõ_ÿ5ÿ’G<â<å)Oá?Êg}ÖgñÙŸýÙüK^ûµ_›ßùßá?Ê_ýÕ_ñÒ/ýÒ¼0·Þz+yÈCøòZ¯õZüöoÿ6ÿ’ÏþìÏæs>çsøòU_õU|ôG4W]uÕUÿ !Ûæª«®ºê(IüGxЃÄGôGóÑýѼ(n½õV>ú£?šŸù™Ÿáßã­Þê­øìÏþl^ú¥_šÅOÿôOóÙŸýÙüÍßü ÿVÇŽã½ßû½ùìÏþlŽ?οdww—ÏþìÏæ»¿û»¹téÿVzЃøê¯þjÞú­ßšÅ_ÿõ_óÙŸýÙüÌÏü ÿoõVoÅWõWóà?˜Ék¿ökó;¿ó;ü{;vŒ÷~ï÷æ«¿ú«yQìîîòÑýÑüôOÿ4—.]âßê¥^ê¥øìÏþlÞú­ßšÅoÿöoóÑýÑüÍßü ÿoõVoÅWõWóà?˜ÅgögóÝßýÝ<ãÏàßêAzïýÞïÍgögó¢¸õÖ[ùèþh~æg~†—z©—â«¿ú«yí×~mþ%ŸýÙŸÍç|ÎçðïuìØ1Þú­ßš¯þê¯æøñã¼(>û³?›¯þê¯æÒ¥Kü[=èAâ£?ú£ùèþh^·Þz+ýÑÍÏüÌÏðïñVoõV|ög6/ýÒ/Í‹â§ú§ùìÏþlþæoþ†«cÇŽñÞïýÞ|ög6Çç_²»»ËgögóÝßýÝ\ºt‰«=èA|õW5oýÖoÍ¿ä³?û³ùœÏùþ#¼Õ[½ŸýÙŸÍK¿ôKó¢øê¯þj¾ú«¿šg<ãü[;vŒ÷~ï÷æ³?û³9~ü8ÿ’ÝÝ]>ú£?šŸþéŸæÒ¥Kü[½ÔK½ŸýÙŸÍ[¿õ[ó¢øíßþm>ú£?š¿ù›¿áßã­Þê­øê¯þjüàó¢øìÏþl¾û»¿›g<ãü[=èAâ½ßû½ùìÏþlþ%¿ýÛ¿Íë¼Îëðá¥^ê¥øê¯þj^ûµ_›ÅoÿöoóÑýÑüÍßü ÿVÇŽã­ßú­ùê¯þjŽ?΋â³?û³ùîïþnžñŒgðoõ =ˆþèæ£?ú£yQÜzë­|ôG4?ó3?ÿÇk½ÖkñÕ_ýÕ¼ôK¿4/ŠŸþéŸæ³?û³ù›¿ùþ­Ž;Æ{¿÷{óÙŸýÙ?~œ‰$þ#<èAâ£?ú£ùèþh^ý×Ígögó3?ó3ü{¼Õ[½ŸýÙŸÍK¿ôKó¢øê¯þj¾ú«¿šg<ãü[;vŒ÷~ï÷æ³?û³9~ü8ÿ’ÝÝ]>ú£?šŸþéŸæÒ¥Kü[½ÔK½ŸýÙŸÍ[¿õ[ó¢øë¿þkÞû½ß›¿ù›¿áßã­Þê­øê¯þjüàó/yí×~m~çw~‡¯=èA¼÷{¿7ŸýÙŸÍ‹bww—þèæ§ú§¹téÿV/õR/ÅWõWóÚ¯ýÚ¼(~û·›þèæoþæoø÷x¯÷z/¾ú«¿šãÇó¢øìÏþl¾û»¿›g<ãü[=èAâ£?ú£ùèþh®ºêª«þ—B¶ÍUW]uÕÿP’x~lsÕUÿZ¯ýÚ¯ÍïüÎïðÜ~ë·~‹×~í׿ª«þ5>û³?›ÏùœÏá¹}Ög}ŸýÙŸÍUWýk|ög6Ÿó9ŸÃsû¬Ïú,>û³?›«®ú×øíßþm^çu^‡çöZ¯õZüöoÿ6W]õ¯%‰çÇ6W]õ¯õÚ¯ýÚüÎïüÏí·~ë·xí×~m®ºêª«®ºê?²m®ºêª«þ‡’Äóc›«®ú×zí×~m~çw~‡çö[¿õ[¼ök¿6W]õ¯ñÙŸýÙ|Îç|Ïí³>ë³øìÏþl®ºê_ã³?û³ùœÏùžÛg}ÖgñÙŸýÙ\uÕ¿Æoÿöoó:¯ó:<·×z­×â·û·¹êª-IçsxnŸõYŸÅgögsÕUÿŸýÙŸÍç|ÎçðÜ>ë³>‹ÏþìÏæª«þ5~û·›×y×á¹½Ök½¿ýÛ¿ÍUWýkIâù±ÍUWýk½ök¿6¿ó;¿Ãsû­ßú-^ûµ_›«®ºêª«®ú„l›«®ºêªÿ¡$ñüØæª«þµ^ûµ_›ßùßá¹ýÖoý¯ýÚ¯ÍUWýk|ög6Ÿó9ŸÃsû¬Ïú,>û³?›«®ú×øìÏþl>çs>‡çöYŸõY|ög6W]õ¯ñÛ¿ýÛ¼Îë¼Ïíµ^ëµøíßþm®ºê_KÏm®ºê_ëµ_ûµùßùžÛoýÖoñÚ¯ýÚ\uÕUW]uÕ dÛ\uÕUWý%‰çÇ6W]õ¯õÚ¯ýÚüÎïüÏí·~ë·xí×~m®ºê_ã³?û³ùœÏùžÛg}ÖgñÙŸýÙ\uÕ¿Ægögó9Ÿó9<·Ïú¬Ïâ³?û³¹êªßþíßæu^çuxn¯õZ¯ÅoÿöosÕUÿZ’x~lsÕUÿZ¯ýÚ¯ÍïüÎïðÜ~ë·~‹×~í׿ª«®ºêª«þ!Ûæª«®ºê(Ië³>‹ÏþìÏæª«þ5>û³?›ÏùœÏá¹}Ög}ŸýÙŸÍUWýküöoÿ6¯ó:¯Ãs{­×z-~û·›«®ú×’Äóc›«®ú×zí×~m~çw~‡çö[¿õ[¼ök¿6W]uÕUW]õÙ6W]uÕUÿCIâù±ÍUWýk½ök¿6¿ó;¿Ãsû­ßú-^ûµ_›«®ú×øìÏþl>çs>‡çöYŸõY|ög6W]õ¯ñÙŸýÙ|Îç|Ïí³>ë³øìÏþl®ºê_ã·û·y×yžÛk½ÖkñÛ¿ýÛ\uÕ¿–$žÛ\uÕ¿Ök¿ökó;¿ó;<·ßú­ßâµ_ûµ¹êª«®ºêªÿ@ȶ¹êª«®úêøñã\ºt‰:vì»»»\uÕ¿Ök¿ökó;¿ó;<·‹/rüøq®ºê_ã³?û³ùœÏùžÛoýÖoñÚ¯ýÚ\uÕ¿ÆOÿôOó6oó6<·Ïú¬Ïâ³?û³¹êª[o½•‡<ä!<··z«·â§ú§¹êª-I<·=èAÜzë­\uÕ¿Ök¿ökó;¿ó;<7Û\uÕUW]uÕ0dÛ\uÕUWýõÑýÑ|Í×| ôYŸõY|ög6W]õ¯õÓ?ýÓ¼ÍÛ¼ ôZ¯õZüöoÿ6W]õ¯õ×ý×¼Ì˼ ô =ˆ¿þë¿æøñã\uÕ¿Æîî.~ðƒ¹té÷;vìý×̓ü`®ºê_ëµ_ûµùßùè§~ê§xë·~k®ºê_ë£?ú£ùš¯ùè³>ë³øìÏþl®ºê_ë«¿ú«ù˜ùè­Þê­øéŸþi®ºêª«®ºê?²m®ºêª«þûèþh¾û»¿€þèæ³?û³¹êª«¯þê¯æ«¿ú«ÙÝÝå­ßú­ùê¯þjŽ?ÎUWý[üöoÿ6ýÑÍßüÍßðVoõV|õW5~ðƒ¹êª‹¿þë¿æ³?û³ù™Ÿù^ê¥^Нþê¯æµ_ûµ¹êª‹ÝÝ]>ú£?šïùžïáAzŸýÙŸÍ{¿÷{sÕUÿ»»»|ög6ßýÝßÍñãÇyï÷~o>û³?›«®ú·úê¯þj¾ú«¿šÝÝ]Þú­ßš¯þê¯æøñã\uÕUW]uÕ0dÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUWýÚÝÝåk¾ækøéŸþiþú¯ÿ€—~é—æµ_ûµù¨ú(üàóå·û·ùžïù~û·›[o½€×~í׿­ßú­y¯÷z/Ž?ÎUÿ»íîîò=ßó=üôOÿ4¿ýÛ¿ Àƒü`^ûµ_›÷z¯÷âµ_ûµùðÝßýÝ<ãÏàEñ =ˆ÷~ï÷æªÿ>û³?›ÏùœÏá³>ë³øìÏþlþ£ìîîò5_ó5üôOÿ4ý× ÀK¿ôKóÒ/ýÒ|Ög}~ðƒ¹êÿžŸþéŸæmÞæmx­×z-~û·›ÿ¿ýÛ¿ÍïüÎïð¢ú¬Ïú,®úßé{¾ç{øéŸþi~û·›ÝÝ]üàóÒ/ýÒ|ÔG}¯ýگ͔ïùžïá§ú§ùíßþmvww9~ü8¯ýÚ¯Í[¿õ[ó^ïõ^\õ¿ßoÿöoó=ßó=üöoÿ6·Þz+Ççµ_ûµyë·~kÞë½Þ‹ÿŸó9ŸÃ‹êµ^ëµxí×~m®úßoww—Ÿù™Ÿá»¿û»ùíßþmüàóÒ/ýÒ¼÷{¿7oõVoŤïùžïỿû»ùíßþmüàóÚ¯ýÚ¼×{½¯ýÚ¯ÍUW]uÕUW½È¶¹êª«®úò×ý×¼Îë¼»»»¼ ßõ]ßÅ{¿÷{óïõÕ_ýÕ|ÌÇ| /ȃü`~ê§~Š—~é—æªÿvwwy×yþú¯ÿšä£?ú£ùª¯ú*þ½$ñ¢z­×z-~û·›«þ÷ûë¿þk^æe^€Ïú¬Ïâ³?û³ùð×ý×¼Îë¼»»»¼ _õU_ÅGôGsÕÿ»»»<ä!aww—×z­×â·û·ùðÑýÑ|Í×| /*Û\õ¿Ëîî.¯ó:¯Ã_ÿõ_ó¼ök¿6?õS?ÅñãÇù÷xŸ÷y¾û»¿›äµ_ûµù©Ÿú)Ž?ÎUÿûìîîò>ïó>üôOÿ4/ÌK¿ôKó]ßõ]¼ôK¿4ÿV¿ýÛ¿Íë¼Îëð¢ú¬Ïú,>û³?›«þwûë¿þkÞæmÞ†[o½•äµ_ûµù©Ÿú)Ž?οÇîî.¯ó:¯Ã_ÿõ_ó‚¼÷{¿7ßõ]ßÅUW]uÕUW=ȶ¹êª«®úð×ý×¼Îë¼»»»¼Ök½oýÖoÍñãÇùíßþm¾ç{¾‡û}×w}ïýÞïÍ¿Õw÷wó>ïó>;vŒ÷~ï÷æµ_ûµ¹õÖ[ùéŸþi~çw~€ãÇóô§?ãÇsÕÿ.»»»¼Îë¼ý× Àk½ÖkñÖoýÖ<øÁæ·û·ùîïþn.]ºÀg}ÖgñÙŸýÙü[ýõ_ÿ5/ó2/Ëêµ^ëµøíßþm®úßí¯ÿú¯y×yvwwø¬Ïú,>û³?›¯ÝÝ]ò‡°»» Àk½ÖkñÖoýÖ?~œßþíßæ{¾ç{¸ßw}×wñÞïýÞ\õ¿ßîî.¯ó:¯Ã_ÿõ_ðZ¯õZüöoÿ6ÿ^ûµ_›ßùßáEe›«þ÷ØÝÝåu^çuøë¿þkŽ;Æ{¿÷{óà?€[o½•¯ùš¯á~/ýÒ/Í_ýÕ_ñoõ>ïó>|÷w7zЃxï÷~o^ûµ_›ßþíßæ»¿û»yÆ3žÀk¿ökó[¿õ[\õ¿ÏÛ¼ÍÛðÓ?ýÓÜï£>ê£xðƒÌñãǹõÖ[ùê¯þj.]ºÀñãÇyúÓŸÎñãÇù·øîïþnÞç}Þ‡Õg}ÖgñÙŸýÙ\õ¿×îî.yÈCØÝÝàAzïýÞïÍK¿ôKó×ý×|÷w7ÏxÆ3xé—~i~ë·~‹ãÇóo±»»Ëë¼Îëð×ý×<èAâ½ßû½yé—~i~û·›ïþîïæÒ¥K¼÷{¿7ßõ]ßÅUW]uÕUW=dÛ\uÕUWýx×y~û·€ïú®ïâ½ßû½y ßþíßæu^çu8~ü8OúÓ9~ü8ÿZ»»»<ä!aww—cÇŽñÛ¿ýÛ¼ôK¿4ôÞïýÞ|Ï÷|ïõ^ïÅw÷wsÕÿ.ŸýÙŸÍç|Îçð^ïõ^|÷w7ô×ý×¼ök¿6—.]àéO:~ðƒù·øéŸþiÞæmÞ€¯úª¯â£?ú£¹êÿ¶ŸþéŸæ}Þç}ØÝÝå~ŸõYŸÅgögóïõÞïýÞ|Ï÷|_õU_ÅGôGó@ý×Ík¿ökséÒ%Ž?ÎÓŸþtŽ?ÎUÿ{ýõ_ÿ5oó6oí·ÞÊý^ëµ^‹ßþíßæ?‚$^ëµ^‹ßþíßæªÿ[>ú£?š¯ùš¯à¥^ê¥øíßþmŽ?ÎÝzë­¼ök¿6ÏxÆ3ø¬Ïú,>û³?›­ßþíßæu^çux©—z)~û·›ãÇs¿ÝÝ]^ûµ_›¿ù›¿໾ë»xï÷~o®úß㻿û»yŸ÷yô ñÓ?ýÓ¼ôK¿4´»»Ëk¿ökó7ó7¼×{½ßýÝßÍ¿ÅGôGó5_ó5üÕ_ý/ýÒ/ÍUÿ·½÷{¿7ßó=ßÀ{½×{ñÝßýÝ<Ðîî.ïýÞïÍÏüÌÏðYŸõY|ög6ÿŸýÙŸÍç|Îçð^ïõ^|÷w7të­·òÚ¯ýÚ<ãÏà·~ë·xí×~m®ºêª«®ºêmsÕUW]õïôÛ¿ýÛ¼Î뼯õZ¯Åoÿöoóü|ög6Ÿó9ŸÀW}ÕWñÑýÑük}ög6Ÿó9ŸÀw}×wñÞïýÞë³øìÏþlþ=n½õVò‡ðZ¯õZüöoÿ6ÏÏw÷wó>ïó>|Ög}ŸýÙŸÍUÿ;}Í×| ýÑÍs{­×z-~û·›¯¿þë¿æe^æeø¨ú(¾ú«¿š«þo9qâ»»»;vŒ¿þë¿æÁ~0ÏÏoÿöoó:¯ó:<øÁæéO:ÿZ¯ó:¯Ãoÿöoðô§??øÁ<·ÝÝ]üàséÒ%üàóô§?«þ÷x™—yþú¯ÿ€ßú­ßâµ_ûµy~vwwyðƒÌ¥K—¸xñ"Çç_ëµ_ûµùßùlsÕÿm·Þz+yÈCxЃÄ_ÿõ_süøqžÛîî.'NœàÁ~0OúÓù·8qâ»»»;vŒ[o½•ãÇóÜ~û·›×y×à­Þê­øéŸþi®ºêª«®ºêmsÕUW]õïôÞïýÞ|Ï÷|?õS?Å[¿õ[óüìîîrâÄ ^ú¥_š¿ú«¿â_ëĉìîîrìØ1vwwyA~ú§š·y›·à«¾ê«øèþh®úßá«¿ú«ù˜ù>ë³>‹ÏþìÏæyé—~iþæoþ†ãÇsñâEþ-^ûµ_›ßùßÀ6WýßôÝßýÝ|ÌÇ| »»»;vŒ·~ë·æ{¾ç{ø¬Ïú,>û³?›þèæk¾ækø®ïú.Þû½ß›äøñã\ºt‰?øÁ<ýéOçªÿ]~û·›÷yŸ÷áÖ[oå~ŸõYŸÅç|ÎçðZ¯õZüöoÿ6ÿ^?ýÓ?ÍÛ¼ÍÛð]ßõ]¼÷{¿7WýßñÛ¿ýÛ¼Î뼯õZ¯ÅoÿöoóÂ<øÁæÏx¶ù×øë¿þk^æe^€·z«·â§ú§yA>ú£?š¯ùš¯à¯þê¯xé—~i®úßAzЃ¸õÖ[ya^ûµ_›ßùßà·~ë·xí×~mþµ$ðZ¯õZüöoÿ6WýßöÝßýݼÏû¼ïõ^ïÅw÷wó‚¼ôK¿4ó7€mþµ¾û»¿›÷yŸ÷à£>ê£øê¯þj^—~é—æoþæo¸xñ"Ç窫®ºêª«ž Ù6W]uÕUÿNyÈC¸õÖ[°Í óÚ¯ýÚüÎïü/^äøñ㼨þú¯ÿš—y™—à­Þê­øéŸþi^ÝÝ]Nœ8Àk½ÖkñÛ¿ýÛ\õ¿Ã[¿õ[ó3?ó3üÕ_ý/ýÒ/Í òÑýÑ|Í×| ¿õ[¿Åk¿ökó¯uâÄ vwwy©—z)þú¯ÿš«þozí×~m~çw~€×z­×⻿û»¹õÖ[y×y>ë³>‹ÏþìÏæßãe^æeøë¿þk.^¼ÈñãÇyAÞú­ßšŸù™Ÿà¯þê¯xé—~i®úßã³?û³ùœÏùô ñÝßýݼök¿6’x­×z-~û·›¯ÏþìÏæs>çsø«¿ú+^ú¥_š«þïøéŸþi¾ú«¿€×~í׿³?û³ya^ûµ_›ßùßàâÅ‹?~œÕWõWó1ó1|ÕW}ýÑÍ òÓ?ýÓ¼ÍÛ¼ ŸõYŸÅgögsÕÿ|ý×ÍGôGpüøq~ú§šæ£?ú£ùš¯ù~ë·~‹×~í׿_ãÖ[oå!yõQÅWõWsÕÿ}·Þz+·Þz+Çç¥_ú¥yA^æe^†¿þë¿À6ÿZïýÞïÍ÷|Ï÷ðS?õS¼õ[¿5/Ègögó9Ÿó9|×w}ïýÞïÍUW]uÕUW=²m®ºêª«þvww9qâ¯õZ¯ÅoÿöoóÂ|ôG4_ó5_ÀoýÖoñÚ¯ýÚ¼¨¾û»¿›÷yŸ÷à³>ë³øìÏþl^˜ãÇséÒ%lsÕÿyÈC¸õÖ[°Í óÕ_ýÕ|ÌÇ| ŸõYŸÅgög󯱻»Ë‰'x¯÷z/¾û»¿›ûýõ_ÿ5~ðƒ9~ü8Wýï÷Ú¯ýÚÜzë­|ög6ïýÞï Àoÿöoó:¯ó:|Ög}ŸýÙŸÍ¿‡$ô që­·òÂ|ög6Ÿó9ŸÀOýÔOñÖoýÖ\õ¿ÇgögóÕ_ýÕ|ôG4ýÑÍñãÇÀk½ÖkñÛ¿ýÛü{½ök¿6¿ó;¿€mîwë­·²»»ËK¿ôKsÕÿ’¸Ÿmþ5Þû½ß›ïùžïà·~ë·xí×~m^¿þë¿æe^æex­×z-~û·›«þïyí×~m~çw~€ßú­ßâµ_ûµù×øéŸþiÞæmÞ€ïú®ïâ½ßû½¹ßïüÎïðZ¯õZ\õÿÓ­·ÞÊCòô që­·ò¯õÚ¯ýÚüÎïü/^äøñã¼ ¿ýÛ¿Íë¼ÎëðYŸõY|ög6W]uÕUW]õLȶ¹êª«®úwøíßþm^çu^€×z­×â·û·ya>û³?›ÏùœÏà«¾ê«øèþh^TŸýÙŸÍç|Îçð]ßõ]¼÷{¿7/Ìk¿ökó;¿ó;<ýéOçÁ~0WýÏ' €cÇŽ±»»Ë óÛ¿ýÛ¼Îë¼ïõ^ïÅw÷wó¯ñÛ¿ýÛ¼Îë¼_õU_ʼn'øê¯þjþú¯ÿšû?~œ·~ë·æ«¾ê«8~ü8WýïôÛ¿ýÛ¼ök¿6ôÛ¿ýÛ¼Î뼟õYŸÅgögóouë­·ò‡<€×z­×â·û·ya¾ú«¿šù˜à³>ë³øìÏþl®úßãÖ[oåøñã?~œ’Àk½ÖkñÛ¿ýÛü{8q‚ÝÝ]^ëµ^‹ÏþìÏæk¾ækøéŸþiè­ßú­ù¨ú(^ûµ_›«þïúîïþnÞç}Þ€—z©—â¯ÿú¯ù×xí×~m~çw~€¿ú«¿â¥_ú¥ya$ðà?˜§?ýé\õË_ÿõ_ó2/ó2;vŒÝÝ]þµ>û³?›ÏùœÏà·~ë·øßù¾û»¿›[o½•û=øÁæ½ßû½ù¬Ïú,®úÿáÖ[oåmÞæmøë¿þk~ê§~Š·~ë·æ_K÷³Í óÛ¿ýÛ¼Î뼯õZ¯ÅoÿöosÕUW]uÕUÏ„l›«®ºêª‡ßþíßæu^çuø¬Ïú,>û³?›æ«¿ú«ù˜ù>ë³>‹ÏþìÏæEõÙŸýÙ|Îç|¿õ[¿Åk¿ökó¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ\õ?Ÿ$^ëµ^‹ßþíßæ…ùíßþm^çu^€×z­×â·û·ù×øìÏþl>çs>€ãdz»»Ë rüøq~ê§~Š×~í׿ªÿ~û·›×y×à³>ë³øìÏþlþ­~û·›×y×àµ^ëµøíßþm^˜ßþíßæu^çuø¬Ïú,>û³?›«þ÷“Àk½ÖkñÛ¿ýÛü{ìîîrâÄ Ž?Îîî./ÌGôGóU_õU\õÏîî.yÈCØÝÝ໾ë»xï÷~oþ5^ûµ_›ßùßÀ6ÿIÜÏ6Wýßò:¯ó:üöoÿ6ŸõYŸÅgögó¯õÚ¯ýÚüÎïüÇgww—ä¥_ú¥ù©Ÿú)üàsÕÿ=¿ó;¿Ã_ÿõ_ó×ý×|÷w7÷û¨ú(¾ú«¿š I<èAâÖ[oå…ÙÝÝåĉ¼Ök½¿ýÛ¿ÍUW]uÕUW=²m®ºêª«þ~û·›×y×à³>ë³øìÏþl^˜ßþíßæu^çuø¬Ïú,>û³?›Õk¿ökó;¿ó;üÖoý¯ýÚ¯Í óÚ¯ýÚüÎïü¿õ[¿Åk¿öksÕÿlý×Í˼ÌËðZ¯õZüöoÿ6/Ìoÿöoó:¯ó:¼Ök½¿ýÛ¿Í¿Æ[¿õ[ó3?ó3ÜïAzïýÞïÍK¿ôKðÛ¿ýÛ|÷w7—.]àøñãüÖoý/ýÒ/ÍUÿûýöoÿ6¯ó:¯Àg}ÖgñÙŸýÙü[ýöoÿ6¯ó:¯À[½Õ[ñÓ?ýÓ¼0¿ýÛ¿Íë¼ÎëðYŸõY|ög6Wýï' €×z­×â·û·ù÷øíßþm^çu^‡û;vŒ·~ë·æµ_ûµyðƒÌ_ÿõ_óÓ?ýÓüÎïü÷û¨ú(¾ú«¿š«þoy×y~û·€×z­×â·û·ù×:qâ»»»Øæ_"‰ûÙæªÿ;Þç}Þ‡ïþîïàØ±cÜzë­?~œ­'N°»»Ëý^ëµ^‹×~í׿µ_ûµ¹õÖ[ùíßþm¾ç{¾‡û½ôK¿4¿õ[¿ÅñãǹêÿI<·¯úª¯â£?ú£ù·’Àk½ÖkñÛ¿ýÛüK$ðZ¯õZüöoÿ6W]uÕUW]õLȶ¹êª«®úwøíßþm^çu^€Ïú¬Ïâ³?û³ya~û·›×y×à³>ë³øìÏþl^T¯ýÚ¯ÍïüÎïð[¿õ[¼ök¿6/Ìk¿ökó;¿ó;üÖoý¯ýÚ¯ÍUÿ³ýöoÿ6¯ó:¯Àk½ÖkñÛ¿ýÛ¼0»»»œ8q€×z­×â·û·ù×x™—yþú¯ÿ€÷z¯÷⻿û»yn»»»¼ök¿6ó7Àk¿ökó[¿õ[\õ¿ßoÿöoó:¯ó:|Ög}ŸýÙŸÍ¿Õoÿöoó:¯ó:|Ög}ŸýÙŸÍ óÛ¿ýÛ¼Îë¼oõVoÅOÿôOsÕÿ~’x­×z-~û·›ÏþìÏæs>çsx©—z)¾û»¿›—~é—æ¹}õW5ó1Ãýþê¯þŠ—~é—æªÿÞç}Þ‡ïþîïàØ±cÜzë­?~œ-IÜÏ6ÿ’?øÁ<ãÏÀ6WýßðÕ_ýÕ|ÌÇ| ÷û«¿ú+^ú¥_š­ÝÝ]Nœ8Áý¾ë»¾‹÷~ï÷æ¹ýõ_ÿ5¯ýگͥK—x¯÷z/¾û»¿›«þï¸õÖ[yÈCƒô žñŒgð@¯ýÚ¯Íw}×wñà?˜-I¼Ök½¿ýÛ¿Í¿D¯õZ¯ÅoÿöosÕUW]uÕUÏ„l›«®ºêª‡ßþíßæu^çuø¬Ïú,>û³?›æ·û·y×y>ë³>‹ÏþìÏæEõÖoýÖüÌÏü ¿õ[¿Åk¿ökó¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ\õ?Û­·ÞÊCò^ëµ^‹ßþíßæ…ùíßþm^çu^€×z­×â·û·ù×øíßþmn½õVvwwùèþh^ÝÝ]üàséÒ%~ë·~‹×~í׿ªÿÝ~û·›×y×à³>ë³øìÏþlþ­~û·›×y×à½Þë½øîïþn^˜ßþíßæu^çuø¨ú(¾ú«¿š«þ÷“Àk½ÖkñÛ¿ýÛü{Üzë­Üzë­üöoÿ6ïýÞï̓ü`^·~ë·æg~ægx¯÷z/¾û»¿›«þwÛÝÝå}Þç}øéŸþiŽ;ÆoÿöoóÒ/ýÒü[<øÁæÏx¶ù—Hâ~¶¹ê¿÷yŸ÷ỿû»¹ßw}×wñÞïýÞü[ìîîò×ý×üõ_ÿ5~ðƒyë·~k^ßþíßæu^çu¸Ÿm®ú¿cww—ãÇs¿¿þë¿æ£?ú£ùßùŽ?Î_ýÕ_ñà?˜ I¼Ök½¿ýÛ¿Í¿D¯õZ¯ÅoÿöosÕUW]uÕUÏ„l›«®ºêª‡ßþíßæu^çuø¬Ïú,>û³?›滿û»yŸ÷y>ë³>‹ÏþìÏæEõÙŸýÙ|Îç|¿õ[¿Åk¿ökó¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ\õ?Ÿ$^ëµ^‹ßþíßæ…ùíßþm^çu^€×z­×â·û·ùÏòÑýÑ|Í×| ŸõYŸÅgögsÕÿn¿ýÛ¿Íë¼ÎëðYŸõY|ög6ÿV¿ýÛ¿Íë¼ÎëðZ¯õZüöoÿ6/Ìoÿöoó:¯ó:|Ög}ŸýÙŸÍUÿûIàµ^ëµøíßþmþ«üõ_ÿ5/ó2/Àñãǹxñ"Wýïµ»»Ëë¼Îëð×ý×;vŒßþíßæ¥_ú¥ù·zí×~m~çw~ÛüK$q?Û\õ¿×îî.ó1Ãw÷ws¿ïú®ïâ½ßû½ù¯òÒ/ýÒüÍßü ¿õ[¿Åk¿öksÕÿm¯ýÚ¯ÍïüÎïð^ïõ^|÷w7ÿ’x­×z-~û·›‰$^ëµ^‹ßþíßæª«®ºêª«ž Ù6W]uÕUÿ¿ýÛ¿Íë¼Îëð^ïõ^|÷w7/Ìgögó9Ÿó9|Ög}ŸýÙŸÍ‹ê³?û³ùœÏù~ë·~‹×~í׿…yí×~m~çw~€§?ýé<øÁæªÿù$ðÒ/ýÒüÕ_ý/Ìoÿöoó:¯ó:¼Õ[½?ýÓ?Í–ŸþéŸæmÞæmx«·z+~ú§š«þwûíßþm^çu^€Ïú¬Ïâ³?û³ù·úë¿þk^æe^€×z­×â·û·ya~ú§š·y›·à³>ë³øìÏþl®úßO¯õZ¯Åoÿöoó_I÷³ÍUÿ;ýõ_ÿ5¯ó:¯Ãîî./õR/ÅOÿôOóà?˜×~í׿w~çw°Í¿DzЃ¸õÖ[¹ê§ÝÝ]^çu^‡¿þë¿àرcüöoÿ6/ýÒ/Í¥þèæk¾ækø¬Ïú,>û³?›«þo»õÖ[yÈCÂýló¯!‰ûÙæ…ùíßþm^çu^€×z­×â·û·¹êª«®ºêªgB¶ÍUW]uÕ¿Ãîî.'Nœàµ^ëµøíßþm^˜ÏþìÏæs>çsø©Ÿú)Þú­ßšÕWõWó1ó1|Ög}ŸýÙŸÍ ó2/ó2üõ_ÿ5¶¹ê‡?øÁ<ãÏÀ6/ÌOÿôOó6oó6|Ög}ŸýÙŸÍ–ßþíßæu^çux­×z-~û·›«þwûíßþm^çu^€Ïú¬Ïâ³?û³ù÷Àk½ÖkñÛ¿ýÛ¼0ŸýÙŸÍç|ÎçðU_õU|ôG4Wýï' €×z­×â·û·ù¯$‰ûÙæªÿ}~û·›·y›·aww€—z©—â·û·9~ü8ÿ^ïýÞïÍ÷|Ï÷ð[¿õ[¼ök¿6/Èîî.'Nœàµ^ëµøíßþm®úßç¯ÿú¯yŸ÷yþú¯ÿ€=èAüôOÿ4/ýÒ/͵ÏþìÏæs>çsø¬Ïú,>û³?›«þï{ðƒÌ3žñ žþô§óà?˜ÕK¿ôKó7ó7Øæ…ùíßþm^çu^€÷z¯÷⻿û»¹êª«®ºêªgB¶ÍUW]uÕ¿Óñãǹté~ðƒyúÓŸÎ óÖoýÖüÌÏü õWÅK¿ôKó¢úíßþm^çu^€ú¨â«¿ú«ya$ð =ˆ[o½•«þwxí×~m~çw~€‹/rüøq^ÏþìÏæs>çsø®ïú.Þû½ß›­ßùßáAz~ðƒya~ú§š·y›·à½Þë½øîïþn®úßí·û·y×y>ë³>‹ÏþìÏæßãÁ~0ÏxÆ3°Í óÞïýÞ|Ï÷|¿õ[¿Åk¿öksÕÿ~’x­×z-~û·›¯[o½•g<ã¼Ök½ÿIÜÏ6WýïòÝßýݼÏû¼÷{¯÷z/¾û»¿›ÿ(ŸýÙŸÍç|ÎçðS?õS¼õ[¿5/Èoÿöoó:¯ó:¼×{½ßýÝßÍUÿ»üõ_ÿ5¯ó:¯Ãîî./õR/Åoÿöosüøqþ£ìîîò7ó7¼ÔK½Çç…ùèþh¾æk¾€ïú®ïâ½ßû½¹êŸÝÝ]þæoþ†ÝÝ]^ëµ^‹ãÇó¼ök¿6¿ó;¿ÀoýÖoñÚ¯ýÚ¼¨Þú­ßšŸù™ŸàéO:~ðƒyA¾û»¿›÷yŸ÷à³>ë³øìÏþl®ºêª«®ºê™msÕUW]õïôÖoýÖüÌÏü OúÓyðƒÌ râÄ vww9vì»»»ükìîîrâÄ ^ú¥_š¿ú«¿âùíßþm^çu^€÷z¯÷⻿û»¹ê‡ÏþìÏæs>çsø©Ÿú)Þú­ßšäµ_ûµùßùžþô§óà?˜Õw÷wó>ïó>¼Õ[½?ýÓ?Í óÑýÑ|Í×| _õU_ÅGôGsÕÿn¿ýÛ¿Íë¼ÎëðYŸõY|ög6ÿïýÞïÍ÷|Ï÷ðWõW¼ôK¿4/ÈCòn½õVlsÕÿ ’x­×z-~û·›IÜïâÅ‹?~œä¯ÿú¯y™—y^ê¥^Š¿þë¿æªÿ=¾û»¿›÷yŸ÷á~ŸõYŸÅgögóé·û·y×y>ê£>Нþê¯æùìÏþl>çs>€ïú®ïâ½ßû½¹ê¿þë¿æu^çuØÝÝà½Þë½øê¯þjŽ?Δ÷~ï÷æ{¾ç{øª¯ú*>ú£?šæe^æeøë¿þkþê¯þŠ—~é—æªÿ}Þú­ßšŸù™Ÿà§~ê§xë·~k^˜‡<ä!Üzë­<ýéOçÁ~0/ª¯þê¯æc>æcø®ïú.Þû½ß›ä½ßû½ùžïùþê¯þŠ—~é—æª«®ºêª«ž Ù6W]uÕUÿNßýÝßÍû¼ÏûðYŸõY|ög6ÏÏoÿöoó:¯ó:¼×{½ßýÝßÍ¿Ö[¿õ[ó3?ó3<ýéOçÁ~0ÏÏ{¿÷{ó=ßó=üÖoý¯ýÚ¯ÍUÿ;üõ_ÿ5/ó2/Àk¿ökó[¿õ[û³?›ŸþéŸæmÞæmø¨ú(¾ú«¿šçç·û·y×yÞë½Þ‹ïþîïæªÿ$ðZ¯õZüöoÿ6ÿ/ýÒ/ÍßüÍßð]ßõ]¼÷{¿7/È{¿÷{ó=ßó=|Ög}ŸýÙŸÍUÿ;üõ_ÿ5¯ó:¯Ãîî.ßõ]ßÅ{¿÷{óŸáøñã\ºt‰ãÇóô§?ãÇóü<ä!áÖ[oàâÅ‹?~œ«þwØÝÝåe^æe¸õÖ[x¯÷z/¾û»¿›ÿh_ýÕ_ÍÇ|ÌÇðÒ/ýÒüÕ_ý/Èoÿöoó:¯ó:<èAâÖ[oåªÿ¾ú«¿šù˜à½Þë½øîïþn^ßþíßæu^çuxЃÄ­·Þʿƭ·ÞÊCò^ûµ_›ßú­ßâùÙÝÝå!y»»»<èAâÖ[o媫®ºêª«Ù6W]uÕUÿN»»»<øÁæÒ¥K?~œßú­ßâ¥_ú¥y ÝÝ]^çu^‡¿þë¿àéO:~ðƒyn¿ó;¿Ãý^ê¥^ŠãÇó@?ýÓ?ÍÛ¼ÍÛðÒ/ýÒüÕ_ýÏí¯ÿú¯y™—yô që­·rÕÿ./ýÒ/ÍßüÍßðS?õS¼õ[¿5ÏímÞæmøéŸþi¾ë»¾‹÷~ï÷æ¹ýõ_ÿ5—.]àAz~ðƒy ×~í׿w~çwxë·~k~ê§~Šçç}Þç}øîïþnÞë½Þ‹ïþîïæªÿý~û·›×y×à³>ë³øìÏþl^˜[o½•g<ã<èAâÁ~0ÏíÁ~0ÏxÆ38~ü8¿õ[¿ÅK¿ôKó@»»»¼Îë¼ý× ÀoýÖoñÚ¯ýÚ\õƒ$^ëµ^‹ßþíßæ…¹õÖ[yÆ3žÀ±cÇxé—~iè«¿ú«ù˜ùŽ?ÎÓŸþtŽ?ÎsûîïþnÞç}Þ€cÇŽqë­·rüøq®úßáe^æeøë¿þk>ë³>‹ÏþìÏæßbww—¿ù›¿á~¯õZ¯Åsûèþh¾æk¾€ú¨â«¿ú«yn_ýÕ_ÍÇ|ÌÇð^ïõ^|÷w7WýïñÞïýÞ|Ï÷|oõVoÅOÿôOóoõ;¿ó;Üï¥^ê¥8~ü8÷ÛÝÝåÁ~0—.]à«¾ê«øèþhžÛîî.¯ó:¯Ã_ÿõ_ð]ßõ]¼÷{¿7Wýï´»»Ëƒü`.]ºÀoýÖoñÚ¯ýÚ<·ÝÝ]^æe^†[o½€ïú®ïâ½ßû½y [o½•g<ã;vŒ—~é—æ¹½ök¿6¿ó;¿Àw}×wñÞïýÞ<·÷yŸ÷ỿû»ø®ïú.Þû½ß›«®ºêª«®zdÛ\uÕUWýøê¯þj>æc>€ãÇóÝßýݼÕ[½¿ýÛ¿ÍÇ|ÌÇð×ý×|ÔG}_ýÕ_Íó#‰ûýÖoý¯ýÚ¯Ís{í×~m~çw~€×~í׿«¾ê«xé—~i¾ç{¾‡þèfww€Ÿú©Ÿâ­ßú­¹ê—ßþíßæu^çu¸ßWõWó^ïõ^?~œ[o½•ù˜á§ú§x©—z)þú¯ÿšççµ_ûµùßù>ë³>‹ÏþìÏæ~û·›×y×á~oýÖoÍg}ÖgñÒ/ýÒüöoÿ6Ÿó9ŸÃoÿöopìØ1n½õVŽ?ÎUÿûýöoÿ6¯ó:¯Àg}ÖgñÙŸýÙ¼0ŸýÙŸÍç|ÎçðYŸõY|ög6Ïí§ú§y›·yŽ?ÎWõWó^ïõ^üõ_ÿ5ïó>ïÃ_ÿõ_ð^ïõ^|÷w7Wýß! €×z­×â·û·ya>û³?›ÏùœÏàµ^ëµøíßþmžÛK¿ôKó7ó7<øÁæ³?û³y¯÷z/n½õV¾ç{¾‡ÏþìÏæ~?õS?Å[¿õ[sÕÿßýÝßÍû¼Ïûp¿×~í׿EõU_õU¼ôK¿4÷ûíßþm^çu^‡ûÙæ¹íîîòà?˜K—.ðÞïýÞ|Ög}~ðƒÙÝÝåk¾ækøìÏþlŽ;Æ_ÿõ_óà?˜«þw¸õÖ[yÈCÂý^ú¥_šãÇó¢x¯÷z/Þû½ß›’Äý~ë·~‹×~í׿>û³?›ÏùœÏá~ýÑÍG}ÔGñà?€Ÿù™Ÿá£?ú£¹õÖ[x­×z-~û·›«þwûìÏþl>çs>€ãÇóÑýÑ|ÔG}Çgww—Ÿù™Ÿá£?ú£ÙÝÝàµ^ëµøíßþmžÛgögó9Ÿó9¼Ök½¿ýÛ¿Ís»õÖ[yÈCÂý>û³?›ú¨âøñãÜzë­|ÌÇ| ?ýÓ? ÀK½ÔKñ×ý×\uÕUW]uÕsA¶ÍUW]uÕ÷~ï÷æ{¾ç{xa^ëµ^‹ßþíßæ‘Äý~ë·~‹×~í׿¹íîîòÚ¯ýÚüÍßü /Ìw}×wñÞïýÞ\õ¿Ów÷wó>ïó>¼0/õR/ÅoÿöosüøqžŸ×~í׿w~çwø¬Ïú,>û³?›çöÝßýݼÏû¼ÿ’=èAüôOÿ4/ýÒ/ÍUÿ7üöoÿ6¯ó:¯Àg}ÖgñÙŸýÙ¼0ŸýÙŸÍç|ÎçðYŸõY|ög6ÏÏ{¿÷{ó=ßó=¼0/õR/Åoÿöosüøq®ú¿C¯õZ¯ÅoÿöoóÂ|ög6Ÿó9ŸÀk½ÖkñÛ¿ýÛ<·ÝÝ]^ûµ_›¿ù›¿á_ò]ßõ]¼÷{¿7WýïñÚ¯ýÚüÎïüÿ¿õ[¿Åk¿öks¿ßþíßæu^çu¸ŸmžŸ¿þë¿æµ_ûµ¹té/ȱcÇøíßþm^ú¥_š«þ÷øèþh¾æk¾†‹Ïú¬Ïâ³?û³y IÜï·~ë·xí×~mžÛ{¿÷{ó=ßó=üK^ëµ^‹ŸþéŸæøñã\õ¿ß{¿÷{ó=ßó=üK^ëµ^‹ŸþéŸæøñã<·ÏþìÏæs>çsx­×z-~û·›ç绿û»yŸ÷y^˜cÇŽqë­·rüøq®ºêª«®ºê¹ Ûæª«®ºê?ÐWõWóÕ_ýÕ<ãÏàŽ;Æ{¿÷{óÙŸýÙ?~œD÷û­ßú-^ûµ_›çgww—ÏþìÏæk¾ækxnzЃøê¯þjÞú­ßš«þwûíßþmÞû½ß›g<ã<·÷z¯÷â«¿ú«9~ü8/Èk¿ökó;¿ó;|Ög}ŸýÙŸÍóó×ý×|ög6?ó3?Ãs;vìïýÞïÍgögsüøq®ú¿ã·û·y×y>ë³>‹ÏþìÏæ…ùìÏþl>çs>€Ïú¬Ïâ³?û³yA¾û»¿›ÏþìÏæÏxÏí£>ê£øìÏþlŽ?ÎUÿ·Hàµ^ëµøíßþm^˜ÏþìÏæs>çsx­×z-~û·›çgww—¯þê¯æ«¿ú«¹téÏí­Þê­øìÏþl^ú¥_š«þw‘Ä¿ÕoýÖoñÚ¯ýÚÜï·û·y×yîg›äÖ[oå½ßû½ùßùžÛk½ÖkñÕ_ýÕ¼ôK¿4WýïòÚ¯ýÚüÎïüÿŸõYŸÅgögó@’¸ßoýÖoñÚ¯ýÚû³?›÷~ï÷æùìÏþl>çs>€×z­×â·û·yA~û·›ÏþìÏæw~çwxnoõVoÅWõWóà?˜«®ºêª«®z>msÕUW]õŸà¯ÿú¯ÙÝÝeww—ãÇóÚ¯ýÚügØÝÝå¯ÿú¯ÙÝÝåøñã?~œ—~é—æªÿ[n½õVn½õVvww9~ü8/ýÒ/ÍñãÇù¶»»Ë_ÿõ_³»»ËñãÇxí×~m®ºêßê¯ÿú¯ÙÝÝåÖ[oåÁ~0/ýÒ/Íñãǹꪋ¿þë¿fww—[o½•?øÁ¼ôK¿4Ç窫þ-vwwùë¿þkvww9~ü8~ðƒyðƒÌUWý[Üzë­Üzë­ìîîrüøqŽ?ÎK¿ôKsÕÿm·Þz+·Þz+¿ýÛ¿Ík¿öksüøq^ú¥_šÿ ·Þz+·Þz+»»»?~œ—~é—æøñã\uÕUW]uÕ l›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®z–ïþîïæÏxïõ^ïŃü`þ+}õW5—.]à£>ê£8~ü8W]uÕUW]uÕUW]uÕ¿ ²m®ºêª«®ºêª«®ºêªçð:¯ó:üWùª¯ú*^ú¥_š¿þë¿æc>æc¸ßK½ÔKñÕ_ýÕ\õ_ë§ú§y›·y^ê¥^Š¿þë¿æ¿ÚWõWó1ó1¼õ[¿5?õS?ÅUW]uÕUW]uÕUW]õ¯‚l›«®ºêª«®ºêª«®ºê9Hâ¿ÊoýÖoñÚ¯ýÚüöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæªÿ:»»»<ä!aww€ßú­ßâµ_ûµùïðà?˜g<ã|ÕW}ýÑÍUW]uÕUW]uÕUW]õ"C¶ÍUW]uÕUW]uÕUW]õ$ñ_å·~ë·xí×~m~û·›×y×á~¯õZ¯ÅoÿöosÕ×y×á·û·x­×z-~û·›ÿ.?ýÓ?ÍÛ¼ÍÛpüøqþê¯þŠ?øÁ\uÕUW]uÕUW]uÕU/dÛ\uÕUW]uÕUW]uÕUÏAÿU~ë·~‹×~í׿·û·y×yî÷Z¯õZüöoÿ6Wý×øéŸþiÞæmÞ†û=ýéOçÁ~0ÿ^ûµ_›ßùßà­ßú­ù©Ÿú)®ºêª«®ºêª«®ºêª ²m®ºêª«®ºêª«®ºêªçðÛ¿ýÛ¼¨>ú£?š¿ù›¿á~ë·~‹ÕK¿ôKsüøq~û·›×y×á~¯õZ¯ÅoÿöosÕ¾ÝÝ]^æe^†[o½€÷z¯÷⻿û»ùïöÛ¿ýÛ¼Îë¼÷û­ßú-^ûµ_›«®ºêª«®ºêª«®ºê_„l›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6WýïñÙŸýÙ|Îç|÷{úӟ΃ü`þ'xí×~m~çw~€?øÁ<ýéO窫®ºêª«®ºêª«®ú!Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²ÍUÿ;ìîîò‡<„ÝÝ]Þë½Þ‹ïþîïæŠßþíßæu^çu¸ßw}×wñÞïýÞ\uÕUW]uÕUW]uÕU/²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Û\õ¿Ãgögó9Ÿó9Üï·~ë·xí×~mþ'yðƒÌ3žñ üàóô§?«®ºêª«®ºêª«®ºê…B¶ÍUW]uÕUW]uÕUW]õoöÚ¯ýÚüÎïüd›«þçÛÝÝå!y»»»<èAâÖ[o嚯þê¯æc>æc¸ßw}×wñÞïýÞ\uÕUW]uÕUW]uÕU/²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx ÛükÝzë­|Ï÷|÷{ЃÄ{¿÷{óü|÷w7ÏxÆ3¸ßg}Ögq¿ÝÝ]~æg~†ïþîïà·û·yé—~iŽ?Îk¿ökó^ïõ^<øÁæùÙÝÝå{¾ç{øéŸþivwwùë¿þküàóÒ/ýÒ¼ök¿6ïõ^ïÅñãÇù·ú™Ÿù~û·›¿þë¿æÖ[oåÖ[oå¥_ú¥9~ü8/ýÒ/Í[¿õ[óZ¯õZüWùîïþnÞç}Þ‡û}ÕW}ýÑÍ¿Ö÷|Ï÷ðÛ¿ýÛÜzë­üõ_ÿ5»»»<øÁæÁ~0Çç­ßú­y­×z-üàóoqë­·ò‡<„û½ôK¿4õWÅUW]uÕUW]uÕUW]õ!Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸Ÿm¾û»¿›ù˜aww—滾ë»xï÷~oès>çsøìÏþl^˜ãÇó]ßõ]¼õ[¿5ÿßó=ßÃgögsë­·ò/yðƒÌw}×wñÚ¯ýÚüg{™—yþú¯ÿšû=ýéOçÁ~0/ª¯ùš¯á³?û³ÙÝÝåEñÞïýÞ|Ög}~ðƒù×zé—~iþæoþ†ûýÕ_ý/ýÒ/ÍUW]uÕUW]uÕUW]õ|!Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ŸmÞç}Þ‡ïþîïæEõQõQ|õW5¯ó:¯Ãoÿöoó¢ú©Ÿú)Þú­ßšÉîî.ïó>ïÃOÿôOó¯õÞïýÞ|×w}ÿYþú¯ÿš—y™—á~zЃ¸õÖ[yQ½Ïû¼ßýÝßÍ¿ÖñãÇù­ßú-^ú¥_šÏþìÏæs>çs¸ß{½×{ñÝßýÝ\uÕUW]uÕUW]uÕUϲm®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}ÕW}ó1Ã=èAâÁ~0¿ó;¿Ãóóô§?ÏùœÏỿû»y ×z­×`ww—¿ù›¿á¹?~œ§?ýé?~œdww—×y×á¯ÿú¯yn/õR/Åk¿öksüøqn½õVþú¯ÿš¿ù›¿á¹½÷{¿7ßõ]ßņþèæk¾æk¸ß{½×{ñÝßýݼ(>û³?›ÏùœÏá¹½Ök½¯ýÚ¯ Àîî.ý×ÍïüÎïðÜŽ?ÎÓŸþtŽ?΋ê¯ÿú¯y™—yî÷à?˜§?ýé\uÕUW]uÕUW]uÕUϲm®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡çç½Þë½øìÏþlüàs¿ÝÝ]>ú£?šïùžïáüàsë­·pìØ1¾ú«¿š÷~ï÷æþú¯ÿš÷~ï÷æoþæox ïú®ïâ½ßû½yAÞç}Þ‡ïþîïæ^ëµ^‹¯þê¯æ¥_ú¥yný×ÍGôGó;¿ó;<ÐW}ÕWñÑýÑüG{™—yþú¯ÿšûýÔOýoýÖoÍ¿äÖ[oå!yô^ïõ^|õW5Çç¹Ýzë­|ôG4?ó3?Ã}ÔG}_ýÕ_Í¿†$è¯þê¯xé—~i®ºêª«®ºêª«®ºêªçl›«®ºêª«®ºêª«®ºêßìµ_ûµùßùÈ6ÿZ¿ýÛ¿Íë¼Îëp¿×z­×â·û·y~^ûµ_›ßùßá¹}×w}ïýÞïÍ òÚ¯ýÚüÎïüÏíØ±cüõ_ÿ5~ðƒy~vwwyðƒÌ¥K—¸ß[½Õ[ñÓ?ýÓæc¸ßk½ÖkñÛ¿ýÛüKÞû½ß›ïùžïá~ÇçâÅ‹ük¼ök¿6¿ó;¿Ãý>ë³>‹ÏþìÏæª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùè½Þë½øîïþn^˜ŸþéŸæmÞæmxn¿õ[¿Åk¿ökóÂ|ôG4_ó5_Ãýüàóô§?ççe^æeøë¿þkî÷Z¯õZüöoÿ6/ŠÝÝ]^ú¥_šg<ãÜï³>ë³øìÏþlþ£üöoÿ6¯ó:¯ÃÙæEñÞïýÞ|Ï÷|÷û¬Ïú,>û³?›É­·ÞÊCòè¯þê¯xé—~i^TïýÞïÍ÷|Ï÷p¿·z«·â§ú§¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡zúӟ΃ü`^˜ÝÝ]Nœ8Á½ÔK½ý×Í¿ä§ú§y›·yÈ6Ïí·û·y×yè·~ë·xí×~m^TßýÝßÍû¼Ïûp¿ãÇsñâEþ£|ög6Ÿó9ŸÃý^ê¥^Š¿þë¿æEñÚ¯ýÚüÎïü÷û¨ú(¾ú«¿šÅgögsüøq^ú¥_šãÇóÒ/ýÒük|õW5ó1Ãýüàóô§?«®ºêª«®ºêª«®ºêy Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßK½ÔKñ×ý×¼($ñ@õQÅWõWó/ùíßþm^çu^‡²ÍsûìÏþl>çs>‡û=èAâÖ[oå_cww—'Nð@¿õ[¿Åk¿ökóá½ßû½ùžïùî÷^ïõ^|÷w7/ŠÏþìÏæs>çs¸ßñãÇù©Ÿú)^ûµ_›ÿl¿ýÛ¿Íë¼Îëð@¶¹êª«®ºêª«®ºêª«ž²m®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û½Ök½¿ýÛ¿Í‹Bô]ßõ]¼÷{¿7ÿ’ßþíßæu^çux Û<·×~í׿w~çw¸ßG}ÔGñÕ_ýÕük½ôK¿4ó7Ãý¾ê«¾Šþèæ?Âk¿ökó;¿ó;Üï³>ë³øìÏþl^¿ýÛ¿Íë¼ÎëðÜÞú­ßš·~ë·æ­Þê­8~ü8ÿ~û·›×y×áþê¯þŠ—~é—æª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóÜ$ñ@ïýÞïÍ{¿÷{ó¯õÑýÑüõ_ÿ5÷{¯÷z/¾û»¿›ÿ/ó2/Ã_ÿõ_s¿Ïú¬Ïâ³?û³yQ½ök¿6¿ó;¿Ã òÒ/ýÒ¼õ[¿5¯ýÚ¯Ík½ÖkñåÖ[oå!yô[¿õ[¼ök¿6W]uÕUW]uÕUW]uÕs@¶ÍUW]uÕUW]uÕUW]õoöÚ¯ýÚüÎïüd›­ßþíßæu^çu¸ßk½ÖkñÛ¿ýÛû³?›ÝÝ]Þû½ß›Ÿù™ŸáEuüøqÞú­ßš¯úª¯âøñãü[Hâ~ë·~‹×~í׿ª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×áló@ý×Í˼ÌËðŸáµ^ëµøíßþmþ#Hâ¾ê«¾ŠþèæßâÖ[oå«¿ú«ùéŸþižñŒgð¢8~ü8¿õ[¿ÅK¿ôKó¯%‰ú­ßú-^ûµ_›«®ºêª«®ºêª«®ºê9 Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼($ñ@¿õ[¿Åk¿ökó/ùíßþm^çu^‡²Ís“Ä}ÕW}/ýÒ/Í¿×ñãÇyé—~iþ#Hâ>ë³>‹ÏþìÏæßë¯ÿú¯ùíßþm~ú§šßùßá…9~ü8¿õ[¿ÅK¿ôKó¯!‰ú­ßú-^ûµ_›«®ºêª«®ºêª«®ºê9 Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼($ñ@¿õ[¿Åk¿ökó/ùíßþm^çu^‡²Ís“Ä}×w}ïýÞïÍÿ$¯ýÚ¯ÍïüÎïp¿Ïú¬Ïâ³?û³ùöÛ¿ýÛüôOÿ4?ýÓ?Í3žñ žÛ{½×{ñÝßýݼ¨~û·›×y×á~ë·~‹×~í׿ª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóÜüàóŒg<ƒû}ÔG}_ýÕ_Íÿ$¯ýÚ¯ÍïüÎïp¿÷z¯÷⻿û»ùÏôÛ¿ýÛ¼õ[¿5—.]âló¢úíßþm^çu^‡ú«¿ú+^ú¥_š«®ºêª«®ºêª«®ºê9 Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼($ñ@¿õ[¿Åk¿ökó/ùíßþm^çu^‡²Ís{ï÷~o¾ç{¾‡û½ôK¿4õWÅ¿Öw÷w#‰?øÁ<èAâÁ~0ÿQÞû½ß›ïùžïá~¯õZ¯Åoÿöoó¢ØÝÝåoþæo¸õÖ[±Í{¿÷{ó¢úîïþnÞç}Þ‡ú«¿ú+^ú¥_šÅoÿöoó:¯ó:æc>†û=èAâÖ[o媫®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóÜvwwyðƒÌ¥K—¸ßK¿ôKóWõW¼¨ÞæmÞ†ŸþéŸæžþô§óà?˜ÿ?ýÓ?ÍÛ¼ÍÛð@¶ù—ìîîrâÄ 軾ë»xï÷~o^¿ýÛ¿Íë¼Îëð@¶yQ½÷{¿7ßó=ßÃýÞê­ÞŠŸþéŸæª«®ºêª«®ºêª«®zȶ¹êª«®ºêª«®ºêª«þÍ^ûµ_›ßùßáló¯õÛ¿ýÛ¼Îë¼÷{­×z-~û·›ççµ_ûµùßùî÷YŸõY|ög6/ I<ÐoýÖoñÚ¯ýÚüK~û·›×y×álóü|ög6Ÿó9ŸÃ½÷{¿7ßõ]ßſ仿û»yŸ÷yè½Þë½øîïþnþ£ìîîrâÄ è·~ë·xí×~mþ%oýÖoÍÏüÌÏp¿ãÇóô§?ãÇó/y×y~û·›û=èAâÖ[oåEõ2/ó2üõ_ÿ5÷ûª¯ú*>ú£?š«®ºêª«®ºêª«®ºêy Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼($ñ@¿õ[¿Åk¿ökó/ùíßþm^çu^‡²Íó³»»Ëk¿ökó7ó7<Ðk¿ökó]ßõ]<øÁæ¹íîîò5_ó5|ög6tìØ1þú¯ÿš?øÁüGzé—~iþæoþ†û}ÕW}ýÑÍ¿ä·û·y×yèÁ~0ßõ]ßÅk¿ökóüìîîò1ó1|÷w7ô]ßõ]¼÷{¿7/ŠÝÝ]Nœ8ÁýÕ_ý/ýÒ/ÍUW]uÕUW]uÕUW]õë³øìÏþl^’x ßú­ßâµ_ûµù—üöoÿ6¯ó:¯ÃÙæùë¿þk^ûµ_›K—.ñÜ^ú¥_š·~ë·æ~ý×Íoÿöo³»»Ësû®ïú.Þû½ß›ÿhýÑÍ×|Í×p¿·z«·â§ú§yQ|ôG4_ó5_Ãs{é—~i^ûµ_›ãÇs¿¿þë¿æ·û·ÙÝÝåÞë½Þ‹ïþîïæEõÓ?ýÓ¼ÍÛ¼ ÷;vì»»»\uÕUW]uÕUW]uÕUϲm®ºêª«®ºêª«®ºêª³×~í׿w~çwx Ûükýöoÿ6¯ó:¯Ãý^ëµ^‹ßþíßæùyí×~m~çw~‡û}Ög}ŸýÙŸÍ‹Bô[¿õ[¼ök¿6ÿ’ßþíßæu^çux Û¼0ý×Í{¿÷{ó7ó7ü[|×w}ïýÞï͆¿þë¿æe^æe¸ßñãǹxñ"/ª÷~ï÷æ{¾ç{ø·x©—z)~û·›ãÇó¢úìÏþl>çs>‡û}ÔG}_ýÕ_ÍUW]uÕUW]uÕUW]õ|!Ûæª«®ºêª«®ºêª«®ú7{í×~m~çw~‡²Í¿Öoÿöoó:¯ó:Üïµ^ëµøíßþmžŸ×~í׿w~çw¸ßg}ÖgñÙŸýÙ¼($ñ@¿õ[¿Åk¿ökó/ùíßþm^çu^‡²Í¿dww—¯þê¯æ«¿ú«¹té/Š×z­×â«¿ú«yé—~iþ3=øÁæÏx÷û«¿ú+^ú¥_šÕgögóÕ_ýÕ\ºt‰űcÇøìÏþl>ú£?š­—y™—á¯ÿú¯¹ß_ýÕ_ñÒ/ýÒ\uÕUW]uÕUW]uÕUϲm®ºêª«®ºêª«®ºêª³ïþîïæÖ[oå>û³?›­[o½•ïþîïæ~~ðƒyï÷~ožŸïþîïæÖ[oå~¯ýÚ¯Ík¿ökó¢øìÏþlè½ßû½yðƒÌ¿äÖ[o廿û»y ÏþìÏæEµ»»ËOÿôOóÓ?ýÓüõ_ÿ5ÏxÆ3x ×z­×â¥_ú¥yï÷~o^ú¥_šÿ _ýÕ_ÍÇ|ÌÇp¿ú¨â«¿ú«ùרÝÝå§ú§ùéŸþin½õVþæoþ†z©—z)^ú¥_š×~í׿­ßú­9~ü8ÿZý×Í˼ÌËp¿=èAÜzë­\uÕUW]uÕUW]uÕU/²m®ºêª«®ºêª«®ºêª«þÚÝÝåÁ~0—.]àøñã\¼x‘ÿi>ú£?š¯ùš¯á~ßõ]ßÅ{¿÷{sÕUW]uÕUW]uÕUW½@ȶ¹êª«®ºêª«®ºêª«®úê½ßû½ùžïùî÷]ßõ]¼÷{¿7ÿ“œ8q‚ÝÝ]Ž;Æîî.W]uÕUW]uÕUW]uÕ …l›«®ºêª«®ºêª«®ºêªÿ§n½õVò‡p¿×~í׿·~ë·øŸâ»¿û»yŸ÷yî÷YŸõY|ög6W]uÕUW]uÕUW]uÕ …l›«®ºêª«®ºêª«®ºêªÿÇ>ú£?š¯ùš¯á~OúÓyðƒÌÿ¯ó:¯Ãoÿöoð =ˆ¿þë¿æøñã\uÕUW]uÕUW]uÕU/²m®ºêª«®ºêª«®ºêª«þÛÝÝåÁ~0—.]à½Þë½øîïþnþ»ýöoÿ6¯ó:¯Ãý¾ë»¾‹÷~ï÷檫®ºêª«®ºêª«®ú!Ûæª«®ºêª«®ºêª«®ºêÿ¹¯þê¯æc>æc¸ßÓŸþtüàóßéu^çuøíßþm^ê¥^Š¿þë¿æª«®ºêª«®ºêª«®z‘ Ûæª«®ºêª«®ºêª«®ºê*^ú¥_š¿ù›¿à­ßú­ù©Ÿú)þ»üöoÿ6¯ó:¯Ãý~ë·~‹×~í׿ª«®ºêª«®ºêª«®z‘ Ûæª«®ºêª«®ºêª«®ºê*þú¯ÿš—y™—á~¿õ[¿Åk¿ökóßá!y·Þz+õQÅWõWsÕUW]uÕUW]uÕUW½ÈmsÕUW]uÕUW]uÕUW]uÕe_ýÕ_ÍÇ|ÌÇðÚ¯ýÚüÖoýÿÕ¾û»¿›÷yŸ÷à¥^ê¥øë¿þk®ºêª«®ºêª«®ºêªdÛ\uÕUW]uÕUW]uÕUW]õ,_ýÕ_Íîî.ýÑÍñãÇù¯ôÝßýÝÜzë­¼õ[¿5/ýÒ/ÍUW]uÕUW]uÕUW]õ¯‚l›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þï@¶ÍUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕÿȶ¹êª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ú¿Ù6W]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUÿw Ûæª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêÿdÛ\uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUWýßl›«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêªÿ;msÕUW]uÕUW]uÕUW]uÕUW]uÕUW]uÕUW]õ²m®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«®ºêª«þïàÔ@ñõ7bX»IEND®B`‚uv-0.9.17+ds1/assets/svg/000077500000000000000000000000001520155276700150405ustar00rootroot00000000000000uv-0.9.17+ds1/assets/svg/Astral.svg000066400000000000000000000065651520155276700170230ustar00rootroot00000000000000 uv-0.9.17+ds1/assets/svg/Benchmark-Dark.svg000066400000000000000000000223701520155276700203360ustar00rootroot00000000000000 0s 2s 4s uv poetry pdm pip-sync 0.99s 1.90s 4.63s 0.06s uv-0.9.17+ds1/assets/svg/Benchmark-Light.svg000066400000000000000000000223701520155276700205240ustar00rootroot00000000000000 0s 2s 4s uv poetry pdm pip-sync 0.99s 1.90s 4.63s 0.06s uv-0.9.17+ds1/changelogs/000077500000000000000000000000001520155276700150515ustar00rootroot00000000000000uv-0.9.17+ds1/changelogs/0.1.x.md000066400000000000000000001771701520155276700161540ustar00rootroot00000000000000# Changelog 0.1.x ## 0.1.1 ### Bug fixes - Fix bug where `python3` is not found in the global path ([#1351](https://github.com/astral-sh/uv/pull/1351)) ### Documentation - Fix diagram alignment ([#1354](https://github.com/astral-sh/uv/pull/1354)) - Grammar nit ([#1345](https://github.com/astral-sh/uv/pull/1345)) ## 0.1.2 ### Enhancements - Add `--upgrade` support to `pip install` ([#1379](https://github.com/astral-sh/uv/pull/1379)) - Add `-U`/`-P` short flags for `--upgrade`/`--upgrade-package` ([#1394](https://github.com/astral-sh/uv/pull/1394)) - Add `UV_NO_CACHE` environment variable ([#1383](https://github.com/astral-sh/uv/pull/1383)) - uv-cache: Add hidden alias for --no-cache-dir ([#1380](https://github.com/astral-sh/uv/pull/1380)) ### Bug fixes - Add fix-up for invalid star comparison with major-only version ([#1410](https://github.com/astral-sh/uv/pull/1410)) - Add fix-up for trailing comma with trailing space ([#1409](https://github.com/astral-sh/uv/pull/1409)) - Allow empty fragments in HTML parser ([#1443](https://github.com/astral-sh/uv/pull/1443)) - Fix search for `python.exe` on Windows ([#1381](https://github.com/astral-sh/uv/pull/1381)) - Ignore invalid extra named `.none` ([#1428](https://github.com/astral-sh/uv/pull/1428)) - Parse `-r` and `-c` entries as relative to containing file ([#1421](https://github.com/astral-sh/uv/pull/1421)) - Avoid import contextlib in `_virtualenv` ([#1406](https://github.com/astral-sh/uv/pull/1406)) - Decode HTML escapes when extracting SHA ([#1440](https://github.com/astral-sh/uv/pull/1440)) - Fix broken URLs parsed from relative paths in registries ([#1413](https://github.com/astral-sh/uv/pull/1413)) - Improve error message for invalid sdist archives ([#1389](https://github.com/astral-sh/uv/pull/1389)) ### Documentation - Re-add license badge to the README ([#1333](https://github.com/astral-sh/uv/pull/1333)) - Replace "novel" in README ([#1365](https://github.com/astral-sh/uv/pull/1365)) - Tweak some grammar in the README ([#1387](https://github.com/astral-sh/uv/pull/1387)) - Update README.md to include venv activate ([#1411](https://github.com/astral-sh/uv/pull/1411)) - Update wording and add `alt` tag ([#1423](https://github.com/astral-sh/uv/pull/1423)) ## 0.1.3 ### Enhancements - Add support for `UV_EXTRA_INDEX_URL` ([#1515](https://github.com/astral-sh/uv/pull/1515)) - Use the system trust store for HTTPS requests ([#1512](https://github.com/astral-sh/uv/pull/1512)) - Automatically detect virtual environments when used via `python -m uv` ([#1504](https://github.com/astral-sh/uv/pull/1504)) - Add warning for empty requirements files ([#1519](https://github.com/astral-sh/uv/pull/1519)) - Support MD5 hashes ([#1556](https://github.com/astral-sh/uv/pull/1556)) ### Bug fixes - Add support for extras in editable requirements ([#1531](https://github.com/astral-sh/uv/pull/1531)) - Apply percent-decoding to file-based URLs ([#1541](https://github.com/astral-sh/uv/pull/1541)) - Apply percent-decoding to filepaths in HTML find-links ([#1544](https://github.com/astral-sh/uv/pull/1544)) - Avoid attempting rename in copy fallback path ([#1546](https://github.com/astral-sh/uv/pull/1546)) - Fix list rendering in `venv --help` output ([#1459](https://github.com/astral-sh/uv/pull/1459)) - Fix trailing commas on `Requires-Python` in HTML indexes ([#1507](https://github.com/astral-sh/uv/pull/1507)) - Read from `/bin/sh` if `/bin/ls` cannot be found when determining libc path ([#1433](https://github.com/astral-sh/uv/pull/1433)) - Remove URL encoding when determining file name ([#1555](https://github.com/astral-sh/uv/pull/1555)) - Support recursive extras ([#1435](https://github.com/astral-sh/uv/pull/1435)) - Use comparable representation for `PackageId` ([#1543](https://github.com/astral-sh/uv/pull/1543)) - fix OS detection for Alpine Linux ([#1545](https://github.com/astral-sh/uv/pull/1545)) - only parse /bin/sh (not /bin/ls) ([#1493](https://github.com/astral-sh/uv/pull/1493)) - pypi-types: fix lenient requirement parsing ([#1529](https://github.com/astral-sh/uv/pull/1529)) - Loosen package script regexp to match spec ([#1482](https://github.com/astral-sh/uv/pull/1482)) - Use string display instead of debug for url parse trace ([#1498](https://github.com/astral-sh/uv/pull/1498)) ### Documentation - Provide example of file based package install. ([#1424](https://github.com/astral-sh/uv/pull/1424)) - Adjust link ([#1434](https://github.com/astral-sh/uv/pull/1434)) - Add troubleshooting section to benchmarks guide ([#1485](https://github.com/astral-sh/uv/pull/1485)) - infra: source github templates ([#1425](https://github.com/astral-sh/uv/pull/1425)) ## 0.1.4 ### Enhancements - Add CMD support ([#1523](https://github.com/astral-sh/uv/pull/1523)) - Improve tracing when encountering invalid `requires-python` values ([#1568](https://github.com/astral-sh/uv/pull/1568)) ### Bug fixes - Add graceful fallback for Artifactory indexes ([#1574](https://github.com/astral-sh/uv/pull/1574)) - Allow URL requirements in editable installs ([#1614](https://github.com/astral-sh/uv/pull/1614)) - Allow repeated dependencies when installing ([#1558](https://github.com/astral-sh/uv/pull/1558)) - Always run `get_requires_for_build_wheel` ([#1590](https://github.com/astral-sh/uv/pull/1590)) - Avoid propagating top-level options to sub-resolutions ([#1607](https://github.com/astral-sh/uv/pull/1607)) - Consistent use of `BIN_NAME` in activation scripts ([#1577](https://github.com/astral-sh/uv/pull/1577)) - Enforce URL constraints for non-URL dependencies ([#1565](https://github.com/astral-sh/uv/pull/1565)) - Allow non-nested archives for `hexdump` and others ([#1564](https://github.com/astral-sh/uv/pull/1564)) - Avoid using `white` coloring in terminal output ([#1576](https://github.com/astral-sh/uv/pull/1576)) - Bump simple metadata cache version ([#1617](https://github.com/astral-sh/uv/pull/1617)) - Better error messages on expect failures in resolver ([#1583](https://github.com/astral-sh/uv/pull/1583)) ### Documentation - Add license to activator scripts ([#1610](https://github.com/astral-sh/uv/pull/1610)) ## 0.1.5 ### Enhancements - Add `CACHEDIR.TAG` to uv-created virtualenvs ([#1653](https://github.com/astral-sh/uv/pull/1653)) ### Bug fixes - Build source distributions in the cache directory instead of the global temporary directory ([#1628](https://github.com/astral-sh/uv/pull/1628)) - Do not remove uv itself on pip sync ([#1649](https://github.com/astral-sh/uv/pull/1649)) - Ensure we retain existing environment variables during `python -m uv` ([#1667](https://github.com/astral-sh/uv/pull/1667)) - Add yank warnings at end of messages ([#1669](https://github.com/astral-sh/uv/pull/1669)) ### Documentation - Add brew to readme ([#1629](https://github.com/astral-sh/uv/pull/1629)) - Document RUST_LOG=trace for additional logging verbosity ([#1670](https://github.com/astral-sh/uv/pull/1670)) - Document local testing instructions ([#1672](https://github.com/astral-sh/uv/pull/1672)) - Minimal markdown nits ([#1664](https://github.com/astral-sh/uv/pull/1664)) - Use `--override` rather than `-o` to specify overrides in README.md ([#1668](https://github.com/astral-sh/uv/pull/1668)) - Remove setuptools & wheel from seed packages on Python 3.12+ ( #1602) ([#1613](https://github.com/astral-sh/uv/pull/1613)) ## 0.1.6 ### Enhancements - Expose find_uv_bin and declare typing support ([#1728](https://github.com/astral-sh/uv/pull/1728)) - Implement `uv cache dir` ([#1734](https://github.com/astral-sh/uv/pull/1734)) - Support `venv --prompt` ([#1570](https://github.com/astral-sh/uv/pull/1570)) - Print activation instructions for a venv after one has been created ([#1580](https://github.com/astral-sh/uv/pull/1580)) ### CLI - Add shell completions generation ([#1675](https://github.com/astral-sh/uv/pull/1675)) - Move `uv clean` to `uv cache clean` ([#1733](https://github.com/astral-sh/uv/pull/1733)) - Allow `-f` alias for `--find-links` ([#1735](https://github.com/astral-sh/uv/pull/1735)) ### Configuration - Control pip timeout duration via environment variable ([#1694](https://github.com/astral-sh/uv/pull/1694)) ### Bug fixes - Add support for absolute paths on Windows ([#1725](https://github.com/astral-sh/uv/pull/1725)) - Don't preserve timestamp in streaming unzip ([#1749](https://github.com/astral-sh/uv/pull/1749)) - Ensure extras trigger an install ([#1727](https://github.com/astral-sh/uv/pull/1727)) - Only preserve the executable bit ([#1743](https://github.com/astral-sh/uv/pull/1743)) - Preserve trailing slash for `--find-links` URLs ([#1720](https://github.com/astral-sh/uv/pull/1720)) - Respect `--index-url` provided via requirements.txt ([#1719](https://github.com/astral-sh/uv/pull/1719)) - Set index URLs for seeding venv ([#1755](https://github.com/astral-sh/uv/pull/1755)) - Support dotted function paths for script entrypoints ([#1622](https://github.com/astral-sh/uv/pull/1622)) - Support recursive extras for URL dependencies ([#1729](https://github.com/astral-sh/uv/pull/1729)) - Better error message for missing space before semicolon in requirements ([#1746](https://github.com/astral-sh/uv/pull/1746)) - Add warning when dependencies are empty with Poetry metadata ([#1650](https://github.com/astral-sh/uv/pull/1650)) - Ignore invalid extras from PyPI ([#1731](https://github.com/astral-sh/uv/pull/1731)) - Improve Poetry warning ([#1730](https://github.com/astral-sh/uv/pull/1730)) - Remove uv version from uv pip compile header ([#1716](https://github.com/astral-sh/uv/pull/1716)) - Fix handling of range requests on servers that return "Method not allowed" ([#1713](https://github.com/astral-sh/uv/pull/1713)) - re-introduce cache healing when we see an invalid cache entry ([#1707](https://github.com/astral-sh/uv/pull/1707)) ### Documentation - Clarify Windows install command in README.md ([#1751](https://github.com/astral-sh/uv/pull/1751)) - Add instructions for installing on Arch Linux ([#1765](https://github.com/astral-sh/uv/pull/1765)) ### Rust API - Allow passing in a custom reqwest Client ([#1745](https://github.com/astral-sh/uv/pull/1745)) ## 0.1.7 ### Enhancements - Stream zip archive when fetching non-range-request metadata ([#1792](https://github.com/astral-sh/uv/pull/1792)) - Support setting request timeout with `UV_HTTP_TIMEOUT` and `HTTP_TIMEOUT` ([#1780](https://github.com/astral-sh/uv/pull/1780)) - Improve error message when git ref cannot be fetched ([#1826](https://github.com/astral-sh/uv/pull/1826)) ### Configuration - Implement `--annotation-style` parameter for `uv pip compile` ([#1679](https://github.com/astral-sh/uv/pull/1679)) ### Bug fixes - Add fixup for `prefect<1.0.0` ([#1825](https://github.com/astral-sh/uv/pull/1825)) - Add support for `>dev` specifier ([#1776](https://github.com/astral-sh/uv/pull/1776)) - Avoid enforcing URL correctness for installed distributions ([#1793](https://github.com/astral-sh/uv/pull/1793)) - Don't expect pinned packages for editables with non-existent extras ([#1847](https://github.com/astral-sh/uv/pull/1847)) - Linker copies files as a fallback when ref-linking fails ([#1773](https://github.com/astral-sh/uv/pull/1773)) - Move conflicting dependencies into PubGrub ([#1796](https://github.com/astral-sh/uv/pull/1796)) - Normalize `VIRTUAL_ENV` path in activation scripts ([#1817](https://github.com/astral-sh/uv/pull/1817)) - Preserve executable bit when untarring archives ([#1790](https://github.com/astral-sh/uv/pull/1790)) - Retain passwords in Git URLs ([#1717](https://github.com/astral-sh/uv/pull/1717)) - Sort output when installing seed packages ([#1822](https://github.com/astral-sh/uv/pull/1822)) - Treat ARM wheels as higher-priority than universal ([#1843](https://github.com/astral-sh/uv/pull/1843)) - Use `git` command to fetch repositories instead of `libgit2` for robust SSH support ([#1781](https://github.com/astral-sh/uv/pull/1781)) - Use redirected URL as base for relative paths ([#1816](https://github.com/astral-sh/uv/pull/1816)) - Use the right marker for the `implementation` field of `pyvenv.cfg` ([#1785](https://github.com/astral-sh/uv/pull/1785)) - Wait for distribution metadata with `--no-deps` ([#1812](https://github.com/astral-sh/uv/pull/1812)) - platform-host: check /bin/sh, then /bin/dash and then /bin/ls ([#1818](https://github.com/astral-sh/uv/pull/1818)) - Ensure that builds within the cache aren't considered Git repositories ([#1782](https://github.com/astral-sh/uv/pull/1782)) - Strip trailing `+` from version number of local Python builds ([#1771](https://github.com/astral-sh/uv/pull/1771)) ### Documentation - Add docs for git authentication ([#1844](https://github.com/astral-sh/uv/pull/1844)) - Update venv activation for windows ([#1836](https://github.com/astral-sh/uv/pull/1836)) - Update README.md to include extras example ([#1806](https://github.com/astral-sh/uv/pull/1806)) ## 0.1.8 ### Bug fixes - Allow duplicate URLs that resolve to the same canonical URL ([#1877](https://github.com/astral-sh/uv/pull/1877)) - Retain authentication attached to URLs when making requests to the same host ([#1874](https://github.com/astral-sh/uv/pull/1874)) - Win Trampoline: Use Python executable path encoded in binary ([#1803](https://github.com/astral-sh/uv/pull/1803)) - Expose types to implement custom `ResolverProvider` ([#1862](https://github.com/astral-sh/uv/pull/1862)) - Search `PATH` when `python` can't be found with `py` ([#1711](https://github.com/astral-sh/uv/pull/1711)) - Avoid displaying "root" package when formatting terms ([#1871](https://github.com/astral-sh/uv/pull/1871)) ### Documentation - Use more universal windows install instructions ([#1811](https://github.com/astral-sh/uv/pull/1811)) ### Rust API - Expose types to implement custom ResolverProvider ([#1862](https://github.com/astral-sh/uv/pull/1862)) ## 0.1.9 ### Enhancements - Add support for `config_settings` in PEP 517 hooks ([#1833](https://github.com/astral-sh/uv/pull/1833)) - feat: allow passing extra config k,v pairs for pyvenv.cfg when creating a venv ([#1852](https://github.com/astral-sh/uv/pull/1852)) ### Bug fixes - Ensure authentication is passed from the index url to distribution files ([#1886](https://github.com/astral-sh/uv/pull/1886)) - Use `rustls-tls-native-roots` in `uv` crate ([#1888](https://github.com/astral-sh/uv/pull/1888)) - pep440: fix version ordering ([#1883](https://github.com/astral-sh/uv/pull/1883)) - Hide index URLs from header if not emitted ([#1835](https://github.com/astral-sh/uv/pull/1835)) ### Documentation - Add changelog ([#1881](https://github.com/astral-sh/uv/pull/1881)) ## 0.1.10 ### Enhancements - Omit `--find-links` from annotation header unless requested ([#1898](https://github.com/astral-sh/uv/pull/1898)) - Write to stdout when `--output-file` is present ([#1892](https://github.com/astral-sh/uv/pull/1892)) ### Bug fixes - Retain authentication when making range requests ([#1902](https://github.com/astral-sh/uv/pull/1902)) - Fix uv-created venv detection ([#1908](https://github.com/astral-sh/uv/pull/1908)) - Fix Windows `py` failure from spurious stderr ([#1885](https://github.com/astral-sh/uv/pull/1885)) - Ignore Python 2 installations when querying for interpreters ([#1905](https://github.com/astral-sh/uv/pull/1905)) ## 0.1.11 ### Enhancements - Add support for pip-compile's `--unsafe-package` flag ([#1889](https://github.com/astral-sh/uv/pull/1889)) - Improve interpreter discovery logging ([#1909](https://github.com/astral-sh/uv/pull/1909)) - Implement `uv pip list` ([#1662](https://github.com/astral-sh/uv/pull/1662)) - Allow round-trip via `freeze` command ([#1936](https://github.com/astral-sh/uv/pull/1936)) - Don't write pip compile output to stdout with `-q` ([#1962](https://github.com/astral-sh/uv/pull/1962)) - Add long-form version output ([#1930](https://github.com/astral-sh/uv/pull/1930)) ### Compatibility - Accept single string for `backend-path` ([#1969](https://github.com/astral-sh/uv/pull/1969)) - Add compatibility for deprecated `python_implementation` marker ([#1933](https://github.com/astral-sh/uv/pull/1933)) - Generate versioned `pip` launchers ([#1918](https://github.com/astral-sh/uv/pull/1918)) ### Bug fixes - Avoid erroring for source distributions with symlinks in archive ([#1944](https://github.com/astral-sh/uv/pull/1944)) - Expand scope of archive timestamping ([#1960](https://github.com/astral-sh/uv/pull/1960)) - Gracefully handle virtual environments with conflicting packages ([#1893](https://github.com/astral-sh/uv/pull/1893)) - Invalidate dependencies when editables are updated ([#1955](https://github.com/astral-sh/uv/pull/1955)) - Make < exclusive for non-pre-release markers ([#1878](https://github.com/astral-sh/uv/pull/1878)) - Properly apply constraints in venv audit ([#1956](https://github.com/astral-sh/uv/pull/1956)) - Re-sync editables on-change ([#1959](https://github.com/astral-sh/uv/pull/1959)) - Remove current directory from PATH in PEP 517 hooks ([#1975](https://github.com/astral-sh/uv/pull/1975)) - Remove `--upgrade` and `--quiet` flags from generated output files ([#1873](https://github.com/astral-sh/uv/pull/1873)) - Use full python version in `pyvenv.cfg` ([#1979](https://github.com/astral-sh/uv/pull/1979)) ### Performance - fix `uv pip install` handling of gzip'd response and PEP 691 ([#1978](https://github.com/astral-sh/uv/pull/1978)) - Remove `spawn_blocking` from version map ([#1966](https://github.com/astral-sh/uv/pull/1966)) ### Documentation - Clarify `lowest` vs. `lowest-direct` resolution strategies ([#1954](https://github.com/astral-sh/uv/pull/1954)) - Improve error message for network timeouts ([#1961](https://github.com/astral-sh/uv/pull/1961)) ## 0.1.12 ### CLI - Add a `--python` flag to allow installation into arbitrary Python interpreters ([#2000](https://github.com/astral-sh/uv/pull/2000)) - Add a `--system` flag for opt-in non-virtualenv installs ([#2046](https://github.com/astral-sh/uv/pull/2046)) ### Enhancements - Add a `--pre` alias for `--prerelease=allow` ([#2049](https://github.com/astral-sh/uv/pull/2049)) - Enable `freeze` and `list` to introspect non-virtualenv Pythons ([#2033](https://github.com/astral-sh/uv/pull/2033)) - Support environment variables in index URLs in requirements files ([#2036](https://github.com/astral-sh/uv/pull/2036)) - Add `--exclude-editable` and `--exclude` args to `uv pip list` ([#1985](https://github.com/astral-sh/uv/pull/1985)) - Always remove color codes from output file ([#2018](https://github.com/astral-sh/uv/pull/2018)) - Support recursive extras in direct `pyproject.toml` files ([#1990](https://github.com/astral-sh/uv/pull/1990)) - Un-cache editable requirements with dynamic metadata ([#2029](https://github.com/astral-sh/uv/pull/2029)) - Use a non-local lockfile for locking system interpreters ([#2045](https://github.com/astral-sh/uv/pull/2045)) - Surface the `EXTERNALLY-MANAGED` message to users ([#2032](https://github.com/astral-sh/uv/pull/2032)) ## 0.1.13 ### Bug fixes - Prioritize `PATH` over `py --list-paths` in Windows selection ([#2057](https://github.com/astral-sh/uv/pull/2057)). This fixes an issue in which the `--system` flag would not work correctly on Windows in GitHub Actions. - Avoid canonicalizing user-provided interpreters ([#2072](https://github.com/astral-sh/uv/pull/2072)). This fixes an issue in which the `--python` flag would not work correctly with pyenv and other interpreters. - Allow pre-releases for requirements in constraints files ([#2069](https://github.com/astral-sh/uv/pull/2069)) - Avoid truncating EXTERNALLY-MANAGED error message ([#2073](https://github.com/astral-sh/uv/pull/2073)) - Extend activation highlighting to entire `venv` command ([#2070](https://github.com/astral-sh/uv/pull/2070)) - Reverse the order of `--index-url` and `--extra-index-url` priority ([#2083](https://github.com/astral-sh/uv/pull/2083)) - Avoid assuming `RECORD` file is in `platlib` ([#2091](https://github.com/astral-sh/uv/pull/2091)) ## 0.1.14 ### Enhancements - Add support for `--system-site-packages` in `uv venv` ([#2101](https://github.com/astral-sh/uv/pull/2101)) - Add support for Python installed from Windows Store ([#2122](https://github.com/astral-sh/uv/pull/2122)) - Expand environment variables in `-r` and `-c` subfile paths ([#2143](https://github.com/astral-sh/uv/pull/2143)) - Treat empty index URL strings as null instead of erroring ([#2137](https://github.com/astral-sh/uv/pull/2137)) - Use space as delimiter for `UV_EXTRA_INDEX_URL` ([#2140](https://github.com/astral-sh/uv/pull/2140)) - Report line and column numbers in `requirements.txt` parser errors ([#2100](https://github.com/astral-sh/uv/pull/2100)) - Improve error messages when `uv` is offline ([#2110](https://github.com/astral-sh/uv/pull/2110)) ### Bug fixes - Future-proof the `pip` entrypoints special-case ([#1982](https://github.com/astral-sh/uv/pull/1982)) - Allow empty extras in `pep508-rs` and add more corner case to tests ([#2128](https://github.com/astral-sh/uv/pull/2128)) - Adjust base Python lookup logic for Windows to respect Windows Store ([#2121](https://github.com/astral-sh/uv/pull/2121)) - Consider editable dependencies to be 'direct' for `--resolution` ([#2114](https://github.com/astral-sh/uv/pull/2114)) - Preserve environment variables in resolved Git dependencies ([#2125](https://github.com/astral-sh/uv/pull/2125)) - Use `prefix` instead of `base_prefix` for environment root ([#2117](https://github.com/astral-sh/uv/pull/2117)) - Wrap unsafe script shebangs in `/bin/sh` ([#2097](https://github.com/astral-sh/uv/pull/2097)) - Make WHEEL parsing error line numbers one indexed ([#2151](https://github.com/astral-sh/uv/pull/2151)) - Determine `site-packages` path based on implementation name ([#2094](https://github.com/astral-sh/uv/pull/2094)) ### Documentation - Add caveats on `--system` support to the README ([#2131](https://github.com/astral-sh/uv/pull/2131)) - Add instructions for `SSL_CERT_FILE` env var ([#2124](https://github.com/astral-sh/uv/pull/2124)) ## 0.1.15 ### Enhancements - Add a `--compile` option to `install` to enable bytecode compilation ([#2086](https://github.com/astral-sh/uv/pull/2086)) - Expose the `--exclude-newer` flag to limit candidate packages based on date ([#2166](https://github.com/astral-sh/uv/pull/2166)) - Add `uv` version to user agent ([#2136](https://github.com/astral-sh/uv/pull/2136)) ### Bug fixes - Set `.metadata` suffix on URL path ([#2123](https://github.com/astral-sh/uv/pull/2123)) - Fallback to non-range requests when HEAD returns 404 ([#2186](https://github.com/astral-sh/uv/pull/2186)) - Allow direct URLs in optional dependencies in editables ([#2206](https://github.com/astral-sh/uv/pull/2206)) - Allow empty values in WHEEL files ([#2170](https://github.com/astral-sh/uv/pull/2170)) - Avoid Windows Store shims in `--python python3`-like invocations ([#2212](https://github.com/astral-sh/uv/pull/2212)) - Expand Windows shim detection to include `python3.12.exe` ([#2209](https://github.com/astral-sh/uv/pull/2209)) - HTML-decode URLs in HTML indexes ([#2215](https://github.com/astral-sh/uv/pull/2215)) - Make direct dependency detection respect markers ([#2207](https://github.com/astral-sh/uv/pull/2207)) - Respect `py --list-paths` fallback in `--python python3` invocations on Windows ([#2214](https://github.com/astral-sh/uv/pull/2214)) - Respect local freshness when auditing installed environment ([#2169](https://github.com/astral-sh/uv/pull/2169)) - Respect markers on URL dependencies in editables ([#2176](https://github.com/astral-sh/uv/pull/2176)) - Respect nested editable requirements in parser ([#2204](https://github.com/astral-sh/uv/pull/2204)) - Run Windows against Python 3.13 ([#2171](https://github.com/astral-sh/uv/pull/2171)) - Error when editables don't match `Requires-Python` ([#2194](https://github.com/astral-sh/uv/pull/2194)) ## 0.1.16 ### Enhancements - Add support for `--no-build-isolation` ([#2258](https://github.com/astral-sh/uv/pull/2258)) - Add support for `--break-system-packages` ([#2249](https://github.com/astral-sh/uv/pull/2249)) - Add support for `.netrc` authentication ([#2241](https://github.com/astral-sh/uv/pull/2241)) - Add support for `--format=freeze` and `--format=json` in `uv pip list` ([#1998](https://github.com/astral-sh/uv/pull/1998)) - Add support for remote `https://` requirements files (#1332) ([#2081](https://github.com/astral-sh/uv/pull/2081)) - Implement `uv pip show` ([#2115](https://github.com/astral-sh/uv/pull/2115)) - Allow `UV_PRERELEASE` to be set via environment variable ([#2240](https://github.com/astral-sh/uv/pull/2240)) - Include exit code for build failures ([#2108](https://github.com/astral-sh/uv/pull/2108)) - Query interpreter to determine correct `virtualenv` paths, enabling `uv venv` with PyPy and others ([#2188](https://github.com/astral-sh/uv/pull/2188)) - Respect non-`sysconfig`-based system Pythons, enabling `--system` installs on Debian and others ([#2193](https://github.com/astral-sh/uv/pull/2193)) ### Bug fixes - Fallback to fresh request on non-validating 304 ([#2218](https://github.com/astral-sh/uv/pull/2218)) - Add `.stdout()` and `.stderr()` outputs to `Printer` ([#2227](https://github.com/astral-sh/uv/pull/2227)) - Close `RECORD` after reading entries during uninstall ([#2259](https://github.com/astral-sh/uv/pull/2259)) - Fix Conda Python detection on Windows ([#2279](https://github.com/astral-sh/uv/pull/2279)) - Fix parsing requirement where a variable follows an operator without a space ([#2273](https://github.com/astral-sh/uv/pull/2273)) - Prefer more recent minor versions in wheel tags ([#2263](https://github.com/astral-sh/uv/pull/2263)) - Retry on Python interpreter launch failures during `--compile` ([#2278](https://github.com/astral-sh/uv/pull/2278)) - Show appropriate activation command based on shell detection ([#2221](https://github.com/astral-sh/uv/pull/2221)) - Escape Windows paths with spaces in `venv` activation command ([#2223](https://github.com/astral-sh/uv/pull/2223)) - Add specialized activation message for `cmd.exe` ([#2226](https://github.com/astral-sh/uv/pull/2226)) - Cache wheel metadata in no-PEP 658 fallback ([#2255](https://github.com/astral-sh/uv/pull/2255)) - Use reparse points to detect Windows installer shims ([#2284](https://github.com/astral-sh/uv/pull/2284)) ### Documentation - Add `PIP_COMPATIBILITY.md` to document known deviations from `pip` ([#2244](https://github.com/astral-sh/uv/pull/2244)) ## 0.1.17 ### Enhancements - Allow more-precise Git URLs to override less-precise Git URLs ([#2285](https://github.com/astral-sh/uv/pull/2285)) - Add support for Metadata 2.2 ([#2293](https://github.com/astral-sh/uv/pull/2293)) - Added ability to select bytecode invalidation mode of generated `.pyc` files ([#2297](https://github.com/astral-sh/uv/pull/2297)) - Add `Seek` fallback for zip files with data descriptors ([#2320](https://github.com/astral-sh/uv/pull/2320)) ### Bug fixes - Support reading UTF-16 requirements files ([#2283](https://github.com/astral-sh/uv/pull/2283)) - Trim rows in `pip list` ([#2298](https://github.com/astral-sh/uv/pull/2298)) - Avoid using setuptools shim of distutils ([#2305](https://github.com/astral-sh/uv/pull/2305)) - Communicate PEP 517 hook results via files ([#2314](https://github.com/astral-sh/uv/pull/2314)) - Increase default buffer size for wheel and source downloads ([#2319](https://github.com/astral-sh/uv/pull/2319)) - Add `Accept-Encoding: identity` to remaining stream paths ([#2321](https://github.com/astral-sh/uv/pull/2321)) - Avoid duplicating authorization header with netrc ([#2325](https://github.com/astral-sh/uv/pull/2325)) - Remove duplicate `INSTALLER` in `RECORD` ([#2336](https://github.com/astral-sh/uv/pull/2336)) ### Documentation - Add a custom suggestion to install wheel into the build environment ([#2307](https://github.com/astral-sh/uv/pull/2307)) - Document the environment variables that uv respects ([#2318](https://github.com/astral-sh/uv/pull/2318)) ## 0.1.18 ### Breaking changes Users that rely on native root certificates (or the `SSL_CERT_FILE`) environment variable must now pass the `--native-tls` command-line flag to enable this behavior. - Enable TLS native root toggling at runtime ([#2362](https://github.com/astral-sh/uv/pull/2362)) ### Enhancements - Add `--dry-run` flag to `uv pip install` ([#1436](https://github.com/astral-sh/uv/pull/1436)) - Implement "Requires" field in `pip show` ([#2347](https://github.com/astral-sh/uv/pull/2347)) - Remove `wheel` from default PEP 517 backend ([#2341](https://github.com/astral-sh/uv/pull/2341)) - Add `UV_SYSTEM_PYTHON` environment variable as alias to `--system` ([#2354](https://github.com/astral-sh/uv/pull/2354)) - Add a `-vv` log level and make `-v` more readable ([#2301](https://github.com/astral-sh/uv/pull/2301)) ### Bug fixes - Expand environment variables prior to detecting scheme ([#2394](https://github.com/astral-sh/uv/pull/2394)) - Fix bug where `--no-binary :all:` prevented build of editable packages ([#2393](https://github.com/astral-sh/uv/pull/2393)) - Ignore inverse dependencies when building graph ([#2360](https://github.com/astral-sh/uv/pull/2360)) - Skip prefetching when `--no-deps` is specified ([#2373](https://github.com/astral-sh/uv/pull/2373)) - Trim injected `python_version` marker to (major, minor) ([#2395](https://github.com/astral-sh/uv/pull/2395)) - Wait for request stream to flush before returning resolution ([#2374](https://github.com/astral-sh/uv/pull/2374)) - Write relative paths for scripts in data directory ([#2348](https://github.com/astral-sh/uv/pull/2348)) - Add dedicated error message for direct filesystem paths in requirements ([#2369](https://github.com/astral-sh/uv/pull/2369)) ## 0.1.19 ### Configuration - Add `UV_NATIVE_TLS` environment variable ([#2412](https://github.com/astral-sh/uv/pull/2412)) - Allow `SSL_CERT_FILE` without requiring `--native-tls` ([#2401](https://github.com/astral-sh/uv/pull/2401)) - Add support for retrieving credentials from `keyring` ([#2254](https://github.com/astral-sh/uv/pull/2254)) ### Bug fixes - Add backoff for transient Windows failures ([#2419](https://github.com/astral-sh/uv/pull/2419)) - Move architecture and operating system probing to Python ([#2381](https://github.com/astral-sh/uv/pull/2381)) - Respect `--native-tls` in `venv` ([#2433](https://github.com/astral-sh/uv/pull/2433)) - Treat non-existent site-packages as empty ([#2413](https://github.com/astral-sh/uv/pull/2413)) ### Documentation - Document HTTP authentication ([#2425](https://github.com/astral-sh/uv/pull/2425)) ### Performance - Improve performance of version range operations ([#2421](https://github.com/astral-sh/uv/pull/2421)) ## 0.1.20 ### Bug fixes - Add in-URL credentials to store prior to creating requests ([#2446](https://github.com/astral-sh/uv/pull/2446)) - Error when direct URL requirements don't match `Requires-Python` ([#2196](https://github.com/astral-sh/uv/pull/2196)) ## 0.1.21 ### Enhancements - Loosen `.dist-info` validation to accept arbitrary versions ([#2441](https://github.com/astral-sh/uv/pull/2441)) ### Bug fixes - Fix macOS architecture detection on i386 machines ([#2454](https://github.com/astral-sh/uv/pull/2454)) ## 0.1.22 ### Enhancements - Add support for PyTorch-style local version semantics ([#2430](https://github.com/astral-sh/uv/pull/2430)) - Add support for Hatch's `{root:uri}` paths in editable installs ([#2492](https://github.com/astral-sh/uv/pull/2492)) - Implement `uv pip check` ([#2397](https://github.com/astral-sh/uv/pull/2397)) - Add pip-like linehaul information to user agent ([#2493](https://github.com/astral-sh/uv/pull/2493)) - Add additional ARM targets to release ([#2417](https://github.com/astral-sh/uv/pull/2417)) ### Bug fixes - Allow direct file path requirements to include fragments ([#2502](https://github.com/astral-sh/uv/pull/2502)) - Avoid panicking on cannot-be-a-base URLs ([#2461](https://github.com/astral-sh/uv/pull/2461)) - Drop `macosx_10_0` from compatible wheel tags on `aarch64` ([#2496](https://github.com/astral-sh/uv/pull/2496)) - Fix operating system detection on \*BSD ([#2505](https://github.com/astral-sh/uv/pull/2505)) - Fix priority of ABI tags ([#2489](https://github.com/astral-sh/uv/pull/2489)) - Fix priority of platform tags for manylinux ([#2483](https://github.com/astral-sh/uv/pull/2483)) - Make > operator exclude post and local releases ([#2471](https://github.com/astral-sh/uv/pull/2471)) - Re-add support for pyenv shims ([#2503](https://github.com/astral-sh/uv/pull/2503)) - Validate required package names against wheel package names ([#2516](https://github.com/astral-sh/uv/pull/2516)) ## 0.1.23 ### Enhancements - Implement `--no-strip-extras` to preserve extras in compilation ([#2555](https://github.com/astral-sh/uv/pull/2555)) - Preserve hashes for pinned packages when compiling without `--upgrade` ([#2532](https://github.com/astral-sh/uv/pull/2532)) - Add a `uv self update` command ([#2228](https://github.com/astral-sh/uv/pull/2228)) - Use relative paths for user-facing messages ([#2559](https://github.com/astral-sh/uv/pull/2559)) - Add `CUSTOM_COMPILE_COMMAND` support to `uv pip compile` ([#2554](https://github.com/astral-sh/uv/pull/2554)) - Add SHA384 and SHA512 hash algorithms ([#2534](https://github.com/astral-sh/uv/pull/2534)) - Treat uninstallable packages as warnings, rather than errors ([#2557](https://github.com/astral-sh/uv/pull/2557)) ### Bug fixes - Allow `VIRTUAL_ENV` to take precedence over `CONDA_PREFIX` ([#2574](https://github.com/astral-sh/uv/pull/2574)) - Ensure mtime of site packages is updated during wheel installation ([#2545](https://github.com/astral-sh/uv/pull/2545)) - Re-test validity after every lenient parsing change ([#2550](https://github.com/astral-sh/uv/pull/2550)) - Run interpreter discovery under `-I` mode ([#2552](https://github.com/astral-sh/uv/pull/2552)) - Search in both `purelib` and `platlib` for site-packages population ([#2537](https://github.com/astral-sh/uv/pull/2537)) - Fix wheel builds and uploads for musl ARM ([#2518](https://github.com/astral-sh/uv/pull/2518)) ### Documentation - Add `--link-mode` defaults to CLI ([#2549](https://github.com/astral-sh/uv/pull/2549)) - Add an example workflow for compiling the current environment's packages ([#1968](https://github.com/astral-sh/uv/pull/1968)) - Add `uv pip check diagnostics` to `PIP_COMPATIBILITY.md` ([#2544](https://github.com/astral-sh/uv/pull/2544)) ## 0.1.24 ### Breaking changes - `uv pip uninstall` no longer supports specifying targets with the `-e` / `--editable` flag ([#2577](https://github.com/astral-sh/uv/pull/2577)) ### Enhancements - Add a garbage collection mechanism to the CLI ([#1217](https://github.com/astral-sh/uv/pull/1217)) - Add progress reporting for named requirement resolution ([#2605](https://github.com/astral-sh/uv/pull/2605)) - Add support for parsing unnamed URL requirements ([#2567](https://github.com/astral-sh/uv/pull/2567)) - Add support for unnamed local directory requirements ([#2571](https://github.com/astral-sh/uv/pull/2571)) - Enable PEP 517 builds for unnamed requirements ([#2600](https://github.com/astral-sh/uv/pull/2600)) - Enable install audits without resolving named requirements ([#2575](https://github.com/astral-sh/uv/pull/2575)) - Enable unnamed requirements for direct URLs ([#2569](https://github.com/astral-sh/uv/pull/2569)) - Respect HTTP client options when reading remote requirements files ([#2434](https://github.com/astral-sh/uv/pull/2434)) - Use PEP 517 build hooks to resolve unnamed requirements ([#2604](https://github.com/astral-sh/uv/pull/2604)) - Use c-string literals and update trampolines ([#2590](https://github.com/astral-sh/uv/pull/2590)) - Support unnamed requirements directly in `uv pip uninstall` ([#2577](https://github.com/astral-sh/uv/pull/2577)) - Add support for unnamed Git and HTTP requirements ([#2578](https://github.com/astral-sh/uv/pull/2578)) - Make self-update an opt-in Cargo feature ([#2606](https://github.com/astral-sh/uv/pull/2606)) - Update minimum rust version (cargo) to 1.76 ([#2618](https://github.com/astral-sh/uv/pull/2618)) ### Bug fixes - Fix self-updates on Windows ([#2598](https://github.com/astral-sh/uv/pull/2598)) - Fix authentication with usernames that contain `@` characters ([#2592](https://github.com/astral-sh/uv/pull/2592)) - Do not error when there are warnings on Python interpreter stderr ([#2599](https://github.com/astral-sh/uv/pull/2599)) - Prevent discovery of cache gitignore when building distributions ([#2615](https://github.com/astral-sh/uv/pull/2615)) ### Rust API - Make `InstallDist.direct_url` public ([#2584](https://github.com/astral-sh/uv/pull/2584)) - Make `AllowedYanks` public ([#2608](https://github.com/astral-sh/uv/pull/2608)) ### Documentation - Fix badge to current CI status ([#2612](https://github.com/astral-sh/uv/pull/2612)) ## 0.1.25 ### Breaking changes - Limit overrides and constraints to `requirements.txt` format ([#2632](https://github.com/astral-sh/uv/pull/2632)) ### Enhancements - Accept `setup.py` and `setup.cfg` files in compile ([#2634](https://github.com/astral-sh/uv/pull/2634)) - Add `--no-binary` and `--only-binary` support to `requirements.txt` ([#2680](https://github.com/astral-sh/uv/pull/2680)) - Allow pre-releases, locals, and URLs in non-editable path requirements ([#2671](https://github.com/astral-sh/uv/pull/2671)) - Use PEP 517 to extract dynamic `pyproject.toml` metadata ([#2633](https://github.com/astral-sh/uv/pull/2633)) - Add `Editable project location` and `Required-by` to `pip show` ([#2589](https://github.com/astral-sh/uv/pull/2589)) - Avoid `prepare_metadata_for_build_wheel` calls for Hatch packages with dynamic dependencies ([#2645](https://github.com/astral-sh/uv/pull/2645)) - Fall back to PEP 517 hooks for non-compliant PEP 621 metadata ([#2662](https://github.com/astral-sh/uv/pull/2662)) - Support `file://localhost/` schemes ([#2657](https://github.com/astral-sh/uv/pull/2657)) - Use normal resolver in `pip sync` ([#2696](https://github.com/astral-sh/uv/pull/2696)) ### CLI - Disallow `pyproject.toml` from `pip uninstall -r` ([#2663](https://github.com/astral-sh/uv/pull/2663)) - Unhide `--emit-index-url` and `--emit-find-links` ([#2691](https://github.com/astral-sh/uv/pull/2691)) - Use dense formatting for requirement version specifiers in diagnostics ([#2601](https://github.com/astral-sh/uv/pull/2601)) ### Performance - Add an in-memory cache for Git references ([#2682](https://github.com/astral-sh/uv/pull/2682)) - Do not force-recompile `.pyc` files ([#2642](https://github.com/astral-sh/uv/pull/2642)) - Read package metadata from `pyproject.toml` when it is statically defined ([#2676](https://github.com/astral-sh/uv/pull/2676)) ### Bug fixes - Don't error on multiple matching index URLs ([#2627](https://github.com/astral-sh/uv/pull/2627)) - Extract local versions from direct URL requirements ([#2624](https://github.com/astral-sh/uv/pull/2624)) - Respect `--no-index` with `--find-links` in `pip sync` ([#2692](https://github.com/astral-sh/uv/pull/2692)) - Use `Scripts` folder for virtualenv activation prompt ([#2690](https://github.com/astral-sh/uv/pull/2690)) ## 0.1.26 ### Bug fixes - Bump simple cache version ([#2712](https://github.com/astral-sh/uv/pull/2712)) ## 0.1.27 ### Enhancements - Add `--exclude-editable` support to `pip-freeze` ([#2740](https://github.com/astral-sh/uv/pull/2740)) - Add `pyproject.toml` et al to list of prompted packages ([#2746](https://github.com/astral-sh/uv/pull/2746)) - Consider installed packages during resolution ([#2596](https://github.com/astral-sh/uv/pull/2596)) - Recursively allow URL requirements for local dependencies ([#2702](https://github.com/astral-sh/uv/pull/2702)) ### Configuration - Add `UV_RESOLUTION` environment variable for `--resolution` ([#2720](https://github.com/astral-sh/uv/pull/2720)) ### Bug fixes - Respect overrides in all direct-dependency iterators ([#2742](https://github.com/astral-sh/uv/pull/2742)) - Respect subdirectories when reading static metadata ([#2728](https://github.com/astral-sh/uv/pull/2728)) ## 0.1.28 ### Enhancements - Recursively resolve direct URL references upfront ([#2684](https://github.com/astral-sh/uv/pull/2684)) ### Performance - Populate the in-memory index when resolving lookahead URLs ([#2761](https://github.com/astral-sh/uv/pull/2761)) ### Bug fixes - Detect Fish via `FISH_VERSION` ([#2781](https://github.com/astral-sh/uv/pull/2781)) - Exclude installed distributions with multiple versions from consideration in the resolver ([#2779](https://github.com/astral-sh/uv/pull/2779)) - Resolve non-deterministic behavior in preferences due to site-packages ordering ([#2780](https://github.com/astral-sh/uv/pull/2780)) - Use canonical URL to key redirect map ([#2764](https://github.com/astral-sh/uv/pull/2764)) - Use distribution database and index for all pre-resolution phases ([#2766](https://github.com/astral-sh/uv/pull/2766)) - Fix `uv self update` on Linux ([#2783](https://github.com/astral-sh/uv/pull/2783)) ## 0.1.29 ### Enhancements - Allow conflicting Git URLs that refer to the same commit SHA ([#2769](https://github.com/astral-sh/uv/pull/2769)) - Allow package lookups across multiple indexes via explicit opt-in (`--index-strategy unsafe-any-match`) ([#2815](https://github.com/astral-sh/uv/pull/2815)) - Allow no-op `--no-compile` flag on CLI ([#2816](https://github.com/astral-sh/uv/pull/2816)) - Upgrade `rs-async-zip` to support data descriptors ([#2809](https://github.com/astral-sh/uv/pull/2809)) ### Bug fixes - Avoid unused extras check in `pip install` for source trees ([#2811](https://github.com/astral-sh/uv/pull/2811)) - Deduplicate editables during install commands ([#2820](https://github.com/astral-sh/uv/pull/2820)) - Fix windows lock race: lock exclusive after all try lock errors ([#2800](https://github.com/astral-sh/uv/pull/2800)) - Preserve `.git` suffixes and casing in Git dependencies ([#2789](https://github.com/astral-sh/uv/pull/2789)) - Respect Git tags and branches that look like short commits ([#2795](https://github.com/astral-sh/uv/pull/2795)) - Enable virtualenv creation on Windows with cpython-x86 ([#2707](https://github.com/astral-sh/uv/pull/2707)) ### Documentation - Document that uv is safe to run concurrently ([#2818](https://github.com/astral-sh/uv/pull/2818)) ## 0.1.30 ### Enhancements - Show resolution diagnostics after `pip install` ([#2829](https://github.com/astral-sh/uv/pull/2829)) ### Performance - Speed up cold-cache `urllib3`-`boto3`-`botocore` performance with batched prefetching ([#2452](https://github.com/astral-sh/uv/pull/2452)) ### Bug fixes - Backtrack on distributions with invalid metadata ([#2834](https://github.com/astral-sh/uv/pull/2834)) - Include LICENSE files in source distribution ([#2855](https://github.com/astral-sh/uv/pull/2855)) - Respect `--no-build` and `--no-binary` in `--find-links` ([#2826](https://github.com/astral-sh/uv/pull/2826)) - Respect cached local `--find-links` in install plan ([#2907](https://github.com/astral-sh/uv/pull/2907)) - Avoid panic with multiple confirmation handlers ([#2903](https://github.com/astral-sh/uv/pull/2903)) - Use scheme parsing to determine absolute vs. relative URLs ([#2904](https://github.com/astral-sh/uv/pull/2904)) - Remove additional 'because' in resolution failure messages ([#2849](https://github.com/astral-sh/uv/pull/2849)) - Use `miette` when printing `pip sync` resolution failures ([#2848](https://github.com/astral-sh/uv/pull/2848)) ## 0.1.31 ### Bug fixes - Ignore direct URL distributions in prefetcher ([#2943](https://github.com/astral-sh/uv/pull/2943)) ## 0.1.32 ### Enhancements - Add a `--require-hashes` command-line setting ([#2824](https://github.com/astral-sh/uv/pull/2824)) - Add hash-checking support to `install` and `sync` ([#2945](https://github.com/astral-sh/uv/pull/2945)) - Add support for URL requirements in `--generate-hashes` ([#2952](https://github.com/astral-sh/uv/pull/2952)) - Allow unnamed requirements for overrides ([#2999](https://github.com/astral-sh/uv/pull/2999)) - Enforce and backtrack on invalid versions in source metadata ([#2954](https://github.com/astral-sh/uv/pull/2954)) - Fall back to distributions without hashes in resolver ([#2949](https://github.com/astral-sh/uv/pull/2949)) - Implement `--emit-index-annotation` to annotate source index for each package ([#2926](https://github.com/astral-sh/uv/pull/2926)) - Log hard-link failures ([#3015](https://github.com/astral-sh/uv/pull/3015)) - Support free-threaded Python ([#2805](https://github.com/astral-sh/uv/pull/2805)) - Support unnamed requirements in `--require-hashes` ([#2993](https://github.com/astral-sh/uv/pull/2993)) - Respect link mode for builds, in `uv pip compile` and for `uv venv` seed packages ([#3016](https://github.com/astral-sh/uv/pull/3016)) - Force color for build error messages ([#3032](https://github.com/astral-sh/uv/pull/3032)) - Surface invalid metadata as hints in error reports ([#2850](https://github.com/astral-sh/uv/pull/2850)) ### Configuration - Add `UV_BREAK_SYSTEM_PACKAGES` environment variable ([#2995](https://github.com/astral-sh/uv/pull/2995)) ### CLI - Remove some restrictions in argument groups ([#3001](https://github.com/astral-sh/uv/pull/3001)) ### Bug fixes - Add `--find-links` source distributions to the registry cache ([#2986](https://github.com/astral-sh/uv/pull/2986)) - Allow comments after all `requirements.txt` entries ([#3018](https://github.com/astral-sh/uv/pull/3018)) - Avoid cache invalidation on credentials renewal ([#3010](https://github.com/astral-sh/uv/pull/3010)) - Avoid calling `normalize_path` with relative paths that extend beyond the current directory ([#3013](https://github.com/astral-sh/uv/pull/3013)) - Deduplicate symbolic links between `purelib` and `platlib` ([#3002](https://github.com/astral-sh/uv/pull/3002)) - Remove unused `--output-file` from `pip install` ([#2975](https://github.com/astral-sh/uv/pull/2975)) - Strip query string when parsing filename from HTML index ([#2961](https://github.com/astral-sh/uv/pull/2961)) - Update hashes without `--upgrade` if not present ([#2966](https://github.com/astral-sh/uv/pull/2966)) ## 0.1.33 ### Breaking changes Using the keyring requires a username to be provided on index URLs now. Previously, the username `oauth2accesstoken` was assumed. This will affect Google Artifact Registry users using `--keyring-provider subprocess` and an index URL without a username. The suggested fix is to add the required username to index URLs, e.g., `https://oauth2accesstoken@`. See [#2976](https://github.com/astral-sh/uv/pull/2976#discussion_r1566521453) for details. ### Enhancements - Allow passing a virtual environment path to `uv pip --python` ([#3064](https://github.com/astral-sh/uv/pull/3064)) - Add compatibility argument for `pip list --outdated` ([#3055](https://github.com/astral-sh/uv/pull/3055)) ### CLI - Enable auto-wrapping of `--help` output ([#3058](https://github.com/astral-sh/uv/pull/3058)) - Show `--require-hashes` CLI argument in help ([#3093](https://github.com/astral-sh/uv/pull/3093)) ### Performance - Incorporate heuristics to improve package prioritization ([#3087](https://github.com/astral-sh/uv/pull/3087)) ### Bug fixes - Fix HTTP authentication when the password includes percent encoded characters (e.g., with Google Artifact Registry) ([#2822](https://github.com/astral-sh/uv/issues/2822)) - Use usernames from URLs when looking for credentials in netrc files and the keyring [#2563](https://github.com/astral-sh/uv/issues/2563)) - Skip `HEAD` requests for indexes that return 403 (e.g., PyPICloud) ([#3070](https://github.com/astral-sh/uv/pull/3070)) - Use kebab-case consistently ([#3080](https://github.com/astral-sh/uv/pull/3080)) - Show package name in no version for direct dependency error ([#3056](https://github.com/astral-sh/uv/pull/3056)) - Avoid erroring when encountering `.tar.bz2` source distributions ([#3069](https://github.com/astral-sh/uv/pull/3069)) ## 0.1.34 ### CLI - Allow `--python` and `--system` on `pip compile` ([#3115](https://github.com/astral-sh/uv/pull/3115)) - Remove `Option` for `--no-cache` ([#3129](https://github.com/astral-sh/uv/pull/3129)) - Rename `--compile` to `--compile-bytecode` ([#3102](https://github.com/astral-sh/uv/pull/3102)) - Accept `0`, `1`, and similar values for Boolean environment variables ([#3113](https://github.com/astral-sh/uv/pull/3113)) ### Configuration - Add `UV_REQUIRE_HASHES` environment variable ([#3125](https://github.com/astral-sh/uv/pull/3125)) - Add negation flags to the CLI ([#3050](https://github.com/astral-sh/uv/pull/3050)) ### Bug fixes - Avoid fetching unnecessary extra versions during resolution ([#3100](https://github.com/astral-sh/uv/pull/3100)) - Avoid deprioritizing recursive editables ([#3133](https://github.com/astral-sh/uv/pull/3133)) - Avoid treating localhost URLs as local file paths ([#3132](https://github.com/astral-sh/uv/pull/3132)) - Hide password in the index printed via `--emit-index-annotation` ([#3112](https://github.com/astral-sh/uv/pull/3112)) - Restore seeding of authentication cache from index URLs ([#3124](https://github.com/astral-sh/uv/pull/3124)) ## 0.1.35 ### Enhancements - Add a `--python-platform` argument to enable resolving against a target platform ([#3111](https://github.com/astral-sh/uv/pull/3111)) - Enforce HTTP timeouts on a per-read (rather than per-request) basis ([#3144](https://github.com/astral-sh/uv/pull/3144)) ### Bug fixes - Avoid preferring constrained over unconstrained packages ([#3148](https://github.com/astral-sh/uv/pull/3148)) - Allow `UV_SYSTEM_PYTHON=1` in addition to `UV_SYSTEM_PYTHON=true` ([#3136](https://github.com/astral-sh/uv/pull/3136)) ## 0.1.36 ### Enhancements - Add support for embedded Python on Windows ([#3161](https://github.com/astral-sh/uv/pull/3161)) - Add Docker image publishing to release pipeline ([#3155](https://github.com/astral-sh/uv/pull/3155)) ### Configuration - Add `UV_CONSTRAINT` environment variable to provide value for `--constraint` ([#3162](https://github.com/astral-sh/uv/pull/3162)) ### Bug fixes - Avoid waiting for metadata for `--no-deps` editables ([#3188](https://github.com/astral-sh/uv/pull/3188)) - Fix `venvlauncher.exe` reference in venv creation ([#3160](https://github.com/astral-sh/uv/pull/3160)) - Fix authentication for URLs with a shared realm ([#3130](https://github.com/astral-sh/uv/pull/3130)) - Restrict observed requirements to direct when `--no-deps` is specified ([#3191](https://github.com/astral-sh/uv/pull/3191)) ### Documentation - Add a versioning policy to the README ([#3151](https://github.com/astral-sh/uv/pull/3151)) ## 0.1.37 ### Enhancements - Change default HTTP read timeout to 30s ([#3182](https://github.com/astral-sh/uv/pull/3182)) - Add `--python-platform` to `sync` and `install` commands ([#3154](https://github.com/astral-sh/uv/pull/3154)) - Add ticks around error messages more consistently ([#3004](https://github.com/astral-sh/uv/pull/3004)) - Fix Docker publish permissions in release pipeline ([#3195](https://github.com/astral-sh/uv/pull/3195)) - Improve tracing for keyring provider ([#3207](https://github.com/astral-sh/uv/pull/3207)) ### Performance - Update keyring provider to be async ([#3089](https://github.com/astral-sh/uv/pull/3089)) ### Bug fixes - Fix fetch of credentials when cache is seeded with username ([#3206](https://github.com/astral-sh/uv/pull/3206)) ### Documentation - Improve `--python-platform` documentation ([#3202](https://github.com/astral-sh/uv/pull/3202)) ## 0.1.38 ### Enhancements - Add alternate manylinux targets to `--python-platform` ([#3229](https://github.com/astral-sh/uv/pull/3229)) - An enum and backticks for lookahead error ([#3216](https://github.com/astral-sh/uv/pull/3216)) - Upgrade macOS target to `12.0` ([#3228](https://github.com/astral-sh/uv/pull/3228)) - Add keyring logs for URL and host fetches ([#3212](https://github.com/astral-sh/uv/pull/3212)) - Combine unresolvable error dependency clauses with the same root ([#3225](https://github.com/astral-sh/uv/pull/3225)) ### CLI - Gave a better name to the `--color` placeholder ([#3226](https://github.com/astral-sh/uv/pull/3226)) - Warn when an unsupported Python version is encountered ([#3250](https://github.com/astral-sh/uv/pull/3250)) ### Configuration - Use directory instead of file when searching for `uv.toml` file ([#3203](https://github.com/astral-sh/uv/pull/3203)) ### Performance - Only perform fetches of credentials for a realm and username combination once ([#3237](https://github.com/astral-sh/uv/pull/3237)) - Unroll self-dependencies via extras ([#3230](https://github.com/astral-sh/uv/pull/3230)) - Use read-write locks instead of mutexes in authentication handling ([#3210](https://github.com/astral-sh/uv/pull/3210)) ### Bug fixes - Avoid removing quotes from requirements markers ([#3214](https://github.com/astral-sh/uv/pull/3214)) - Avoid adding extras when expanding constraints ([#3232](https://github.com/astral-sh/uv/pull/3232)) - Reinstall package when editable label is removed ([#3219](https://github.com/astral-sh/uv/pull/3219)) ### Documentation - Add `RAYON_NUM_THREADS` to environment variable docs ([#3223](https://github.com/astral-sh/uv/pull/3223)) - Document support for HTTP proxy variables ([#3247](https://github.com/astral-sh/uv/pull/3247)) - Fix documentation for `--python-platform` ([#3220](https://github.com/astral-sh/uv/pull/3220)) ## 0.1.39 ### Enhancements - Add `--target` support to `sync` and `install` ([#3257](https://github.com/astral-sh/uv/pull/3257)) - Implement `--index-strategy unsafe-best-match` ([#3138](https://github.com/astral-sh/uv/pull/3138)) ### Bug fixes - Fix `platform_machine` tag for `--python-platform` on macOS ARM ([#3267](https://github.com/astral-sh/uv/pull/3267)) ### Release - Build a separate ARM wheel for macOS ([#3268](https://github.com/astral-sh/uv/pull/3268)) - Use `macos-12` to build release wheels ([#3264](https://github.com/astral-sh/uv/pull/3264)) ## 0.1.40 ### Enhancements - Add `--allow-existing` to overwrite existing virtualenv ([#2548](https://github.com/astral-sh/uv/pull/2548)) - Respect and enable uninstalls of legacy editables (`.egg-link`) ([#3415](https://github.com/astral-sh/uv/pull/3415)) - Respect and enable uninstalls of existing `.egg-info` packages ([#3380](https://github.com/astral-sh/uv/pull/3380)) ### CLI - Accept `--no-upgrade`, `--no-refresh`, etc. on the CLI ([#3328](https://github.com/astral-sh/uv/pull/3328)) ### Configuration - Expose `UV_NO_BUILD_ISOLATION` as environment variable ([#3318](https://github.com/astral-sh/uv/pull/3318)) - Expose `UV_PYTHON` as an environment variable ([#3284](https://github.com/astral-sh/uv/pull/3284)) - Expose `UV_LINK_MODE` as environment variable ([#3315](https://github.com/astral-sh/uv/pull/3315)) - Add `UV_CUSTOM_COMPILE_COMMAND` to environment variable docs ([#3382](https://github.com/astral-sh/uv/pull/3382)) ### Bug fixes - Ignore 401 HTTP responses with multiple indexes ([#3292](https://github.com/astral-sh/uv/pull/3292)) - Avoid panic for file URLs ([#3306](https://github.com/astral-sh/uv/pull/3306)) - Quote version parse errors consistently ([#3325](https://github.com/astral-sh/uv/pull/3325)) - Detect current environment when `uv` is invoked from within a virtualenv ([#3379](https://github.com/astral-sh/uv/pull/3379)) - Unset target when creating virtual environments ([#3362](https://github.com/astral-sh/uv/pull/3362)) - Update activation scripts from virtualenv ([#3376](https://github.com/astral-sh/uv/pull/3376)) - Use canonical URLs in satisfaction check ([#3373](https://github.com/astral-sh/uv/pull/3373)) ### Preview features - Add basic `tool.uv.sources` support ([#3263](https://github.com/astral-sh/uv/pull/3263)) - Improve non-git error message ([#3403](https://github.com/astral-sh/uv/pull/3403)) - Preserve given for `tool.uv.sources` paths ([#3412](https://github.com/astral-sh/uv/pull/3412)) - Restore verbatim in error message ([#3402](https://github.com/astral-sh/uv/pull/3402)) - Use preview mode for tool.uv.sources ([#3277](https://github.com/astral-sh/uv/pull/3277)) - Use top-level `--isolated` for `uv run` ([#3431](https://github.com/astral-sh/uv/pull/3431)) - add basic "install from lock file" operation ([#3340](https://github.com/astral-sh/uv/pull/3340)) - uv-resolver: add initial version of universal lock file format ([#3314](https://github.com/astral-sh/uv/pull/3314)) ## 0.1.41 ### Bug fixes - Remove unconstrained version error from requirements ([#3443](https://github.com/astral-sh/uv/pull/3443)) ## 0.1.42 This release includes stabilized support for persistent configuration in uv. uv will now read project configuration from a `pyproject.toml` or `uv.toml` file in the current directory or any parent directory, along with user configuration at `~/.config/uv/uv.toml` (or `$XDG_CONFIG_HOME/uv/uv.toml`) on macOS and Linux, and `%APPDATA%\uv\uv.toml` on Windows. See: [Persistent Configuration](https://github.com/astral-sh/uv?tab=readme-ov-file#persistent-configuration) for more. ### Enhancements - Respect `MACOSX_DEPLOYMENT_TARGET` in `--python-platform` ([#3470](https://github.com/astral-sh/uv/pull/3470)) ### Configuration - Add documentation for persistent configuration ([#3467](https://github.com/astral-sh/uv/pull/3467)) - Add JSON Schema export to SchemaStore ([#3461](https://github.com/astral-sh/uv/pull/3461)) - Merge user and workspace settings ([#3462](https://github.com/astral-sh/uv/pull/3462)) ### Bug fixes - Use Metadata10 to parse PKG-INFO of legacy editable ([#3450](https://github.com/astral-sh/uv/pull/3450)) - Apply normcase to line from easy-install.pth ([#3451](https://github.com/astral-sh/uv/pull/3451)) - Upgrade `async_http_range_reader` to v0.8.0 to respect redirects in range requests ([#3460](https://github.com/astral-sh/uv/pull/3460)) - Use last non-EOL version for `--python-platform` macOS ([#3469](https://github.com/astral-sh/uv/pull/3469)) ### Preview features - Use environment layering for `uv run --with` ([#3447](https://github.com/astral-sh/uv/pull/3447)) - Warn when missing minimal bounds when using `tool.uv.sources` ([#3452](https://github.com/astral-sh/uv/pull/3452)) ## 0.1.43 ### Enhancements - Annotate sources of requirements in `pip compile` output ([#3269](https://github.com/astral-sh/uv/pull/3269)) - Track origin for `setup.py` files and friends ([#3481](https://github.com/astral-sh/uv/pull/3481)) ### Configuration - Consolidate concurrency limits and expose as environment variables ([#3493](https://github.com/astral-sh/uv/pull/3493)) ### Release - Use manylinux: auto to enable `musllinux_1_2` aarch64 builds ([#3444](https://github.com/astral-sh/uv/pull/3444)) - Enable musllinux_1_1 wheels ([#3523](https://github.com/astral-sh/uv/pull/3523)) ### Bug fixes - Avoid keyword arguments for PEP 517 build hooks ([#3517](https://github.com/astral-sh/uv/pull/3517)) - Apply advisory locks when building source distributions ([#3525](https://github.com/astral-sh/uv/pull/3525)) - Avoid attempting to build editables when fetching metadata ([#3563](https://github.com/astral-sh/uv/pull/3563)) - Clone individual files on windows ReFS ([#3551](https://github.com/astral-sh/uv/pull/3551)) - Filter irrelevant requirements from source annotations ([#3479](https://github.com/astral-sh/uv/pull/3479)) - Make cache clearing robust to directories without read permissions ([#3524](https://github.com/astral-sh/uv/pull/3524)) - Respect constraints on editable dependencies ([#3554](https://github.com/astral-sh/uv/pull/3554)) - Skip Python 2 versions when locating Python ([#3476](https://github.com/astral-sh/uv/pull/3476)) - Make `--isolated` a global argument ([#3558](https://github.com/astral-sh/uv/pull/3558)) - Allow unknown `pyproject.toml` fields ([#3511](https://github.com/astral-sh/uv/pull/3511)) - Change error value detection for glibc ([#3487](https://github.com/astral-sh/uv/pull/3487)) ### Preview features - Create virtualenv if it doesn't exist in project API ([#3499](https://github.com/astral-sh/uv/pull/3499)) - Discover `uv run` projects hierarchically ([#3494](https://github.com/astral-sh/uv/pull/3494)) - Read and write `uv.lock` based on project root ([#3497](https://github.com/astral-sh/uv/pull/3497)) - Read package name from `pyproject.toml` in `uv run` ([#3496](https://github.com/astral-sh/uv/pull/3496)) - Rebrand workspace API as project API ([#3489](https://github.com/astral-sh/uv/pull/3489)) ## 0.1.44 ### Release Reverts "Use manylinux: auto to enable `musllinux_1_2` aarch64 builds ([#3444](https://github.com/astral-sh/uv/pull/3444))" The manylinux change appeared to introduce SSL errors when building aarch64 Docker images, e.g., > invalid peer certificate: BadSignature The v0.1.42 behavior for aarch64 manylinux builds is restored in this release. See [#3576](https://github.com/astral-sh/uv/pull/3576) ## 0.1.45 ### Enhancements - Parse and store extras on editable requirements ([#3629](https://github.com/astral-sh/uv/pull/3629)) - Allow local versions in wheel filenames ([#3596](https://github.com/astral-sh/uv/pull/3596)) - Create lib64 symlink for 64-bit, non-macOS, POSIX environments ([#3584](https://github.com/astral-sh/uv/pull/3584)) ### Configuration - Add `UV_CONCURRENT_INSTALLS` variable in favor of `RAYON_NUM_THREADS` ([#3646](https://github.com/astral-sh/uv/pull/3646)) - Add serialization and deserialization for `--find-links` ([#3619](https://github.com/astral-sh/uv/pull/3619)) - Apply combination logic to merge CLI and persistent configuration ([#3618](https://github.com/astral-sh/uv/pull/3618)) ### Performance - Parallelize resolver ([#3627](https://github.com/astral-sh/uv/pull/3627)) ### Bug fixes - Reduce sensitivity of unknown option error to discard Python 2 interpreters ([#3580](https://github.com/astral-sh/uv/pull/3580)) - Respect installed packages in `uv run` ([#3603](https://github.com/astral-sh/uv/pull/3603)) - Separate cache construction from initialization ([#3607](https://github.com/astral-sh/uv/pull/3607)) - Add missing `"directory"` branch in source match ([#3608](https://github.com/astral-sh/uv/pull/3608)) - Fix source annotation in pip compile `annotation-style=line` output ([#3637](https://github.com/astral-sh/uv/pull/3637)) - Run cargo update to pull in h2 ([#3638](https://github.com/astral-sh/uv/pull/3638)) - URL-decode hashes in HTML fragments ([#3655](https://github.com/astral-sh/uv/pull/3655)) - Always print JSON output with `--format` json ([#3671](https://github.com/astral-sh/uv/pull/3671)) ### Documentation - Add `UV_CONFIG_FILE` environment variable to documentation ([#3653](https://github.com/astral-sh/uv/pull/3653)) - Explicitly mention `--user` in compatibility guide ([#3666](https://github.com/astral-sh/uv/pull/3666)) ### Release - Add musl ppc64le support ([#3537](https://github.com/astral-sh/uv/pull/3537)) - Retag musl aarch64 for manylinux2014 ([#3624](https://github.com/astral-sh/uv/pull/3624)) ### Preview features - Add direct URL conversion to lockfile ([#3633](https://github.com/astral-sh/uv/pull/3633)) - Add hashes and versions to all distributions ([#3589](https://github.com/astral-sh/uv/pull/3589)) - Add local path conversions from lockfile ([#3609](https://github.com/astral-sh/uv/pull/3609)) - Add missing `"directory"` branch in source match ([#3608](https://github.com/astral-sh/uv/pull/3608)) - Add registry file size to lockfile ([#3652](https://github.com/astral-sh/uv/pull/3652)) - Add registry source distribution support to lockfile ([#3649](https://github.com/astral-sh/uv/pull/3649)) - Refactor editables for supporting them in bluejay commands ([#3639](https://github.com/astral-sh/uv/pull/3639)) - Rename `sourcedist` to `sdist` in lockfile ([#3590](https://github.com/astral-sh/uv/pull/3590)) - Respect installed packages in `uv run` ([#3603](https://github.com/astral-sh/uv/pull/3603)) - Support lossless serialization for Git dependencies in lockfile ([#3630](https://github.com/astral-sh/uv/pull/3630)) uv-0.9.17+ds1/changelogs/0.2.x.md000066400000000000000000002465241520155276700161550ustar00rootroot00000000000000# Changelog 0.2.x ## 0.2.0 Starting with this release, uv will use the **minor** version tag to indicate breaking changes. ### Breaking In this release, discovery of Python interpreters has changed. These changes should have a limited effect in most use-cases, however, it has been marked as a breaking change because the interpreter used by uv could change in some edge cases. When multiple Python interpreters are installed, uv makes an attempt to find the exact version you requested. Previously, uv would stop at the first Python interpreter it discovered — if the interpreter did not satisfy the requested version, uv would fail. Now, uv will query multiple Python interpreters until it finds the requested version, skipping interpreters that are broken or do not satisfy the request. Additionally, uv now allows requests for interpreter implementations such as `pypy` and `cpython`. For example, the request `--python cpython` will ignore a `python` executable that's implemented by `pypy`. These requests may also include a version, e.g., `--python pypy@3.10`. By default, uv will accept _any_ interpreter implementation. In summary, the following Python interpreter requests are now allowed: - A Python version without an implementation name, e.g., `3.10` - A path to a directory containing a Python installation, e.g., `./foo/.venv` - A path to a Python executable, e.g., `~/bin/python` - A Python implementation without a version, e.g., `pypy` or `cpython` - A Python implementation name and version, e.g., `pypy3.8` or `pypy@3.8` - The name of a Python executable (for lookup in the `PATH`), e.g., `foopython3` Previously, interpreter requests that were not versions or paths were always treated as executable names. To align the user expectations, uv now respects the interpreter that starts it. For example, `python -m uv ...` will now prefer the `python` interpreter that was used to start uv instead of searching for a virtual environment. We now check if discovered interpreters are virtual environments. This means that setting `VIRTUAL_ENV` to a Python installation directory that is _not_ a virtual environment will no longer work. Instead, use `--system` or `--python ` to request the interpreter. ### Enhancements - Rewrite Python interpreter discovery ([#3266](https://github.com/astral-sh/uv/pull/3266)) - Add support for requesting `pypy` interpreters by implementation name ([#3706](https://github.com/astral-sh/uv/pull/3706)) - Discover and prefer the parent interpreter when invoked with `python -m uv` [#3736](https://github.com/astral-sh/uv/pull/3736) - Add PEP 714 support for HTML API client ([#3697](https://github.com/astral-sh/uv/pull/3697)) - Add PEP 714 support for JSON API client ([#3698](https://github.com/astral-sh/uv/pull/3698)) - Write relative paths with unnamed requirement syntax ([#3682](https://github.com/astral-sh/uv/pull/3682)) - Allow relative Python executable paths in Windows trampoline ([#3717](https://github.com/astral-sh/uv/pull/3717)) - Add support for clang and msvc in missing header error ([#3753](https://github.com/astral-sh/uv/pull/3753)) ### CLI - Allow `--constraint` files in `pip sync` ([#3741](https://github.com/astral-sh/uv/pull/3741)) - Allow `--config-file` to be passed before or after command name ([#3730](https://github.com/astral-sh/uv/pull/3730)) - Make `--offline` a global argument ([#3729](https://github.com/astral-sh/uv/pull/3729)) ### Performance - Improve performance in complex resolutions by reducing cost of PubGrub package clones ([#3688](https://github.com/astral-sh/uv/pull/3688)) ### Bug fixes - Evaluate arbitrary markers to `false` ([#3681](https://github.com/astral-sh/uv/pull/3681)) - Improve `DirWithoutEntrypoint` error message ([#3690](https://github.com/astral-sh/uv/pull/3690)) - Improve display of root package in range errors ([#3711](https://github.com/astral-sh/uv/pull/3711)) - Propagate URL errors in verbatim parsing ([#3720](https://github.com/astral-sh/uv/pull/3720)) - Report yanked packages in `--dry-run` ([#3740](https://github.com/astral-sh/uv/pull/3740)) ### Release - Drop native `manylinux` wheel in favor of dual-tagged wheel ([#3685](https://github.com/astral-sh/uv/pull/3685)) - The `python-patch` test feature is no longer on by default and must be manually enabled to test patch version behavior ([#3746](https://github.com/astral-sh/uv/pull/3746)) ### Documentation - Add `--prefix` link to compatibility guide ([#3734](https://github.com/astral-sh/uv/pull/3734)) - Add `--only-binary` to compatibility guide ([#3735](https://github.com/astral-sh/uv/pull/3735)) - Add instructions for building and updating `uv-trampolines` ([#3731](https://github.com/astral-sh/uv/pull/3731)) - Add notes for testing on Windows ([#3658](https://github.com/astral-sh/uv/pull/3658)) ### Preview features - Add initial implementation of `uv tool run` ([#3657](https://github.com/astral-sh/uv/pull/3657)) - Add offline support to `uv tool run` and `uv run` ([#3676](https://github.com/astral-sh/uv/pull/3676)) - Better error message for `uv run` failures ([#3691](https://github.com/astral-sh/uv/pull/3691)) - Discover workspaces without using them in resolution ([#3585](https://github.com/astral-sh/uv/pull/3585)) - Support editables in `uv sync` ([#3692](https://github.com/astral-sh/uv/pull/3692)) - Track editable requirements in lockfile ([#3725](https://github.com/astral-sh/uv/pull/3725)) ## 0.2.1 ### Bug fixes - Re-added the dynamically-linked Linux binary ([#3762](https://github.com/astral-sh/uv/pull/3762)) ### Preview features - Allow users to specify a custom source package to `uv tool run` ([#3677](https://github.com/astral-sh/uv/pull/3677)) ## 0.2.2 ### Enhancements - Report yanks for cached and resolved packages ([#3772](https://github.com/astral-sh/uv/pull/3772)) - Improve error message when default Python is not found ([#3770](https://github.com/astral-sh/uv/pull/3770)) ### Bug fixes - Do not treat interpreters discovered via `CONDA_PREFIX` as system interpreters ([#3771](https://github.com/astral-sh/uv/pull/3771)) ## 0.2.3 ### Enhancements - Incorporate build tag into wheel prioritization ([#3781](https://github.com/astral-sh/uv/pull/3781)) - Avoid displaying log for satisfied editables if none are requested ([#3795](https://github.com/astral-sh/uv/pull/3795)) - Improve logging during interpreter discovery ([#3790](https://github.com/astral-sh/uv/pull/3790)) - Improve logging for environment locking ([#3792](https://github.com/astral-sh/uv/pull/3792)) - Improve logging of interpreter implementation ([#3791](https://github.com/astral-sh/uv/pull/3791)) - Remove extra details from interpreter query traces ([#3803](https://github.com/astral-sh/uv/pull/3803)) - Use colon more consistently in error messages ([#3788](https://github.com/astral-sh/uv/pull/3788)) ### Configuration - Add JSON alias for `unsafe-any-match` ([#3820](https://github.com/astral-sh/uv/pull/3820)) ### Release - Remove redundant dynamically linked Linux binary again (#3762)" ([#3778](https://github.com/astral-sh/uv/pull/3778)) - Remove `aarch64-unknown-linux-gnu` from list of expected binaries ([#3761](https://github.com/astral-sh/uv/pull/3761)) ### Bug fixes - Always include package names for Git and HTTPS dependencies ([#3821](https://github.com/astral-sh/uv/pull/3821)) - Fix interpreter cache collisions for relative virtualenv paths ([#3823](https://github.com/astral-sh/uv/pull/3823)) - Ignore unnamed requirements in preferences ([#3826](https://github.com/astral-sh/uv/pull/3826)) - Search for `python3` in unix virtual environments ([#3798](https://github.com/astral-sh/uv/pull/3798)) - Use a cross-platform representation for relative paths in `pip compile` ([#3804](https://github.com/astral-sh/uv/pull/3804)) ### Preview features - Allow specification of additional requirements in `uv tool run` ([#3678](https://github.com/astral-sh/uv/pull/3678)) ## 0.2.4 ### CLI - Allow `--system` and `--python` to be passed together ([#3830](https://github.com/astral-sh/uv/pull/3830)) ### Bug fixes - Ignore `libc` on other platforms ([#3825](https://github.com/astral-sh/uv/pull/3825)) ## 0.2.5 ### Enhancements - Add support for x86 Windows ([#3873](https://github.com/astral-sh/uv/pull/3873)) - Add support for `prepare_metadata_for_build_editable` hook ([#3870](https://github.com/astral-sh/uv/pull/3870)) - Add concurrent progress bars for downloads ([#3252](https://github.com/astral-sh/uv/pull/3252)) ### Bug fixes - Update bundled Python URLs and add `"arm"` architecture variant ([#3855](https://github.com/astral-sh/uv/pull/3855)) ### Preview features - Add context to failed `uv tool run` ([#3882](https://github.com/astral-sh/uv/pull/3882)) - Add persistent storage of installed toolchains ([#3797](https://github.com/astral-sh/uv/pull/3797)) - Gate discovery of managed toolchains with preview ([#3835](https://github.com/astral-sh/uv/pull/3835)) - Initial workspace support ([#3705](https://github.com/astral-sh/uv/pull/3705)) - Move editable discovery behind `--preview` for now ([#3884](https://github.com/astral-sh/uv/pull/3884)) ## 0.2.6 ### Enhancements - Support PEP 508 requirements for editables ([#3946](https://github.com/astral-sh/uv/pull/3946)) - Discard fragments when parsing unnamed URLs ([#3940](https://github.com/astral-sh/uv/pull/3940)) - Port all Git functionality to use Git CLI ([#3833](https://github.com/astral-sh/uv/pull/3833)) - Use statically linked C runtime on Windows ([#3966](https://github.com/astral-sh/uv/pull/3966)) ### Bug fixes - Disable concurrent progress bars in Jupyter Notebooks ([#3890](https://github.com/astral-sh/uv/pull/3890)) - Initialize multi-progress state before individual bars ([#3901](https://github.com/astral-sh/uv/pull/3901)) - Add missing `i686` alias for `x86` ([#3899](https://github.com/astral-sh/uv/pull/3899)) - Add missing `ppc64le` alias for `powerpc64le` ([#3963](https://github.com/astral-sh/uv/pull/3963)) - Fix reference to `--python-version` patch behavior ([#3989](https://github.com/astral-sh/uv/pull/3989)) - Avoid race condition in `OnceMap` ([#3987](https://github.com/astral-sh/uv/pull/3987)) ### Preview features - Add `uv run --package` ([#3864](https://github.com/astral-sh/uv/pull/3864)) - Add index URL parameters to Project CLI ([#3984](https://github.com/astral-sh/uv/pull/3984)) - Avoid re-adding solutions to forked state ([#3967](https://github.com/astral-sh/uv/pull/3967)) - Draft for user docs for workspaces ([#3866](https://github.com/astral-sh/uv/pull/3866)) - Include all extras when generating lockfile ([#3912](https://github.com/astral-sh/uv/pull/3912)) - Remove unstable uv lock from pip interface ([#3970](https://github.com/astral-sh/uv/pull/3970)) - Respect resolved Git SHAs in `uv lock` ([#3956](https://github.com/astral-sh/uv/pull/3956)) - Use lockfile in `uv run` ([#3894](https://github.com/astral-sh/uv/pull/3894)) - Use lockfile versions as resolution preferences ([#3921](https://github.com/astral-sh/uv/pull/3921)) - Use universal resolution in `uv lock` ([#3969](https://github.com/astral-sh/uv/pull/3969)) ## 0.2.7 ### CLI - Support `NO_COLOR` and `FORCE_COLOR` environment variables ([#3979](https://github.com/astral-sh/uv/pull/3979)) ### Performance - Avoid building packages with dynamic versions ([#4058](https://github.com/astral-sh/uv/pull/4058)) - Avoid work-stealing in bytecode compilation ([#4004](https://github.com/astral-sh/uv/pull/4004)) ### Bug fixes - Avoid dropping `pip sync` requirements with markers ([#4051](https://github.com/astral-sh/uv/pull/4051)) - Bias towards local directories for bare editable requirements ([#3995](https://github.com/astral-sh/uv/pull/3995)) - Preserve fragments when applying verbatim redirects ([#4038](https://github.com/astral-sh/uv/pull/4038)) - Avoid 'are incompatible' for singular bounded versions ([#4003](https://github.com/astral-sh/uv/pull/4003)) ### Preview features - Fix a bug where no warning is output when parsing of workspace settings fails. ([#4014](https://github.com/astral-sh/uv/pull/4014)) - Normalize extras in lockfile ([#3958](https://github.com/astral-sh/uv/pull/3958)) - Respect `Requires-Python` in universal resolution ([#3998](https://github.com/astral-sh/uv/pull/3998)) ## 0.2.8 ### Bug fixes - Fix `uv venv` handling when `VIRTUAL_ENV` refers to an non-existent environment ([#4073](https://github.com/astral-sh/uv/pull/4073)) ### Preview features - Default to current Python minor if `Requires-Python` is absent ([#4070](https://github.com/astral-sh/uv/pull/4070)) - Enforce `Requires-Python` when syncing ([#4068](https://github.com/astral-sh/uv/pull/4068)) - Track supported Python range in lockfile ([#4065](https://github.com/astral-sh/uv/pull/4065)) ## 0.2.9 ### Enhancements - Respect existing `.egg-link` files in site packages ([#4082](https://github.com/astral-sh/uv/pull/4082)) ### Bug fixes - Avoid extra-only filtering for constraints ([#4095](https://github.com/astral-sh/uv/pull/4095)) ### Documentation - Add install link for specific version to README ([#4105](https://github.com/astral-sh/uv/pull/4105)) ### Preview features - Add support for development dependencies ([#4036](https://github.com/astral-sh/uv/pull/4036)) - Avoid enforcing distribution ID uniqueness for extras ([#4104](https://github.com/astral-sh/uv/pull/4104)) - Ignore upper-bounds on `Requires-Python` ([#4086](https://github.com/astral-sh/uv/pull/4086)) ## 0.2.10 ### Enhancements - Accept `file://` URLs for `requirements.txt` et all references ([#4145](https://github.com/astral-sh/uv/pull/4145)) - Add support for `--prefix` ([#4085](https://github.com/astral-sh/uv/pull/4085)) ### CLI - Add `pyproject.toml` to CLI help ([#4181](https://github.com/astral-sh/uv/pull/4181)) - Drop "registry" prefix from request timeout log ([#4144](https://github.com/astral-sh/uv/pull/4144)) ### Bug fixes - Allow transitive URLs via recursive extras ([#4155](https://github.com/astral-sh/uv/pull/4155)) - Avoid pre-fetching for unbounded minimum versions ([#4149](https://github.com/astral-sh/uv/pull/4149)) - Avoid showing dev hints for Python requirements ([#4111](https://github.com/astral-sh/uv/pull/4111)) - Include non-standard ports in keyring host queries ([#4061](https://github.com/astral-sh/uv/pull/4061)) - Omit URL dependencies from pre-release hints ([#4140](https://github.com/astral-sh/uv/pull/4140)) - Improve static metadata extraction for Poetry projects ([#4182](https://github.com/astral-sh/uv/pull/4182)) ### Documentation - Document bytecode compilation in pip compatibility guide ([#4195](https://github.com/astral-sh/uv/pull/4195)) - Fix PEP 508 link in preview doc `specifying_dependencies` ([#4158](https://github.com/astral-sh/uv/pull/4158)) - Clarify role of `--system` flag ([#4031](https://github.com/astral-sh/uv/pull/4031)) ### Preview features - Add `uv toolchain install` ([#4164](https://github.com/astral-sh/uv/pull/4164)) - Add `uv toolchain list` ([#4163](https://github.com/astral-sh/uv/pull/4163)) - Add extra and dev dependency validation to lockfile ([#4112](https://github.com/astral-sh/uv/pull/4112)) - Add markers to edges rather than distributions ([#4166](https://github.com/astral-sh/uv/pull/4166)) - Cap `Requires-Python` comparisons at the patch version ([#4150](https://github.com/astral-sh/uv/pull/4150)) - Do not create a virtual environment when locking ([#4147](https://github.com/astral-sh/uv/pull/4147)) - Don't panic with invalid wheel source ([#4191](https://github.com/astral-sh/uv/pull/4191)) - Fetch managed toolchains in `uv run` ([#4143](https://github.com/astral-sh/uv/pull/4143)) - Fix PEP 508 link in preview doc `specifying_dependencies` ([#4158](https://github.com/astral-sh/uv/pull/4158)) - Ignore tags in universal resolution ([#4174](https://github.com/astral-sh/uv/pull/4174)) - Implement `Toolchain::find_or_fetch` and use in `uv venv --preview` ([#4138](https://github.com/astral-sh/uv/pull/4138)) - Lock all packages in workspace ([#4016](https://github.com/astral-sh/uv/pull/4016)) - Recreate project environment if `--python` or `requires-python` doesn't match ([#3945](https://github.com/astral-sh/uv/pull/3945)) - Respect `--find-links` in `lock` and `sync` ([#4183](https://github.com/astral-sh/uv/pull/4183)) - Set `--dev` to default for `uv run` and `uv sync` ([#4118](https://github.com/astral-sh/uv/pull/4118)) - Track `Markers` via a PubGrub package variant ([#4123](https://github.com/astral-sh/uv/pull/4123)) - Use union of `requires-python` in workspace ([#4041](https://github.com/astral-sh/uv/pull/4041)) - make universal resolver fork only when markers are disjoint ([#4135](https://github.com/astral-sh/uv/pull/4135)) ## 0.2.11 ### Enhancements - Add support for local directories with `--index-url` ([#4226](https://github.com/astral-sh/uv/pull/4226)) - Add mTLS support ([#4171](https://github.com/astral-sh/uv/pull/4171)) - Allow version specifiers to be used in Python version requests ([#4214](https://github.com/astral-sh/uv/pull/4214)) ### Bug fixes - Always install as editable when duplicate dependencies are requested ([#4208](https://github.com/astral-sh/uv/pull/4208)) - Avoid crash with `XDG_CONFIG_HOME=/dev/null` ([#4200](https://github.com/astral-sh/uv/pull/4200)) - Improve handling of missing interpreters during discovery ([#4218](https://github.com/astral-sh/uv/pull/4218)) - Make missing `METADATA` file a recoverable error ([#4247](https://github.com/astral-sh/uv/pull/4247)) - Represent build tag as `u64` ([#4253](https://github.com/astral-sh/uv/pull/4253)) ### Documentation - Document Windows 10 requirement ([#4210](https://github.com/astral-sh/uv/pull/4210)) ### Release - Re-add `aarch64-unknown-linux-gnu` binary to release assets ([#4254](https://github.com/astral-sh/uv/pull/4254)) ### Preview features - Add changelog for preview changes ([#4251](https://github.com/astral-sh/uv/pull/4251)) - Allow direct URLs for dev dependencies ([#4233](https://github.com/astral-sh/uv/pull/4233)) - Create temporary environments in dedicated cache bucket ([#4223](https://github.com/astral-sh/uv/pull/4223)) - Improve output when an older toolchain version is already installed ([#4248](https://github.com/astral-sh/uv/pull/4248)) - Initial implementation of `uv add` and `uv remove` ([#4193](https://github.com/astral-sh/uv/pull/4193)) - Refactor project interpreter request for `requires-python` specifiers ([#4216](https://github.com/astral-sh/uv/pull/4216)) - Replace `toolchain fetch` with `toolchain install` ([#4228](https://github.com/astral-sh/uv/pull/4228)) - Support locking relative paths ([#4205](https://github.com/astral-sh/uv/pull/4205)) - Warn when 'requires-python' does not include a lower bound ([#4234](https://github.com/astral-sh/uv/pull/4234)) ## 0.2.12 ### Enhancements - Allow specific `--only-binary` and `--no-binary` packages to override `:all:` ([#4067](https://github.com/astral-sh/uv/pull/4067)) - Flatten ORs and ANDs in marker construction ([#4260](https://github.com/astral-sh/uv/pull/4260)) - Skip invalid interpreters when searching for requested interpreter executable name ([#4308](https://github.com/astral-sh/uv/pull/4308)) - Display keyring stderr during queries ([#4343](https://github.com/astral-sh/uv/pull/4343)) - Allow discovery of uv binary relative to package root ([#4336](https://github.com/astral-sh/uv/pull/4336)) - Use relative path for `lib64` symlink ([#4268](https://github.com/astral-sh/uv/pull/4268)) ### CLI - Add uv version to debug output ([#4259](https://github.com/astral-sh/uv/pull/4259)) - Allow `--no-binary` with `uv pip compile` ([#4301](https://github.com/astral-sh/uv/pull/4301)) - Hide `--no-system` from the CLI ([#4292](https://github.com/astral-sh/uv/pull/4292)) - Make `--reinstall`, `--upgrade`, and `--refresh` shared arguments ([#4319](https://github.com/astral-sh/uv/pull/4319)) ### Configuration - Add `UV_EXCLUDE_NEWER` environment variable ([#4287](https://github.com/astral-sh/uv/pull/4287)) ### Bug fixes - Allow normalization to completely eliminate markers ([#4271](https://github.com/astral-sh/uv/pull/4271)) - Avoid treating direct path archives as always dynamic ([#4283](https://github.com/astral-sh/uv/pull/4283)) - De-duplicate markers during normalization ([#4263](https://github.com/astral-sh/uv/pull/4263)) - Fix incorrect parsing of requested Python version as empty version specifiers ([#4289](https://github.com/astral-sh/uv/pull/4289)) - Suggest correct command to create a virtual environment when encountering externally managed interpreters ([#4314](https://github.com/astral-sh/uv/pull/4314)) - Use consistent order for extra groups in lockfile ([#4275](https://github.com/astral-sh/uv/pull/4275)) ### Documentation - Add `pip-compile` defaults to `PIP_COMPATIBILITY.md` ([#4302](https://github.com/astral-sh/uv/pull/4302)) - Expand on `pip-compile` default differences ([#4306](https://github.com/astral-sh/uv/pull/4306)) - Tweak copy on some command-line arguments ([#4293](https://github.com/astral-sh/uv/pull/4293)) - Move the preview changelog so the GitHub Release shows stable changes ([#4290](https://github.com/astral-sh/uv/pull/4290)) ### Preview features - Add `--force` option to `uv toolchain install` ([#4313](https://github.com/astral-sh/uv/pull/4313)) - Add `--no-build`, `--no-build-package`, and binary variants ([#4322](https://github.com/astral-sh/uv/pull/4322)) - Add `EXTERNALLY-MANAGED` markers to managed toolchains ([#4312](https://github.com/astral-sh/uv/pull/4312)) - Add `uv toolchain find` ([#4206](https://github.com/astral-sh/uv/pull/4206)) - Add persistent configuration for non-`pip` APIs ([#4294](https://github.com/astral-sh/uv/pull/4294)) - Add support for adding/removing development dependencies ([#4327](https://github.com/astral-sh/uv/pull/4327)) - Add support for listing system toolchains ([#4172](https://github.com/astral-sh/uv/pull/4172)) - Add support for toolchain requests by key ([#4332](https://github.com/astral-sh/uv/pull/4332)) - Allow multiple toolchains to be requested in `uv toolchain install` ([#4334](https://github.com/astral-sh/uv/pull/4334)) - Fix relative and absolute path handling in lockfiles ([#4266](https://github.com/astral-sh/uv/pull/4266)) - Load configuration options from workspace root ([#4295](https://github.com/astral-sh/uv/pull/4295)) - Omit project name from workspace errors ([#4299](https://github.com/astral-sh/uv/pull/4299)) - Read Python version files during toolchain installs ([#4335](https://github.com/astral-sh/uv/pull/4335)) - Remove extraneous installations in `uv sync` by default ([#4366](https://github.com/astral-sh/uv/pull/4366)) - Respect `requires-python` in `uv lock` ([#4282](https://github.com/astral-sh/uv/pull/4282)) - Respect workspace-wide `requires-python` in interpreter selection ([#4298](https://github.com/astral-sh/uv/pull/4298)) - Support unnamed requirements in `uv add` ([#4326](https://github.com/astral-sh/uv/pull/4326)) - Use portable slash paths in lockfile ([#4324](https://github.com/astral-sh/uv/pull/4324)) - Use registry URL for fetching source distributions from lockfile ([#4280](https://github.com/astral-sh/uv/pull/4280)) - `uv sync --no-clean` ([#4367](https://github.com/astral-sh/uv/pull/4367)) - Filter dependencies by tracking markers on resolver forks ([#4339](https://github.com/astral-sh/uv/pull/4339)) - Use `Requires-Python` to filter dependencies during universal resolution ([#4273](https://github.com/astral-sh/uv/pull/4273)) ## 0.2.13 ### Enhancements - Add resolver tracing logs for when we filter requirements ([#4381](https://github.com/astral-sh/uv/pull/4381)) ### Preview features - Add `--workspace` option to `uv add` ([#4362](https://github.com/astral-sh/uv/pull/4362)) - Ignore query errors during `uv toolchain list` ([#4382](https://github.com/astral-sh/uv/pull/4382)) - Respect `.python-version` files and fetch managed toolchains in uv project commands ([#4361](https://github.com/astral-sh/uv/pull/4361)) - Respect `.python-version` in `uv venv --preview` ([#4360](https://github.com/astral-sh/uv/pull/4360)) ## 0.2.14 ### Enhancements - Support toolchain requests with platform-tag style Python implementations and version ([#4407](https://github.com/astral-sh/uv/pull/4407)) ### CLI - Use "Prepared" instead of "Downloaded" in logs ([#4394](https://github.com/astral-sh/uv/pull/4394)) ### Bug fixes - Treat mismatched directory and file urls as unsatisfied requirements ([#4393](https://github.com/astral-sh/uv/pull/4393)) ### Preview features - Expose `toolchain-preference` as a CLI and configuration file option ([#4424](https://github.com/astral-sh/uv/pull/4424)) - Improve handling of command arguments in `uv run` and `uv tool run` ([#4404](https://github.com/astral-sh/uv/pull/4404)) - Add `tool.uv.sources` support for `uv add` ([#4406](https://github.com/astral-sh/uv/pull/4406)) - Use correct lock path for workspace dependencies ([#4421](https://github.com/astral-sh/uv/pull/4421)) - Filter out sibling dependencies in resolver forks ([#4415](https://github.com/astral-sh/uv/pull/4415)) ## 0.2.15 ### Enhancements - Add `--emit-build-options` flag to `uv pip compile` interface ([#4463](https://github.com/astral-sh/uv/pull/4463)) - Add `pythonw` support for gui scripts on Windows ([#4409](https://github.com/astral-sh/uv/pull/4409)) - Add `uv pip tree` ([#3859](https://github.com/astral-sh/uv/pull/3859)) ### CLI - Adjust the docs for the pip CLI commands ([#4445](https://github.com/astral-sh/uv/pull/4445)) - Fix casing of `--no-compile` alias ([#4453](https://github.com/astral-sh/uv/pull/4453)) ### Bug fixes - Fix ordering of prefer-system toolchain preference ([#4441](https://github.com/astral-sh/uv/pull/4441)) - Respect index strategy in source distribution builds ([#4468](https://github.com/astral-sh/uv/pull/4468)) ### Documentation - Add documentation for using uv in a Docker image ([#4433](https://github.com/astral-sh/uv/pull/4433)) ## 0.2.16 ### Enhancements - Add a universal resolution mode to `uv pip compile` with `--universal` ([#4505](https://github.com/astral-sh/uv/pull/4505)) - Add support for `--no-strip-markers` in `uv pip compile` output ([#4503](https://github.com/astral-sh/uv/pull/4503)) - Add `--no-dedupe` support to `uv pip tree` ([#4449](https://github.com/astral-sh/uv/pull/4449)) ### Bug fixes - Enable more precise environment locking with `--prefix` ([#4506](https://github.com/astral-sh/uv/pull/4506)) - Allow local index references in `requirements.txt` files ([#4525](https://github.com/astral-sh/uv/pull/4525)) - Allow non-`file://` paths to serve as `--index-url` values ([#4524](https://github.com/astral-sh/uv/pull/4524)) - Make `.egg-info` filename parsing spec compliant ([#4533](https://github.com/astral-sh/uv/pull/4533)) - Gracefully handle non-existent packages in local indexes ([#4545](https://github.com/astral-sh/uv/pull/4545)) - Read content length from response rather than request ([#4488](https://github.com/astral-sh/uv/pull/4488)) - Read persistent configuration from non-workspace `pyproject.toml` ([#4526](https://github.com/astral-sh/uv/pull/4526)) - Avoid panic for invalid, non-base index URLs ([#4527](https://github.com/astral-sh/uv/pull/4527)) ### Performance - Skip submodule update for fresh clones ([#4482](https://github.com/astral-sh/uv/pull/4482)) - Use shared client in Git fetch implementation ([#4487](https://github.com/astral-sh/uv/pull/4487)) ### Preview features - Add `--package` argument to `uv add` and `uv remove` ([#4556](https://github.com/astral-sh/uv/pull/4556)) - Add `uv tool install` ([#4492](https://github.com/astral-sh/uv/pull/4492)) - Fallback to interpreter discovery in `uv run` ([#4549](https://github.com/astral-sh/uv/pull/4549)) - Make `uv.sources` without `--preview` non-fatal ([#4558](https://github.com/astral-sh/uv/pull/4558)) - Remove non-existent extras from lockfile ([#4479](https://github.com/astral-sh/uv/pull/4479)) - Support conflicting URL in separate forks ([#4435](https://github.com/astral-sh/uv/pull/4435)) - Automatically detect workspace packages in `uv add` ([#4557](https://github.com/astral-sh/uv/pull/4557)) - Omit `distribution.sdist` from lockfile when it is redundant ([#4528](https://github.com/astral-sh/uv/pull/4528)) - Remove `source` and `version` from lock file when unambiguous ([#4513](https://github.com/astral-sh/uv/pull/4513)) - Allow `uv lock` to read overrides from `tool.uv` (#4108) ([#4369](https://github.com/astral-sh/uv/pull/4369)) ## 0.2.17 ### Bug fixes - Avoid enforcing extra-only constraints ([#4570](https://github.com/astral-sh/uv/pull/4570)) ### Preview features - Add `--extra` to `uv add` and enable fine-grained updates ([#4566](https://github.com/astral-sh/uv/pull/4566)) ## 0.2.18 ### CLI - Make `--universal` and `--python-platform` mutually exclusive ([#4598](https://github.com/astral-sh/uv/pull/4598)) - Add `--depth` and `--prune` support to `pip tree` ([#4440](https://github.com/astral-sh/uv/pull/4440)) ### Bug fixes - Handle cycles when propagating markers ([#4595](https://github.com/astral-sh/uv/pull/4595)) - Ignore `py` not found errors during interpreter discovery ([#4620](https://github.com/astral-sh/uv/pull/4620)) - Merge markers when applying constraints ([#4648](https://github.com/astral-sh/uv/pull/4648)) - Retry on spurious failures when caching built wheels ([#4605](https://github.com/astral-sh/uv/pull/4605)) - Sort indexes during graph edge removal ([#4649](https://github.com/astral-sh/uv/pull/4649)) - Treat Python version as a lower bound in `--universal` ([#4597](https://github.com/astral-sh/uv/pull/4597)) - Fix the incorrect handling of markers in `pip tree` ([#4611](https://github.com/astral-sh/uv/pull/4611)) - Improve toolchain and environment missing error messages ([#4596](https://github.com/astral-sh/uv/pull/4596)) ### Documentation - Explicitly mention use of seed packages during `uv venv --seed` ([#4588](https://github.com/astral-sh/uv/pull/4588)) ### Preview features - Add `uv tool list` ([#4630](https://github.com/astral-sh/uv/pull/4630)) - Add `uv tool uninstall` ([#4641](https://github.com/astral-sh/uv/pull/4641)) - Add support for specifying `name@version` in `uv tool run` ([#4572](https://github.com/astral-sh/uv/pull/4572)) - Allow `uv add` to specify optional dependency groups ([#4607](https://github.com/astral-sh/uv/pull/4607)) - Allow the package spec to be passed positionally in `uv tool install` ([#4564](https://github.com/astral-sh/uv/pull/4564)) - Avoid infinite loop for cyclic installs ([#4633](https://github.com/astral-sh/uv/pull/4633)) - Indent wheels like dependencies in the lockfile ([#4582](https://github.com/astral-sh/uv/pull/4582)) - Sync all packages in a virtual workspace ([#4636](https://github.com/astral-sh/uv/pull/4636)) - Use inline table for dependencies in lockfile ([#4581](https://github.com/astral-sh/uv/pull/4581)) - Make `source` field in lock file more structured ([#4627](https://github.com/astral-sh/uv/pull/4627)) ## 0.2.19 ### Enhancements - Indicate when we retried requests during network errors ([#4725](https://github.com/astral-sh/uv/pull/4725)) ### CLI - Add `--disable-pip-version-check` to compatibility arguments ([#4672](https://github.com/astral-sh/uv/pull/4672)) - Allow `uv pip sync` to clear an environment with opt-in ([#4517](https://github.com/astral-sh/uv/pull/4517)) - Add `--invert` to `uv pip tree` ([#4621](https://github.com/astral-sh/uv/pull/4621)) - Omit `(*)` in `uv pip tree` for empty packages ([#4673](https://github.com/astral-sh/uv/pull/4673)) - Add `--package` to `uv pip tree` ([#4655](https://github.com/astral-sh/uv/pull/4655)) ### Bug fixes - Fix bug where git cache did not validate commits correctly ([#4698](https://github.com/astral-sh/uv/pull/4698)) - Narrow `requires-python` requirement in resolver forks ([#4707](https://github.com/astral-sh/uv/pull/4707)) - Fix bug when pruning the last package in `uv pip tree` ([#4652](https://github.com/astral-sh/uv/pull/4652)) ### Preview features - Remove dangling environments in `uv tool uninstall` ([#4740](https://github.com/astral-sh/uv/pull/4740)) - Respect upgrades in `uv tool install` ([#4736](https://github.com/astral-sh/uv/pull/4736)) - Add PEP 723 support to `uv run` ([#4656](https://github.com/astral-sh/uv/pull/4656)) - Add `tool dir` and `toolchain dir` commands ([#4695](https://github.com/astral-sh/uv/pull/4695)) - Omit `pythonX.Y` segment in stdlib path for managed toolchains on Windows ([#4727](https://github.com/astral-sh/uv/pull/4727)) - Add `uv toolchain uninstall` ([#4646](https://github.com/astral-sh/uv/pull/4646)) - Add `uvx` alias for `uv tool run` ([#4632](https://github.com/astral-sh/uv/pull/4632)) - Allow configuring the toolchain fetch strategy ([#4601](https://github.com/astral-sh/uv/pull/4601)) - Drop `prefer` prefix from `toolchain-preference` values ([#4602](https://github.com/astral-sh/uv/pull/4602)) - Enable projects to opt-out of workspace management ([#4565](https://github.com/astral-sh/uv/pull/4565)) - Fetch managed toolchains if necessary in `uv tool install` and `uv tool run` ([#4717](https://github.com/astral-sh/uv/pull/4717)) - Fix tool dist-info directory normalization ([#4686](https://github.com/astral-sh/uv/pull/4686)) - Lock the toolchains directory during toolchain operations ([#4733](https://github.com/astral-sh/uv/pull/4733)) - Log when we start solving a fork ([#4684](https://github.com/astral-sh/uv/pull/4684)) - Reinstall entrypoints with `--force` ([#4697](https://github.com/astral-sh/uv/pull/4697)) - Respect data scripts in `uv tool install` ([#4693](https://github.com/astral-sh/uv/pull/4693)) - Set fork solution as preference when resolving ([#4662](https://github.com/astral-sh/uv/pull/4662)) - Show dedicated message for tools with no entrypoints ([#4694](https://github.com/astral-sh/uv/pull/4694)) - Support unnamed requirements in `uv tool install` ([#4716](https://github.com/astral-sh/uv/pull/4716)) ## 0.2.20 - Fix issue where the standalone installer failed due to a missing `uvx` binary ([#4743](https://github.com/astral-sh/uv/pull/4743)) ## 0.2.21 - Fix issue where standalone installer failed to due missing `uvx.exe` binary on Windows ([#4756](https://github.com/astral-sh/uv/pull/4756)) ### CLI - Differentiate `freeze` and `list` help text ([#4751](https://github.com/astral-sh/uv/pull/4751)) ### Preview features - Replace tool environments on updated Python request ([#4746](https://github.com/astral-sh/uv/pull/4746)) ## 0.2.22 ### CLI - Add `--exclude-newer` to installer arguments ([#4785](https://github.com/astral-sh/uv/pull/4785)) - Bold durations in CLI messages ([#4818](https://github.com/astral-sh/uv/pull/4818)) - Drop crate description from the `uv` help menu ([#4773](https://github.com/astral-sh/uv/pull/4773)) - Update "about" in help menu ([#4782](https://github.com/astral-sh/uv/pull/4782)) ### Configuration - Add `UV_OVERRIDE` environment variable for `--override` ([#4836](https://github.com/astral-sh/uv/pull/4836)) ### Bug fixes - Always use release-only comparisons for `requires-python` ([#4794](https://github.com/astral-sh/uv/pull/4794)) - Avoid hangs before exiting CLI ([#4793](https://github.com/astral-sh/uv/pull/4793)) - Preserve verbatim URLs for `--find-links` ([#4838](https://github.com/astral-sh/uv/pull/4838)) ### Preview features - Always use base interpreter for cached environments ([#4805](https://github.com/astral-sh/uv/pull/4805)) - Cache tool environments in `uv tool run` ([#4784](https://github.com/astral-sh/uv/pull/4784)) - Check hash of downloaded python toolchain ([#4806](https://github.com/astral-sh/uv/pull/4806)) - Remove incompatible wheels from `uv.lock` ([#4799](https://github.com/astral-sh/uv/pull/4799)) - `uv cache prune` removes all cached environments ([#4845](https://github.com/astral-sh/uv/pull/4845)) - Add dedicated help menu for `uvx` ([#4770](https://github.com/astral-sh/uv/pull/4770)) - Change "toolchain" to "python" ([#4735](https://github.com/astral-sh/uv/pull/4735)) - Create empty environment for `uv run --isolated` ([#4849](https://github.com/astral-sh/uv/pull/4849)) - Deduplicate when install or uninstall python ([#4841](https://github.com/astral-sh/uv/pull/4841)) - Require at least one target for toolchain uninstalls ([#4820](https://github.com/astral-sh/uv/pull/4820)) - Resolve requirements prior to nuking tool environments ([#4788](https://github.com/astral-sh/uv/pull/4788)) - Tweak installation language in toolchain install ([#4811](https://github.com/astral-sh/uv/pull/4811)) - Use already-installed tools in `uv tool run` ([#4750](https://github.com/astral-sh/uv/pull/4750)) - Use cached environments in PEP 723 execution ([#4789](https://github.com/astral-sh/uv/pull/4789)) - Use optimized versions of managed Python on Linux ([#4775](https://github.com/astral-sh/uv/pull/4775)) - Fill Python requests with platform information during automatic fetches ([#4810](https://github.com/astral-sh/uv/pull/4810)) - Remove installed python for force installation ([#4807](https://github.com/astral-sh/uv/pull/4807)) - Add tool version to list command ([#4674](https://github.com/astral-sh/uv/pull/4674)) - Add entrypoints to tool list ([#4661](https://github.com/astral-sh/uv/pull/4661)) ## 0.2.23 ### Enhancements - Update Windows trampoline binaries ([#4864](https://github.com/astral-sh/uv/pull/4864)) - Show user-facing warning when falling back to copy installs ([#4880](https://github.com/astral-sh/uv/pull/4880)) ### Bug fixes - Initialize all `--prefix` subdirectories ([#4895](https://github.com/astral-sh/uv/pull/4895)) - Respect `requires-python` when prefetching ([#4900](https://github.com/astral-sh/uv/pull/4900)) - Partially revert `Requires-Python` version narrowing ([#4902](https://github.com/astral-sh/uv/pull/4902)) ### Preview features - Avoid creating cache directories in tool directory ([#4868](https://github.com/astral-sh/uv/pull/4868)) - Add progress bar when downloading python ([#4840](https://github.com/astral-sh/uv/pull/4840)) - Add some decoration to tool CLI ([#4865](https://github.com/astral-sh/uv/pull/4865)) - Add some text decoration to toolchain CLI ([#4882](https://github.com/astral-sh/uv/pull/4882)) - Add user-facing output to indicate PEP 723 script ([#4881](https://github.com/astral-sh/uv/pull/4881)) - Ensure Pythons are aligned in `uv python list` ([#4884](https://github.com/astral-sh/uv/pull/4884)) - Fix always-plural message in uv python install ([#4866](https://github.com/astral-sh/uv/pull/4866)) - Skip installing `--with` requirements if present in base environment ([#4879](https://github.com/astral-sh/uv/pull/4879)) - Sort dependencies before wheels and source distributions ([#4897](https://github.com/astral-sh/uv/pull/4897)) - Improve logging during resolver forking ([#4894](https://github.com/astral-sh/uv/pull/4894)) ## 0.2.24 ### Enhancements - Add support for 'any' Python requests ([#4948](https://github.com/astral-sh/uv/pull/4948)) - Allow constraints to be provided in `--upgrade-package` ([#4952](https://github.com/astral-sh/uv/pull/4952)) - Add `manylinux_2_31` to supported `--python-platform` ([#4965](https://github.com/astral-sh/uv/pull/4965)) - Improve marker simplification ([#4639](https://github.com/astral-sh/uv/pull/4639)) ### CLI - Display short help menu when `--help` is used ([#4772](https://github.com/astral-sh/uv/pull/4772)) - Allow `uv help` global options during `uv help` ([#4906](https://github.com/astral-sh/uv/pull/4906)) - Use paging for `uv help` display when available ([#4909](https://github.com/astral-sh/uv/pull/4909)) ### Performance - Switch to single threaded async runtime ([#4934](https://github.com/astral-sh/uv/pull/4934)) ### Bug fixes - Avoid AND-ing multi-term specifiers in marker normalization ([#4911](https://github.com/astral-sh/uv/pull/4911)) - Avoid inferring package name for GitHub Archives ([#4928](https://github.com/astral-sh/uv/pull/4928)) - Retry on connection reset network errors ([#4960](https://github.com/astral-sh/uv/pull/4960)) - Apply extra to overrides and constraints ([#4829](https://github.com/astral-sh/uv/pull/4829)) ### Rust API - Allow `uv` crate to be used as a library ([#4642](https://github.com/astral-sh/uv/pull/4642)) ### Preview features - Add Python installation guide ([#4942](https://github.com/astral-sh/uv/pull/4942)) - Add `uv python pin` ([#4950](https://github.com/astral-sh/uv/pull/4950)) - Add command-separation for Python discovery display ([#4916](https://github.com/astral-sh/uv/pull/4916)) - Avoid debug error for `uv run` with unknown Python version ([#4913](https://github.com/astral-sh/uv/pull/4913)) - Enable `--all` to uninstall all managed Pythons ([#4932](https://github.com/astral-sh/uv/pull/4932)) - Enable `--all` to uninstall all managed tools ([#4937](https://github.com/astral-sh/uv/pull/4937)) - Filter out markers based on Python requirement ([#4912](https://github.com/astral-sh/uv/pull/4912)) - Implement `uv tree` ([#4708](https://github.com/astral-sh/uv/pull/4708)) - Improve 'any' search message during `uv python install` ([#4940](https://github.com/astral-sh/uv/pull/4940)) - Lock for the duration of tool commands ([#4720](https://github.com/astral-sh/uv/pull/4720)) - Perform lock in `uv sync` by default ([#4839](https://github.com/astral-sh/uv/pull/4839)) - Reinstall and recreate environments when interpreter is removed ([#4935](https://github.com/astral-sh/uv/pull/4935)) - Respect `--isolated` in `uv python install` ([#4938](https://github.com/astral-sh/uv/pull/4938)) - Respect resolver settings in `uv remove` ([#4930](https://github.com/astral-sh/uv/pull/4930)) - Update "Python versions" documentation ([#4943](https://github.com/astral-sh/uv/pull/4943)) - Warn if tool binary directory is not on path ([#4951](https://github.com/astral-sh/uv/pull/4951)) - Avoid reparsing wheel URLs ([#4947](https://github.com/astral-sh/uv/pull/4947)) - Avoid serializing if lockfile does not change ([#4945](https://github.com/astral-sh/uv/pull/4945)) ## 0.2.25 ### Enhancements - Include PyPy-specific executables when creating virtual environments with `uv venv` ([#5047](https://github.com/astral-sh/uv/pull/5047)) - Add a custom error message for `--no-build-isolation` `torch` dependencies ([#5041](https://github.com/astral-sh/uv/pull/5041)) - Improve missing `wheel` error message with `--no-build-isolation` ([#4964](https://github.com/astral-sh/uv/pull/4964)) ### CLI - Add `--no-pager` option in `help` command ([#5007](https://github.com/astral-sh/uv/pull/5007)) - Unhide `--isolated` global argument ([#5005](https://github.com/astral-sh/uv/pull/5005)) - Warn when unused `pyproject.toml` configuration is detected ([#5025](https://github.com/astral-sh/uv/pull/5025)) ### Bug fixes - Fall back to streaming wheel when `Content-Length` header is absent ([#5000](https://github.com/astral-sh/uv/pull/5000)) - Fix substring marker expression disjointness checks ([#4998](https://github.com/astral-sh/uv/pull/4998)) - Lock directories to synchronize wheel-install copies ([#4978](https://github.com/astral-sh/uv/pull/4978)) - Normalize out complementary == or != markers ([#5050](https://github.com/astral-sh/uv/pull/5050)) - Retry on permission errors when persisting extracted source distributions to the cache ([#5076](https://github.com/astral-sh/uv/pull/5076)) - Set absolute URLs prior to uploading to PyPI ([#5038](https://github.com/astral-sh/uv/pull/5038)) - Exclude `--upgrade-package` from the `pip compile` header ([#5032](https://github.com/astral-sh/uv/pull/5032)) - Exclude `--upgrade-package` when option and value are passed as a single argument ([#5033](https://github.com/astral-sh/uv/pull/5033)) - Add split to cover marker universe when existing splits are incomplete ([#5074](https://github.com/astral-sh/uv/pull/5074)) - Use correct `pyproject.toml` path in warnings ([#5069](https://github.com/astral-sh/uv/pull/5069)) ### Documentation - Fix `CONTRIBUTING.md` instructions to install multiple Python versions ([#5015](https://github.com/astral-sh/uv/pull/5015)) - Use versioned badges when uploading to PyPI ([#5039](https://github.com/astral-sh/uv/pull/5039)) ### Preview features - Add documentation for running scripts ([#4968](https://github.com/astral-sh/uv/pull/4968)) - Add guide for tools ([#4982](https://github.com/astral-sh/uv/pull/4982)) - Allow URL dependencies in tool run `--from` ([#5002](https://github.com/astral-sh/uv/pull/5002)) - Add guide for authenticating to Azure Artifacts ([#4857](https://github.com/astral-sh/uv/pull/4857)) - Improve rc file detection based on rustup ([#5026](https://github.com/astral-sh/uv/pull/5026)) - Rename `python install --force` parameter to `--reinstall` ([#4999](https://github.com/astral-sh/uv/pull/4999)) - Use lockfile to prefill resolver index ([#4495](https://github.com/astral-sh/uv/pull/4495)) - `uv tool install` hint the correct when the executable is available ([#5019](https://github.com/astral-sh/uv/pull/5019)) - `uv tool run` error messages references `uvx` when appropriate ([#5014](https://github.com/astral-sh/uv/pull/5014)) - `uvx` warns when requested executable is not provided by the package [#5071](https://github.com/astral-sh/uv/pull/5071)) - Exit with zero when `uv tool install` request is already satisfied ([#4986](https://github.com/astral-sh/uv/pull/4986)) - Respect the libc of the execution environment with `uv python list` ([#5036](https://github.com/astral-sh/uv/pull/5036)) - Update standalone Pythons to include 3.12.4 ([#5042](https://github.com/astral-sh/uv/pull/5042)) - `uv tool run` suggest valid commands when command is not found ([#4997](https://github.com/astral-sh/uv/pull/4997)) - Add Windows path updates for `uv tool` ([#5029](https://github.com/astral-sh/uv/pull/5029)) - Add a command to append uv's binary directory to PATH ([#4975](https://github.com/astral-sh/uv/pull/4975)) ## 0.2.26 ### CLI - Add `--no-progress` global option to hide all progress animations ([#5098](https://github.com/astral-sh/uv/pull/5098)) ### Performance - Cache downloaded wheel when range requests aren't supported ([#5089](https://github.com/astral-sh/uv/pull/5089)) ### Bug fixes - Download wheel to disk when streaming unzip failed with HTTP streaming error ([#5094](https://github.com/astral-sh/uv/pull/5094)) - Filter out invalid wheels based on `requires-python` ([#5084](https://github.com/astral-sh/uv/pull/5084)) - Filter out none ABI wheels with mismatched Python versions ([#5087](https://github.com/astral-sh/uv/pull/5087)) - Lock Git cache on resolve ([#5051](https://github.com/astral-sh/uv/pull/5051)) - Change order of `pip compile` command checks to handle exact argument first ([#5111](https://github.com/astral-sh/uv/pull/5111)) ### Documentation - Document that `--universal` implies `--no-strip-markers` ([#5121](https://github.com/astral-sh/uv/pull/5121)) ### Preview features - Indicate that `uv lock --upgrade` has updated the lock file ([#5110](https://github.com/astral-sh/uv/pull/5110)) - Sort managed Python installations by version ([#5140](https://github.com/astral-sh/uv/pull/5140)) - Support workspace to workspace path dependencies ([#4833](https://github.com/astral-sh/uv/pull/4833)) - Allow conflicting locals when forking ([#5104](https://github.com/astral-sh/uv/pull/5104)) - Rework `pyproject.toml` reformatting to respect original indentation ([#5075](https://github.com/astral-sh/uv/pull/5075)) ## 0.2.27 ### Enhancements - Add GraalPy support ([#5141](https://github.com/astral-sh/uv/pull/5141)) - Add a `--verify-hashes` hash-checking mode ([#4007](https://github.com/astral-sh/uv/pull/4007)) - Discover all `python3.x` executables in the `PATH` ([#5148](https://github.com/astral-sh/uv/pull/5148)) - Support `--link-mode=symlink` ([#5208](https://github.com/astral-sh/uv/pull/5208)) - Warn about unconstrained direct deps in lowest resolution ([#5142](https://github.com/astral-sh/uv/pull/5142)) - Log origin of version selection ([#5186](https://github.com/astral-sh/uv/pull/5186)) - Key hash policy on version, rather than package ([#5169](https://github.com/astral-sh/uv/pull/5169)) ### CLI - Make missing project table a tracing warning ([#5194](https://github.com/astral-sh/uv/pull/5194)) - Remove trailing period from user-facing messages ([#5218](https://github.com/astral-sh/uv/pull/5218)) ### Bug fixes - Make entrypoint writes atomic to avoid overwriting symlinks ([#5165](https://github.com/astral-sh/uv/pull/5165)) - Use `which`-retrieved path directly when spawning pager ([#5198](https://github.com/astral-sh/uv/pull/5198)) - Don't apply irrelevant constraints when validating site-packages ([#5231](https://github.com/astral-sh/uv/pull/5231)) - Respect local versions for all user requirements ([#5232](https://github.com/astral-sh/uv/pull/5232)) ### Preview features - Add `--frozen` to `uv add`, `uv remove`, and `uv tree` ([#5214](https://github.com/astral-sh/uv/pull/5214)) - Add `--locked` and `--frozen` to `uv run` CLI ([#5196](https://github.com/astral-sh/uv/pull/5196)) - Add `uv tool dir --bin` to show executable directory ([#5160](https://github.com/astral-sh/uv/pull/5160)) - Add `uv tool list --show-paths` to show install paths ([#5164](https://github.com/astral-sh/uv/pull/5164)) - Add color to `python pin` CLI ([#5215](https://github.com/astral-sh/uv/pull/5215)) - Added a way to inspect installation scripts on Powershell( Windows) ([#5157](https://github.com/astral-sh/uv/pull/5157)) - Avoid TOCTOU errors in `.python-version` reads ([#5223](https://github.com/astral-sh/uv/pull/5223)) - Only show the Python installed on the system if `--python-preference only-system` is specified ([#5219](https://github.com/astral-sh/uv/pull/5219)) - Check `python pin` compatibility with `Requires-Python` ([#4989](https://github.com/astral-sh/uv/pull/4989)) - Enforce hashes in lockfile install ([#5170](https://github.com/astral-sh/uv/pull/5170)) - Fix reference to `uv run` in `uv tree` CLI ([#5216](https://github.com/astral-sh/uv/pull/5216)) - Handle universal vs. fork markers with `ResolverMarkers` ([#5099](https://github.com/astral-sh/uv/pull/5099)) - Implement `uv init` ([#4791](https://github.com/astral-sh/uv/pull/4791)) - Make Python install robust to individual failures ([#5199](https://github.com/astral-sh/uv/pull/5199)) - Make registry hashes optional in the lockfile ([#5166](https://github.com/astral-sh/uv/pull/5166)) - Merge extras in lockfile ([#5181](https://github.com/astral-sh/uv/pull/5181)) - Move integration guide docs and edit Azure integration guide ([#5117](https://github.com/astral-sh/uv/pull/5117)) - Process completed Python installs and uninstalls as a stream ([#5203](https://github.com/astral-sh/uv/pull/5203)) - Skip invalid tools in `uv tool list` ([#5156](https://github.com/astral-sh/uv/pull/5156)) - Touch-ups to tools guide ([#5202](https://github.com/astral-sh/uv/pull/5202)) - Use +- install output for Python versions ([#5201](https://github.com/astral-sh/uv/pull/5201)) - Use display representation for download error ([#5173](https://github.com/astral-sh/uv/pull/5173)) - Use specialized error message for invalid Python install / uninstall requests ([#5171](https://github.com/astral-sh/uv/pull/5171)) - Use the strongest hash in the lockfile ([#5167](https://github.com/astral-sh/uv/pull/5167)) - Write project guide ([#5195](https://github.com/astral-sh/uv/pull/5195)) - Write tools concept document ([#5207](https://github.com/astral-sh/uv/pull/5207)) - Fix reference to `projects.md` ([#5154](https://github.com/astral-sh/uv/pull/5154)) - Fixes to the settings documentation ([#5177](https://github.com/astral-sh/uv/pull/5177)) - Set exact version specifiers when resolving from lockfile ([#5193](https://github.com/astral-sh/uv/pull/5193)) ## 0.2.28 ### Enhancements - Output stable ordering to `requirements.txt` in universal mode ([#5334](https://github.com/astral-sh/uv/pull/5334)) - Allow symlinks with `--find-links` ([#5323](https://github.com/astral-sh/uv/pull/5323)) - Add support for variations of `pythonw.exe` ([#5259](https://github.com/astral-sh/uv/pull/5259)) ### CLI - Stylize `Requires-Python` consistently in CLI output ([#5304](https://github.com/astral-sh/uv/pull/5304)) - Add `--show-version-specifiers` to `tree` ([#5240](https://github.com/astral-sh/uv/pull/5240)) ### Performance - Avoid always rebuilding dynamic metadata ([#5206](https://github.com/astral-sh/uv/pull/5206)) - Avoid URL parsing when deserializing wheels ([#5235](https://github.com/astral-sh/uv/pull/5235)) ### Bug fixes - Avoid cache prune failure due to removed interpreter ([#5286](https://github.com/astral-sh/uv/pull/5286)) - Avoid including empty extras in resolution ([#5306](https://github.com/astral-sh/uv/pull/5306)) - If multiple indices contain the same version, use the first index ([#5288](https://github.com/astral-sh/uv/pull/5288)) - Include URLs on graph edges ([#5312](https://github.com/astral-sh/uv/pull/5312)) - Match wheel tags against `Requires-Python` major-minor ([#5289](https://github.com/astral-sh/uv/pull/5289)) - Remove Simple API cache files for alternative indexes in `cache clean` ([#5353](https://github.com/astral-sh/uv/pull/5353)) - Remove extraneous `are` from wheel tag error messages ([#5303](https://github.com/astral-sh/uv/pull/5303)) - Allow conflicting pre-release strategies when forking ([#5150](https://github.com/astral-sh/uv/pull/5150)) - Use tag error rather than requires-python error for ABI filtering ([#5296](https://github.com/astral-sh/uv/pull/5296)) ### Preview features - Add `requires-python` to `uv init` ([#5322](https://github.com/astral-sh/uv/pull/5322)) - Add `uv add --no-editable` ([#5246](https://github.com/astral-sh/uv/pull/5246)) - Add constraint dependencies to pyproject.toml ([#5248](https://github.com/astral-sh/uv/pull/5248)) - Add support for requirements files in `uv run` ([#4973](https://github.com/astral-sh/uv/pull/4973)) - Avoid redundant members update in `uv init` ([#5321](https://github.com/astral-sh/uv/pull/5321)) - Create member `pyproject.toml` prior to workspace discovery ([#5317](https://github.com/astral-sh/uv/pull/5317)) - Fix `uv init .` ([#5330](https://github.com/astral-sh/uv/pull/5330)) - Fix `uv init` creation of a sub-package by path ([#5247](https://github.com/astral-sh/uv/pull/5247)) - Fix colors in `uv tool run` suggestion ([#5267](https://github.com/astral-sh/uv/pull/5267)) - Improve consistency of `tool` CLI ([#5326](https://github.com/astral-sh/uv/pull/5326)) - Make tool install robust to malformed receipts ([#5305](https://github.com/astral-sh/uv/pull/5305)) - Reduce spacing between nav items ([#5310](https://github.com/astral-sh/uv/pull/5310)) - Respect exclusions in `uv init` ([#5318](https://github.com/astral-sh/uv/pull/5318)) - Store resolution options in lockfile ([#5264](https://github.com/astral-sh/uv/pull/5264)) - Use backticks in project init message ([#5302](https://github.com/astral-sh/uv/pull/5302)) - Ignores workspace when `--isolated` flag is used in `uv init` ([#5290](https://github.com/astral-sh/uv/pull/5290)) - Normalize directory names in `uv init` ([#5292](https://github.com/astral-sh/uv/pull/5292)) - Avoid project discovery in `uv python pin` if `--isolated` is provided ([#5354](https://github.com/astral-sh/uv/pull/5354)) - Show symbolic links in `uv python list` ([#5343](https://github.com/astral-sh/uv/pull/5343)) - Discover workspace from target path in `uv init` ([#5250](https://github.com/astral-sh/uv/pull/5250)) - Do not create nested workspace in `uv init` ([#5293](https://github.com/astral-sh/uv/pull/5293)) ## 0.2.29 ### Enhancements - Add `--ci` mode to `uv cache prune` ([#5391](https://github.com/astral-sh/uv/pull/5391)) - Display Python installation key for discovered interpreters ([#5365](https://github.com/astral-sh/uv/pull/5365)) ### Bug fixes - Allow symlinks to files in scripts directory ([#5380](https://github.com/astral-sh/uv/pull/5380)) - Always accept already-installed pre-releases ([#5419](https://github.com/astral-sh/uv/pull/5419)) - Validate successful metadata fetch for direct dependencies ([#5392](https://github.com/astral-sh/uv/pull/5392)) ### Documentation - Add warning to `--link-mode=symlink` documentation ([#5387](https://github.com/astral-sh/uv/pull/5387)) ### Preview features - Add PyPy finder ([#5337](https://github.com/astral-sh/uv/pull/5337)) - Add `uv init --virtual` ([#5396](https://github.com/astral-sh/uv/pull/5396)) - Allow `uv init` in unmanaged projects ([#5372](https://github.com/astral-sh/uv/pull/5372)) - Allow comments in `.python-version[s]` ([#5350](https://github.com/astral-sh/uv/pull/5350)) - Always show lock updates in `uv lock` ([#5413](https://github.com/astral-sh/uv/pull/5413)) - Improvements to the docs content ([#5426](https://github.com/astral-sh/uv/pull/5426)) - Fix blurring from nav title box shadow ([#5374](https://github.com/astral-sh/uv/pull/5374)) - Ignore Ctrl-C signals in `uv run` and `uv tool run` ([#5395](https://github.com/astral-sh/uv/pull/5395)) - Ignore hidden directories in workspace discovery ([#5408](https://github.com/astral-sh/uv/pull/5408)) - Increase padding between each nav section ([#5373](https://github.com/astral-sh/uv/pull/5373)) - Mark `--raw-sources` as conflicting with sources-specific arguments ([#5378](https://github.com/astral-sh/uv/pull/5378)) - Omit empty uv.tool.dev-dependencies on `uv init` ([#5406](https://github.com/astral-sh/uv/pull/5406)) - Omit interpreter path during `uv venv` with managed Python ([#5311](https://github.com/astral-sh/uv/pull/5311)) - Omit interpreter path from output when using managed Python ([#5313](https://github.com/astral-sh/uv/pull/5313)) - Reject Git CLI arguments with non-Git sources ([#5377](https://github.com/astral-sh/uv/pull/5377)) - Retain dependency specifier in `uv add` with sources ([#5370](https://github.com/astral-sh/uv/pull/5370)) - Show additions and removals in `uv lock` updates ([#5410](https://github.com/astral-sh/uv/pull/5410)) - Skip 'Nothing to uninstall' message when removing dangling environments ([#5382](https://github.com/astral-sh/uv/pull/5382)) - Support `requirements.txt` files in `uv tool install` and `uv tool run` ([#5362](https://github.com/astral-sh/uv/pull/5362)) - Use env variables in Github Actions docs ([#5411](https://github.com/astral-sh/uv/pull/5411)) - Use logo in documentation ([#5421](https://github.com/astral-sh/uv/pull/5421)) - Warn on `requirements.txt`-provided arguments in `uv run` et al ([#5364](https://github.com/astral-sh/uv/pull/5364)) ## 0.2.30 ### Enhancements - Infer missing `.exe` in Windows Python discovery ([#5456](https://github.com/astral-sh/uv/pull/5456)) - Make `--reinstall` imply `--refresh` ([#5425](https://github.com/astral-sh/uv/pull/5425)) ### CLI - Add `--no-config` to replace `--isolated` ([#5463](https://github.com/astral-sh/uv/pull/5463)) - Cache metadata for source tree dependencies ([#5423](https://github.com/astral-sh/uv/pull/5423)) ### Bug fixes - Avoid canonicalizing executables on Windows ([#5446](https://github.com/astral-sh/uv/pull/5446)) - Set standard permissions for temporary files ([#5457](https://github.com/astral-sh/uv/pull/5457)) ### Preview features - Allow distributions to be absent in deserialization ([#5453](https://github.com/astral-sh/uv/pull/5453)) - Merge identical forks ([#5405](https://github.com/astral-sh/uv/pull/5405)) - Minor consistency fixes for code blocks ([#5437](https://github.com/astral-sh/uv/pull/5437)) - Prefer "lockfile" to "lock file" ([#5427](https://github.com/astral-sh/uv/pull/5427)) - Update documentation sections ([#5452](https://github.com/astral-sh/uv/pull/5452)) - Use `sitecustomize.py` to implement environment layering ([#5462](https://github.com/astral-sh/uv/pull/5462)) - Use stripped variants by default in Python install ([#5451](https://github.com/astral-sh/uv/pull/5451)) ## 0.2.31 ### Enhancements - Add `--relocatable` flag to `uv venv` ([#5515](https://github.com/astral-sh/uv/pull/5515)) - Support `xz`-compressed packages ([#5513](https://github.com/astral-sh/uv/pull/5513)) - Warn, but don't error, when encountering tilde `.dist-info` directories ([#5520](https://github.com/astral-sh/uv/pull/5520)) ### Bug fixes - Make `pip list --editable` conflict with `--exclude-editable` ([#5506](https://github.com/astral-sh/uv/pull/5506)) - Add some missing reinstall-refresh calls ([#5497](https://github.com/astral-sh/uv/pull/5497)) - Avoid warning users for missing self-extra lower bounds ([#5518](https://github.com/astral-sh/uv/pull/5518)) - Generate hashes for `--find-links` entries ([#5544](https://github.com/astral-sh/uv/pull/5544)) - Retain editable designation for cached wheel installs ([#5545](https://github.com/astral-sh/uv/pull/5545)) - Use 666 rather than 644 for default permissions ([#5498](https://github.com/astral-sh/uv/pull/5498)) - Retry on incomplete body ([#5555](https://github.com/astral-sh/uv/pull/5555)) - Ban `--no-cache` with `--link-mode=symlink` ([#5519](https://github.com/astral-sh/uv/pull/5519)) ### Preview features - Allow `uv pip install` for unmanaged projects ([#5504](https://github.com/astral-sh/uv/pull/5504)) - Compare simplified paths in Windows exclusion tests ([#5525](https://github.com/astral-sh/uv/pull/5525)) - Respect reinstalls in cached environments ([#5499](https://github.com/astral-sh/uv/pull/5499)) - Use `hatchling` rather than implicit `setuptools` default ([#5527](https://github.com/astral-sh/uv/pull/5527)) - Use relocatable installs to support concurrency-safe cached environments ([#5509](https://github.com/astral-sh/uv/pull/5509)) - Support `--editable` installs for `uv tool` ([#5454](https://github.com/astral-sh/uv/pull/5454)) - Fix basic case of overlapping markers ([#5488](https://github.com/astral-sh/uv/pull/5488)) ## 0.2.32 ### Enhancements - Deprecate the `--isolated` flag in favor of `--no-config` ([#5466](https://github.com/astral-sh/uv/pull/5466)) - Re-enable `requires-python` narrowing in forks ([#5583](https://github.com/astral-sh/uv/pull/5583)) ### Performance - Skip copying to empty entries in seekable zip ([#5571](https://github.com/astral-sh/uv/pull/5571)) - Use a consistent buffer size for downloads ([#5569](https://github.com/astral-sh/uv/pull/5569)) - Use a consistent buffer size when writing out zip files ([#5570](https://github.com/astral-sh/uv/pull/5570)) ### Bug fixes - Avoid setting executable permissions on files we might not own ([#5582](https://github.com/astral-sh/uv/pull/5582)) - Statically link liblzma ([#5577](https://github.com/astral-sh/uv/pull/5577)) ### Preview features - Implement `uv run --directory` ([#5566](https://github.com/astral-sh/uv/pull/5566)) - Add `--isolated` support to `uv run` ([#5471](https://github.com/astral-sh/uv/pull/5471)) - Add `--no-workspace` and `--no-project` in lieu of `--isolated` ([#5465](https://github.com/astral-sh/uv/pull/5465)) - Add documentation for cache clearing ([#5517](https://github.com/astral-sh/uv/pull/5517)) - Add forks to lockfile, don't read them yet ([#5480](https://github.com/astral-sh/uv/pull/5480)) - Add links to documentation footer ([#5616](https://github.com/astral-sh/uv/pull/5616)) - Error when multiple git references are provided in `uv add` ([#5502](https://github.com/astral-sh/uv/pull/5502)) - Improvements to the project concept docs ([#5634](https://github.com/astral-sh/uv/pull/5634)) - List installed tools when no command is provided to `uv tool run` ([#5553](https://github.com/astral-sh/uv/pull/5553)) - Make `--directory` a global argument ([#5579](https://github.com/astral-sh/uv/pull/5579)) - Reframe use of `--isolated` in `tool run` ([#5470](https://github.com/astral-sh/uv/pull/5470)) - Remove `--isolated` usages from the `uv python` API ([#5468](https://github.com/astral-sh/uv/pull/5468)) - Rename more use of "lock file" to "lockfile" ([#5629](https://github.com/astral-sh/uv/pull/5629)) - Suppress resolver output by default in `uv run` and `uv tool run` ([#5580](https://github.com/astral-sh/uv/pull/5580)) - Wrap documentation at 100 characters ([#5635](https://github.com/astral-sh/uv/pull/5635)) ## 0.2.33 ### Enhancements - Add support for `ksh` to relocatable virtual environments ([#5640](https://github.com/astral-sh/uv/pull/5640)) ### CLI - Add help sections for global options ([#5665](https://github.com/astral-sh/uv/pull/5665)) - Move `--python` and `--python-version` into the "Python options" help ([#5691](https://github.com/astral-sh/uv/pull/5691)) - Show help specific options (i.e. `--no-pager`) in `uv help` ([#5516](https://github.com/astral-sh/uv/pull/5516)) - Update top-level command descriptions ([#5706](https://github.com/astral-sh/uv/pull/5706)) ### Bug fixes - Remove lingering executables after failed installs ([#5666](https://github.com/astral-sh/uv/pull/5666)) - Switch from heuristic freshness lifetime to hard-coded value ([#5654](https://github.com/astral-sh/uv/pull/5654)) ### Documentation - Don't use equals signs for CLI options with values ([#5704](https://github.com/astral-sh/uv/pull/5704)) ### Preview features - Add `--package` to `uv sync` ([#5656](https://github.com/astral-sh/uv/pull/5656)) - Add documentation for caching the uv cache in GHA ([#5663](https://github.com/astral-sh/uv/pull/5663)) - Avoid persisting `uv add` calls that result in resolver errors ([#5664](https://github.com/astral-sh/uv/pull/5664)) - Bold active nav links for accessibility ([#5673](https://github.com/astral-sh/uv/pull/5673)) - Check idempotence in packse lock scenarios ([#5485](https://github.com/astral-sh/uv/pull/5485)) - Detect python version from python project by default in `uv venv` ([#5592](https://github.com/astral-sh/uv/pull/5592)) - Drop badges from docs landing ([#5617](https://github.com/astral-sh/uv/pull/5617)) - Fix non-registry serialization for receipts ([#5668](https://github.com/astral-sh/uv/pull/5668)) - Generate CLI reference for documentation ([#5685](https://github.com/astral-sh/uv/pull/5685)) - Improve copy of console command examples ([#5397](https://github.com/astral-sh/uv/pull/5397)) - Improve the project guide ([#5626](https://github.com/astral-sh/uv/pull/5626)) - Improve the Python version concepts documentation ([#5638](https://github.com/astral-sh/uv/pull/5638)) - Improve the dependency concept documentation ([#5658](https://github.com/astral-sh/uv/pull/5658)) - Include newly-added optional dependencies in lockfile ([#5686](https://github.com/astral-sh/uv/pull/5686)) - Initialize the cache in `uv init` ([#5669](https://github.com/astral-sh/uv/pull/5669)) - Limit sync after `uv add` ([#5705](https://github.com/astral-sh/uv/pull/5705)) - Move pip-compatibility doc into pip interface section ([#5670](https://github.com/astral-sh/uv/pull/5670)) - Move settings reference to reference section ([#5689](https://github.com/astral-sh/uv/pull/5689)) - Omit the nav bar title when it has no use ([#5316](https://github.com/astral-sh/uv/pull/5316)) - Omit transitive development dependencies from workspace lockfile ([#5646](https://github.com/astral-sh/uv/pull/5646)) - Prioritize forks based on Python narrowing ([#5642](https://github.com/astral-sh/uv/pull/5642)) - Prioritize forks based on upper bounds ([#5643](https://github.com/astral-sh/uv/pull/5643)) - Prompt an early jump to the feature overview during first steps ([#5655](https://github.com/astral-sh/uv/pull/5655)) - Remove breadcrumbs for navigation ([#5676](https://github.com/astral-sh/uv/pull/5676)) - Replace `--python-preference installed` with `managed` ([#5637](https://github.com/astral-sh/uv/pull/5637)) - Set lower bounds in `uv add` ([#5688](https://github.com/astral-sh/uv/pull/5688)) - Simplify GHA `UV_SYSTEM_PYTHON` examples ([#5659](https://github.com/astral-sh/uv/pull/5659)) - Support legacy tool receipts with PEP 508 requirements ([#5679](https://github.com/astral-sh/uv/pull/5679)) - Unhide the experimental top-level commands ([#5700](https://github.com/astral-sh/uv/pull/5700)) - Use "uv" for title of index instead of "Introduction" ([#5677](https://github.com/astral-sh/uv/pull/5677)) - Use fork markers and fork preferences in resolution with lockfile ([#5481](https://github.com/astral-sh/uv/pull/5481)) - Use full requirement when serializing receipt ([#5494](https://github.com/astral-sh/uv/pull/5494)) - Use intersection rather than union for `requires-python` ([#5644](https://github.com/astral-sh/uv/pull/5644)) - `uvx` warn when no executables are available ([#5675](https://github.com/astral-sh/uv/pull/5675)) ## 0.2.34 ### Enhancements - Always strip in release mode ([#5745](https://github.com/astral-sh/uv/pull/5745)) - Assume `git+` prefix when URLs end in `.git` ([#5868](https://github.com/astral-sh/uv/pull/5868)) - Support build constraints ([#5639](https://github.com/astral-sh/uv/pull/5639)) ### CLI - Create help sections for build, install, resolve, and index ([#5693](https://github.com/astral-sh/uv/pull/5693)) - Improve CLI documentation for global options ([#5834](https://github.com/astral-sh/uv/pull/5834)) - Improve `--python` CLI documentation ([#5869](https://github.com/astral-sh/uv/pull/5869)) - Improve display order of top-level commands ([#5830](https://github.com/astral-sh/uv/pull/5830)) ### Bug fixes - Allow downloading wheels for metadata with `--no-binary` ([#5707](https://github.com/astral-sh/uv/pull/5707)) - Reject `pyproject.toml` in `--config-file` ([#5842](https://github.com/astral-sh/uv/pull/5842)) - Remove double-proxy nodes in error reporting ([#5738](https://github.com/astral-sh/uv/pull/5738)) - Respect pre-release preferences from input files ([#5736](https://github.com/astral-sh/uv/pull/5736)) - Support overlapping local and non-local requirements in forks ([#5812](https://github.com/astral-sh/uv/pull/5812)) ### Preview features - Add "next steps" to some early documentation pages ([#5825](https://github.com/astral-sh/uv/pull/5825)) - Add `--no-build-isolation` to uv lock et al ([#5829](https://github.com/astral-sh/uv/pull/5829)) - Add `--no-sources` to avoid reading from `tool.uv.sources` ([#5801](https://github.com/astral-sh/uv/pull/5801)) - Add `uv add --no-sync` and `uv remove --no-sync` ([#5881](https://github.com/astral-sh/uv/pull/5881)) - Add a guide for publishing packages ([#5794](https://github.com/astral-sh/uv/pull/5794)) - Address some feedback in the tools documentation ([#5827](https://github.com/astral-sh/uv/pull/5827)) - Avoid lingering dev and optional dependencies in `uv tree` ([#5766](https://github.com/astral-sh/uv/pull/5766)) - Avoid mismatch in `--locked` with Git dependencies ([#5865](https://github.com/astral-sh/uv/pull/5865)) - Avoid panic when re-locking with precise commit ([#5863](https://github.com/astral-sh/uv/pull/5863)) - Avoid using already-installed tools on `--upgrade` or `--reinstall` ([#5799](https://github.com/astral-sh/uv/pull/5799)) - Better workspace documentation ([#5728](https://github.com/astral-sh/uv/pull/5728)) - Collapse policies section into reference ([#5696](https://github.com/astral-sh/uv/pull/5696)) - Don't show deprecated warning in `uvx --isolated` ([#5798](https://github.com/astral-sh/uv/pull/5798)) - Ensure `python`-to-`pythonX.Y` symlink exists in downloaded Pythons ([#5849](https://github.com/astral-sh/uv/pull/5849)) - Fix CLI reference URLs to subcommands ([#5722](https://github.com/astral-sh/uv/pull/5722)) - Fix some console blocks in the environment doc ([#5826](https://github.com/astral-sh/uv/pull/5826)) - Group resolver options in lockfile ([#5853](https://github.com/astral-sh/uv/pull/5853)) - Improve CLI documentation for `uv tree` ([#5870](https://github.com/astral-sh/uv/pull/5870)) - Improve documentation for `uv init` CLI ([#5862](https://github.com/astral-sh/uv/pull/5862)) - Improvements to the documentation ([#5718](https://github.com/astral-sh/uv/pull/5718)) - Link to the GitHub integration guide from the cache concept ([#5828](https://github.com/astral-sh/uv/pull/5828)) - Make some minor tweaks to the docs ([#5786](https://github.com/astral-sh/uv/pull/5786)) - Omit local segments when adding uv add bounds ([#5753](https://github.com/astral-sh/uv/pull/5753)) - Remove top-level bar from Python installs ([#5788](https://github.com/astral-sh/uv/pull/5788)) - Replace `uv help python` references in CLI documentation with links ([#5871](https://github.com/astral-sh/uv/pull/5871)) - Respect `.python-version` in `--isolated` runs ([#5741](https://github.com/astral-sh/uv/pull/5741)) - Respect malformed `.dist-info` directories in tool installs ([#5756](https://github.com/astral-sh/uv/pull/5756)) - Reuse existing virtualenvs with `--no-project` ([#5846](https://github.com/astral-sh/uv/pull/5846)) - Rewrite resolver docs ([#5723](https://github.com/astral-sh/uv/pull/5723)) - Show default and possible options in CLI reference documentation ([#5720](https://github.com/astral-sh/uv/pull/5720)) - Skip files when detecting workspace members ([#5735](https://github.com/astral-sh/uv/pull/5735)) - Support empty dependencies in PEP 723 scripts ([#5864](https://github.com/astral-sh/uv/pull/5864)) - Support uv add `--dev` in virtual workspaces ([#5821](https://github.com/astral-sh/uv/pull/5821)) - Update documentation index ([#5824](https://github.com/astral-sh/uv/pull/5824)) - Update resolver reference documentation ([#5823](https://github.com/astral-sh/uv/pull/5823)) - Update the override section with some content from the README ([#5820](https://github.com/astral-sh/uv/pull/5820)) - Update the resolution concept documentation ([#5813](https://github.com/astral-sh/uv/pull/5813)) - Use cache for Python install temporary directories ([#5787](https://github.com/astral-sh/uv/pull/5787)) - Use lockfile directly in `uv tree` ([#5761](https://github.com/astral-sh/uv/pull/5761)) - Use uv installer during build ([#5854](https://github.com/astral-sh/uv/pull/5854)) - Filter `uv tree` to current platform by default ([#5763](https://github.com/astral-sh/uv/pull/5763)) - Redact registry credentials in lockfile ([#5803](https://github.com/astral-sh/uv/pull/5803)) - Show extras and dev dependencies in `uv tree` ([#5768](https://github.com/astral-sh/uv/pull/5768)) - Support `--python-platform` in `uv tree` ([#5764](https://github.com/astral-sh/uv/pull/5764)) - Add help heading for `--no-sources` ([#5833](https://github.com/astral-sh/uv/pull/5833)) - Avoid reusing incompatible distributions across lock and sync ([#5845](https://github.com/astral-sh/uv/pull/5845)) - Fix broken anchor links in docs about dependencies ([#5769](https://github.com/astral-sh/uv/pull/5769)) - Fix the default value of python-preference in docs/reference/settings.md ([#5755](https://github.com/astral-sh/uv/pull/5755)) - Improve CLI documentation for `uv run` ([#5841](https://github.com/astral-sh/uv/pull/5841)) - Remove some trailing backticks from the docs ([#5781](https://github.com/astral-sh/uv/pull/5781)) - Use `uvx` in docs serve contributing command ([#5795](https://github.com/astral-sh/uv/pull/5795)) ## 0.2.35 ### CLI - Deprecate `--system` and `--no-system` in `uv venv` ([#5925](https://github.com/astral-sh/uv/pull/5925)) - Make `--upgrade` imply `--refresh` ([#5943](https://github.com/astral-sh/uv/pull/5943)) - Warn when there are missing bounds on transitive dependencies with `--resolution-strategy lowest` ([#5953](https://github.com/astral-sh/uv/pull/5953)) ### Configuration - Add support for `no-build-isolation-package` ([#5894](https://github.com/astral-sh/uv/pull/5894)) ### Performance - Enable LTO optimizations in release builds to reduce binary size ([#5904](https://github.com/astral-sh/uv/pull/5904)) - Prefetch metadata in `--no-deps` mode ([#5918](https://github.com/astral-sh/uv/pull/5918)) ### Bug fixes - Display portable paths in POSIX virtual environment activation commands ([#5956](https://github.com/astral-sh/uv/pull/5956)) - Respect subdirectories when locating Git workspaces ([#5944](https://github.com/astral-sh/uv/pull/5944)) ### Documentation - Improve the `uv venv` CLI documentation ([#5963](https://github.com/astral-sh/uv/pull/5963)) ### Preview features - Add CLI flags to reference documentation ([#5926](https://github.com/astral-sh/uv/pull/5926)) - Add `update` alias for `uv tool upgrade` ([#5948](https://github.com/astral-sh/uv/pull/5948)) - Add caveat about pip interface name ([#5940](https://github.com/astral-sh/uv/pull/5940)) - Add hint for long help to `uvx` ([#5971](https://github.com/astral-sh/uv/pull/5971)) - Avoid requires-python warning in virtual-only workspace ([#5895](https://github.com/astral-sh/uv/pull/5895)) - Discard forks when using `--upgrade` ([#5905](https://github.com/astral-sh/uv/pull/5905)) - Document the `tool upgrade` command ([#5947](https://github.com/astral-sh/uv/pull/5947)) - Document virtual environment discovery ([#5965](https://github.com/astral-sh/uv/pull/5965)) - Enable mirror for `python-build-standalone` downloads ([#5719](https://github.com/astral-sh/uv/pull/5719)) - Fix reuse of Git commits in lockfile ([#5908](https://github.com/astral-sh/uv/pull/5908)) - Ignore local configuration in tool commands ([#5923](https://github.com/astral-sh/uv/pull/5923)) - Improve the CLI documentation for `uv add` ([#5914](https://github.com/astral-sh/uv/pull/5914)) - Improve the CLI documentation for `uv remove` ([#5916](https://github.com/astral-sh/uv/pull/5916)) - Improve the `uv lock` CLI documentation ([#5932](https://github.com/astral-sh/uv/pull/5932)) - Improve the `uv python` CLI documentation ([#5961](https://github.com/astral-sh/uv/pull/5961)) - Improve the `uv sync` CLI documentation ([#5930](https://github.com/astral-sh/uv/pull/5930)) - Improve the `uv tree` CLI documentation ([#5917](https://github.com/astral-sh/uv/pull/5917)) - Fix link to tools concept page ([#5906](https://github.com/astral-sh/uv/pull/5906)) - Add `uv tool upgrade` command ([#5197](https://github.com/astral-sh/uv/pull/5197)) - Implement marker trees using algebraic decision diagrams ([#5898](https://github.com/astral-sh/uv/pull/5898)) - Make repeated `uv add` operations simpler ([#5922](https://github.com/astral-sh/uv/pull/5922)) - Move some documents to relevant sections ([#5968](https://github.com/astral-sh/uv/pull/5968)) - Rename `distribution` to `packages` in lockfile ([#5861](https://github.com/astral-sh/uv/pull/5861)) - Respect `--upgrade-package` in tool install ([#5941](https://github.com/astral-sh/uv/pull/5941)) - Respect `--upgrade-package` when resolving from lockfile ([#5907](https://github.com/astral-sh/uv/pull/5907)) - Retain and respect settings in tool upgrades ([#5937](https://github.com/astral-sh/uv/pull/5937)) - Search beyond workspace root when discovering configuration ([#5931](https://github.com/astral-sh/uv/pull/5931)) - Show build and install summaries in `uv run` and `uv tool run` ([#5899](https://github.com/astral-sh/uv/pull/5899)) - Support relative path wheels ([#5969](https://github.com/astral-sh/uv/pull/5969)) - Update the interface for declaring Python download preferences ([#5936](https://github.com/astral-sh/uv/pull/5936)) - Use cached environments for `--with` layers ([#5897](https://github.com/astral-sh/uv/pull/5897)) - Warn when project-specific settings are passed to non-project `uv run` commands ([#5977](https://github.com/astral-sh/uv/pull/5977)) ## 0.2.36 ### Bug fixes - Use consistent canonicalization for URLs ([#5980](https://github.com/astral-sh/uv/pull/5980)) - Improve warning message when parsing `pyproject.toml` fails ([#6009](https://github.com/astral-sh/uv/pull/6009)) - Improve handling of overlapping markers in universal resolver ([#5887](https://github.com/astral-sh/uv/pull/5887)) ### Preview features - Add resolver error context to `run` and `tool run` ([#5991](https://github.com/astral-sh/uv/pull/5991)) - Avoid replacing executables on no-op upgrades ([#5998](https://github.com/astral-sh/uv/pull/5998)) - Colocate Python install cache with destination directory ([#6043](https://github.com/astral-sh/uv/pull/6043)) - Filter mixed sources from `--find-links` entries in lockfile ([#6025](https://github.com/astral-sh/uv/pull/6025)) - Fix some outdated documentation discussing Python environments ([#6058](https://github.com/astral-sh/uv/pull/6058)) - Fix projects guide typo ([#6033](https://github.com/astral-sh/uv/pull/6033)) - Fix tools guide typo ([#6027](https://github.com/astral-sh/uv/pull/6027)) - Hide python options in `uv tool list` help ([#6003](https://github.com/astral-sh/uv/pull/6003)) - Improve top-level help for `uv tool` commands ([#5983](https://github.com/astral-sh/uv/pull/5983)) - Move help documentation into dedicated page ([#6057](https://github.com/astral-sh/uv/pull/6057)) - Remove `editable: false` support ([#5987](https://github.com/astral-sh/uv/pull/5987)) - Remove uses of `Option` in `ResolutionGraph` ([#6035](https://github.com/astral-sh/uv/pull/6035)) - Resolve relative `tool.uv.sources` relative to containing project ([#6045](https://github.com/astral-sh/uv/pull/6045)) - Support PEP 723 scripts in `uv add` and `uv remove` ([#5995](https://github.com/astral-sh/uv/pull/5995)) - Support `tool.uv` in PEP 723 scripts ([#5990](https://github.com/astral-sh/uv/pull/5990)) - Treat local indexes as registry sources in lockfile ([#6016](https://github.com/astral-sh/uv/pull/6016)) - Use simplified paths in lockfile ([#6049](https://github.com/astral-sh/uv/pull/6049)) - Use upgrade-specific output for tool upgrade ([#5997](https://github.com/astral-sh/uv/pull/5997)) ## 0.2.37 ### Performance - Avoid cloning requirement for unchanged markers ([#6116](https://github.com/astral-sh/uv/pull/6116)) ### Bug fixes - Fix loading of cached metadata for Git distributions with subdirectories ([#6094](https://github.com/astral-sh/uv/pull/6094)) ### Error messages - Add env var to `--link-mode=copy` warning ([#6103](https://github.com/astral-sh/uv/pull/6103)) - Avoid displaying "failed to download" on build failures for local source distributions ([#6075](https://github.com/astral-sh/uv/pull/6075)) - Improve display of available package ranges ([#6118](https://github.com/astral-sh/uv/pull/6118)) - Use "your requirements" consistently in resolver error messages ([#6113](https://github.com/astral-sh/uv/pull/6113)) ### Preview features - Add `python-version-file` to GitHub integration documentation ([#6086](https://github.com/astral-sh/uv/pull/6086)) - Always narrow markers by Python version ([#6076](https://github.com/astral-sh/uv/pull/6076)) - Avoid warning for redundant `--no-project` ([#6111](https://github.com/astral-sh/uv/pull/6111)) - Change the definition of `--locked` to require satisfaction check ([#6102](https://github.com/astral-sh/uv/pull/6102)) - Improve debug log for interpreter requests during project commands ([#6120](https://github.com/astral-sh/uv/pull/6120)) - Improve display of resolution errors for workspace member conflicts with optional dependencies ([#6123](https://github.com/astral-sh/uv/pull/6123)) - Improve resolver error messages for single-project workspaces ([#6095](https://github.com/astral-sh/uv/pull/6095)) - Improve resolver error messages referencing workspace members ([#6092](https://github.com/astral-sh/uv/pull/6092)) - Invalidate `uv.lock` if registry sources are removed ([#6026](https://github.com/astral-sh/uv/pull/6026)) - Propagate fork markers to extras ([#6065](https://github.com/astral-sh/uv/pull/6065)) - Redact Git credentials from `pyproject.toml` ([#6074](https://github.com/astral-sh/uv/pull/6074)) - Redact Git credentials in lockfile ([#6070](https://github.com/astral-sh/uv/pull/6070)) - Remove 'tool' reference on `uv run` CLI ([#6110](https://github.com/astral-sh/uv/pull/6110)) - Remove `same-graph` merging in resolver ([#6077](https://github.com/astral-sh/uv/pull/6077)) - Strip SHA when constructing package source ([#6097](https://github.com/astral-sh/uv/pull/6097)) - Treat Git sources as immutable in lockfile ([#6109](https://github.com/astral-sh/uv/pull/6109)) - Use the proper singular form for workspace member dependencies in resolver errors ([#6128](https://github.com/astral-sh/uv/pull/6128)) - Use sets rather than vectors for lockfile requirements ([#6107](https://github.com/astral-sh/uv/pull/6107)) - Normalize `python_version` markers to `python_full_version` ([#6126](https://github.com/astral-sh/uv/pull/6126)) - Update Pythons to include Python 3.12.5 ([#6087](https://github.com/astral-sh/uv/pull/6087)) uv-0.9.17+ds1/changelogs/0.3.x.md000066400000000000000000000431231520155276700161440ustar00rootroot00000000000000# Changelog 0.3.x ## 0.3.0 This release introduces the uv [project](https://docs.astral.sh/uv/guides/projects/), [tool](https://docs.astral.sh/uv/guides/tools/), [script](https://docs.astral.sh/uv/guides/scripts/), and [python](https://docs.astral.sh/uv/guides/install-python/) interfaces. If you've been following uv's development, you've probably seen these new commands behind a preview flag. Now, the interfaces are stable and ready for production-use. These features are all documented in [new, comprehensive documentation](https://docs.astral.sh/uv/). This release also stabilizes preview functionality in `uv venv`: - `uv venv --python ` will [automatically download](https://docs.astral.sh/uv/concepts/python-versions/#requesting-a-version) the Python version if required - `uv venv` will read the required Python version from the `.python-version` file or `pyproject.toml` The `uv pip` interface should not be affected by any breaking changes. Note the following changelog entries does not include all the new features since they were added incrementally as preview features. See the [feature page](https://docs.astral.sh/uv/getting-started/features/) in the documentation for a comprehensive listing, or read the [blog post](https://astral.sh/blog/uv-unified-python-packaging) for more context on the new features. ### Breaking changes - Migrate to XDG and Linux strategy for macOS directories ([#5806](https://github.com/astral-sh/uv/pull/5806)) - Move concurrency settings to top-level ([#4257](https://github.com/astral-sh/uv/pull/4257)) - Apply system Python filtering to executable name requests ([#4309](https://github.com/astral-sh/uv/pull/4309)) - Remove `--legacy-setup-py` command-line argument ([#4255](https://github.com/astral-sh/uv/pull/4255)) - Stabilize preview features ([#6166](https://github.com/astral-sh/uv/pull/6166)) ### Enhancements - Add 32-bit Windows target ([#6252](https://github.com/astral-sh/uv/pull/6252)) - Add support for `python_version in ...` markers ([#6172](https://github.com/astral-sh/uv/pull/6172)) - Allow user to constrain supported lock environments ([#6210](https://github.com/astral-sh/uv/pull/6210)) - Lift requirement that .egg-info filenames must include version ([#6179](https://github.com/astral-sh/uv/pull/6179)) - Change "any of" to "all of" in error messages ([#6222](https://github.com/astral-sh/uv/pull/6222)) - Collapse redundant dependency clauses enumerating available versions ([#6160](https://github.com/astral-sh/uv/pull/6160)) - Collapse unavailable packages in resolver errors ([#6154](https://github.com/astral-sh/uv/pull/6154)) - Fix messages for unavailable packages when range is plural ([#6221](https://github.com/astral-sh/uv/pull/6221)) - Improve resolver error messages when `--offline` is used ([#6156](https://github.com/astral-sh/uv/pull/6156)) - Avoid overwriting dependencies with different markers in `uv add` ([#6010](https://github.com/astral-sh/uv/pull/6010)) - Simplify available package version ranges when the name includes markers or extras ([#6162](https://github.com/astral-sh/uv/pull/6162)) - Simplify version ranges reported for unavailable packages ([#6155](https://github.com/astral-sh/uv/pull/6155)) - Rename `environment-markers` to `resolution-markers` ([#6240](https://github.com/astral-sh/uv/pull/6240)) - Support `uv add -r requirements.txt` ([#6005](https://github.com/astral-sh/uv/pull/6005)) ### CLI - Hide global options in `uv generate-shell-completion` ([#6170](https://github.com/astral-sh/uv/pull/6170)) - Show generate-shell-completion command in `uv help` ([#6180](https://github.com/astral-sh/uv/pull/6180)) - Special-case reinstalls in environment update summaries ([#6243](https://github.com/astral-sh/uv/pull/6243)) - Add output when `uv add` and `uv remove` update scripts ([#6231](https://github.com/astral-sh/uv/pull/6231)) - Add support for `package@latest` in `tool run` ([#6138](https://github.com/astral-sh/uv/pull/6138)) - Show `python find` output with `-q` ([#6256](https://github.com/astral-sh/uv/pull/6256)) - Warn when `--upgrade` is passed to `tool run` ([#6140](https://github.com/astral-sh/uv/pull/6140)) ### Configuration - Allow customizing the tool install directory with `UV_TOOL_BIN_DIR` ([#6207](https://github.com/astral-sh/uv/pull/6207)) ### Performance - Use `FxHash` in `uv-auth` ([#6149](https://github.com/astral-sh/uv/pull/6149)) ### Bug fixes - Avoid panicking when the resolver thread encounters a closed channel ([#6182](https://github.com/astral-sh/uv/pull/6182)) - Respect release-only semantics of `python_full_version` when constructing markers ([#6171](https://github.com/astral-sh/uv/pull/6171)) - Tolerate missing `[project]` table in `uv venv` ([#6178](https://github.com/astral-sh/uv/pull/6178)) - Avoid using workspace `lock_path` as relative root ([#6157](https://github.com/astral-sh/uv/pull/6157)) ### Documentation - Preview changes are now included in the standard changelog ([#6259](https://github.com/astral-sh/uv/pull/6259)) - Document dynamic metadata behavior for cache ([#5993](https://github.com/astral-sh/uv/pull/5993)) - Document the effect of ordering on package priority ([#6211](https://github.com/astral-sh/uv/pull/6211)) - Make some edits to the workspace concept documentation ([#6223](https://github.com/astral-sh/uv/pull/6223)) - Update environment variables doc ([#5994](https://github.com/astral-sh/uv/pull/5994)) - Disable collapsible navigation in the documentation ([#5674](https://github.com/astral-sh/uv/pull/5674)) - Document `uv add` and `uv remove` behavior with markers ([#6163](https://github.com/astral-sh/uv/pull/6163)) - Document the Python installation directory ([#6227](https://github.com/astral-sh/uv/pull/6227)) - Document the `uv.pip` section semantics ([#6225](https://github.com/astral-sh/uv/pull/6225)) - Document the cache directory ([#6229](https://github.com/astral-sh/uv/pull/6229)) - Document the tools directory ([#6228](https://github.com/astral-sh/uv/pull/6228)) - Document yanked packages caveat during sync ([#6219](https://github.com/astral-sh/uv/pull/6219)) - Link to persistent configuration options in Python versions document ([#6226](https://github.com/astral-sh/uv/pull/6226)) - Link to the projects concept from the dependencies concept ([#6224](https://github.com/astral-sh/uv/pull/6224)) - Improvements to the Docker installation guide ([#6216](https://github.com/astral-sh/uv/pull/6216)) - Increase the size of navigation entries ([#6233](https://github.com/astral-sh/uv/pull/6233)) - Install `ca-certificates` in docker and use pipefail ([#6208](https://github.com/astral-sh/uv/pull/6208)) - Add script support to feature highlights in index ([#6251](https://github.com/astral-sh/uv/pull/6251)) - Show `uv generate-shell-completion` in CLI documentation reference ([#6146](https://github.com/astral-sh/uv/pull/6146)) - Update Docker guide for projects ([#6217](https://github.com/astral-sh/uv/pull/6217)) - Use `uv add --script` in guide ([#6215](https://github.com/astral-sh/uv/pull/6215)) - Show pinned version example on in GitHub Actions integration guide ([#6234](https://github.com/astral-sh/uv/pull/6234)) ## 0.3.1 ### Enhancements - Add `--with-editable` support to `uv run` ([#6262](https://github.com/astral-sh/uv/pull/6262)) - Respect `.python-version` files and `pyproject.toml` in `uv python find` ([#6369](https://github.com/astral-sh/uv/pull/6369)) - Allow manylinux compatibility override via `_manylinux` module ([#6039](https://github.com/astral-sh/uv/pull/6039)) ### CLI - Avoid treating `uv add -r` as `--raw-sources` ([#6287](https://github.com/astral-sh/uv/pull/6287)) ### Bug fixes - Always invoke found interpreter when `uv run python` is used ([#6363](https://github.com/astral-sh/uv/pull/6363)) - Avoid adding extra newline for script with non-empty prelude ([#6366](https://github.com/astral-sh/uv/pull/6366)) - Fix metadata cache instability for lockfile ([#6332](https://github.com/astral-sh/uv/pull/6332)) - Handle Ctrl-C properly in `uvx` invocations ([#6346](https://github.com/astral-sh/uv/pull/6346)) - Ignore workspace discovery errors with `--no-workspace` ([#6328](https://github.com/astral-sh/uv/pull/6328)) - Invalidate `uv.lock` when virtual `dev-dependencies` change ([#6291](https://github.com/astral-sh/uv/pull/6291)) - Make cache robust to removed archives ([#6284](https://github.com/astral-sh/uv/pull/6284)) - Preserve Git username for SSH dependencies ([#6335](https://github.com/astral-sh/uv/pull/6335)) - Respect `--no-build-isolation` in `uv add` ([#6368](https://github.com/astral-sh/uv/pull/6368)) - Respect `.python-version` files in `uv run` outside projects ([#6361](https://github.com/astral-sh/uv/pull/6361)) - Use `sys_executable` for `uv run` invocations ([#6354](https://github.com/astral-sh/uv/pull/6354)) - Use atomic write for `pip compile` output ([#6274](https://github.com/astral-sh/uv/pull/6274)) - Use consistent logic for deserializing short revisions ([#6341](https://github.com/astral-sh/uv/pull/6341)) ### Documentation - Remove the preview default value of `python-preference` ([#6301](https://github.com/astral-sh/uv/pull/6301)) - Update env vars doc about `XDG_*` variables on macOS ([#6337](https://github.com/astral-sh/uv/pull/6337)) ## 0.3.2 ### Configuration - Add support for configuring `python-downloads` with `UV_PYTHON_DOWNLOADS` ([#6436](https://github.com/astral-sh/uv/pull/6436)) - Add support for configuring the `python-preference` with `UV_PYTHON_PREFERENCE` ([#6432](https://github.com/astral-sh/uv/pull/6432)) - Deny invalid members in workspace schema ([#6450](https://github.com/astral-sh/uv/pull/6450)) ### Performance - Stop streaming wheels when `METADATA` is discovered (if range requests aren't supported) ([#6470](https://github.com/astral-sh/uv/pull/6470)) ### Bug fixes - Remove URI type from JSON Schema ([#6449](https://github.com/astral-sh/uv/pull/6449)) - Fix retrieval of credentials for URLs from cache ([#6452](https://github.com/astral-sh/uv/pull/6452)) - Restore `cache` suffix on Windows cache path ([#6482](https://github.com/astral-sh/uv/pull/6482)) - Treat `.pyw` files as scripts in `uv run` on Windows ([#6453](https://github.com/astral-sh/uv/pull/6453)) - Treat invalid extras as `false` in marker evaluation ([#6395](https://github.com/astral-sh/uv/pull/6395)) - Avoid overwriting symlinks in `pip compile` output ([#6487](https://github.com/astral-sh/uv/pull/6487)) ### Documentation - Add `uv run` hint to the `uvx` guide ([#6454](https://github.com/astral-sh/uv/pull/6454)) - Add a guide for using uv with FastAPI ([#6401](https://github.com/astral-sh/uv/pull/6401)) - Add tip for using `managed = false` to disable project management ([#6465](https://github.com/astral-sh/uv/pull/6465)) - Clarify the `uv tool run`, `uvx`, and `uv run` relationships ([#6455](https://github.com/astral-sh/uv/pull/6455)) - Fix references to `--python-downloads` (it is `--no-python-downloads`) ([#6439](https://github.com/astral-sh/uv/pull/6439)) - Further clarifications to the tools documentation ([#6474](https://github.com/astral-sh/uv/pull/6474)) - Update docs dockerfile (bullseye -> bookworm) ([#6441](https://github.com/astral-sh/uv/pull/6441)) - Update the installation documentation page ([#6468](https://github.com/astral-sh/uv/pull/6468)) - Update pip compatibility pages to mention configuration files support ([#6410](https://github.com/astral-sh/uv/pull/6410)) - Add `uv run` docs for gui scripts ([#6478](https://github.com/astral-sh/uv/pull/6478)) ## 0.3.3 ### Enhancements - Add `uv sync --no-install-project` to skip installation of the project ([#6538](https://github.com/astral-sh/uv/pull/6538)) - Add `uv sync --no-install-workspace` to skip installation of all workspace members ([#6539](https://github.com/astral-sh/uv/pull/6539)) - Add `uv sync --no-install-package` to skip installation of specific packages ([#6540](https://github.com/astral-sh/uv/pull/6540)) - Show previous version in self update message ([#6473](https://github.com/astral-sh/uv/pull/6473)) ### CLI - Add `--no-project` alias for `uv python pin --no-workspace` ([#6514](https://github.com/astral-sh/uv/pull/6514)) - Ignore `.python-version` files in `uv venv` with `--no-config` ([#6513](https://github.com/astral-sh/uv/pull/6513)) - Include virtual environment interpreters in `uv python find` ([#6521](https://github.com/astral-sh/uv/pull/6521)) - Respect `-` as stdin channel for `uv run` ([#6481](https://github.com/astral-sh/uv/pull/6481)) - Revert changes to pyproject.toml when sync fails during `uv add` ([#6526](https://github.com/astral-sh/uv/pull/6526)) ### Configuration - Add `UV_COMPILE_BYTECODE` environment variable ([#6530](https://github.com/astral-sh/uv/pull/6530)) ### Bug fixes - Set `VIRTUAL_ENV` for `uv run` invocations ([#6543](https://github.com/astral-sh/uv/pull/6543)) - Ignore errors in workspace discovery with `--no-project` ([#6554](https://github.com/astral-sh/uv/pull/6554)) ### Documentation - Add documentation for `uv python find` ([#6527](https://github.com/astral-sh/uv/pull/6527)) - Add uv tool install example in Docker ([#6547](https://github.com/astral-sh/uv/pull/6547)) - Document why we do lower bounds ([#6516](https://github.com/astral-sh/uv/pull/6516)) - Fix to miss string termination in PowerShell commands for shell autocompletion documentation ([#6491](https://github.com/astral-sh/uv/pull/6491)) - Fix incorrect workspace members keyword ([#6502](https://github.com/astral-sh/uv/pull/6502)) - Use proper environment variables for Windows ([#6433](https://github.com/astral-sh/uv/pull/6433)) - Improve caveat in `uvx` note ([#6546](https://github.com/astral-sh/uv/pull/6546)) ## 0.3.4 ### CLI - Show `--editable` on the `uv add` CLI ([#6608](https://github.com/astral-sh/uv/pull/6608)) - Add `--refresh` to `tool run` warning for `--with` dependencies ([#6609](https://github.com/astral-sh/uv/pull/6609)) ### Bug fixes - Allow per dependency build isolation for `setup.py`-based projects ([#6517](https://github.com/astral-sh/uv/pull/6517)) - Avoid un-strict syncing by-default for build isolation ([#6606](https://github.com/astral-sh/uv/pull/6606)) - Respect `--no-build-isolation-package` in `uv sync` ([#6605](https://github.com/astral-sh/uv/pull/6605)) - Respect extras and markers on virtual dev dependencies ([#6620](https://github.com/astral-sh/uv/pull/6620)) - Support PEP 723 scripts in GUI files ([#6611](https://github.com/astral-sh/uv/pull/6611)) - Update lockfile after setting minimum bounds in `uv add` ([#6618](https://github.com/astral-sh/uv/pull/6618)) - Use relative paths for `--find-links` and local registries ([#6566](https://github.com/astral-sh/uv/pull/6566)) - Use separate types to represent raw vs. resolver markers ([#6646](https://github.com/astral-sh/uv/pull/6646)) - Parse wheels `WHEEL` and `METADATA` files as email messages ([#6616](https://github.com/astral-sh/uv/pull/6616)) - Support unquoted hrefs in `--find-links` and other HTML sources ([#6622](https://github.com/astral-sh/uv/pull/6622)) - Don't canonicalize paths to user requirements ([#6560](https://github.com/astral-sh/uv/pull/6560)) ### Documentation - Add FastAPI guide to overview ([#6603](https://github.com/astral-sh/uv/pull/6603)) - Add docs for disabling build isolation with `uv sync` ([#6607](https://github.com/astral-sh/uv/pull/6607)) - Add example of reading script from stdin using echo ([#6567](https://github.com/astral-sh/uv/pull/6567)) - Add tip to use intermediate layers in Docker builds ([#6650](https://github.com/astral-sh/uv/pull/6650)) - Clarify need to include `pyproject.toml` with `--no-install-project` ([#6581](https://github.com/astral-sh/uv/pull/6581)) - Move `WORKDIR` directive in Docker examples ([#6652](https://github.com/astral-sh/uv/pull/6652)) - Remove duplicate `WORKDIR` directive in Docker example ([#6651](https://github.com/astral-sh/uv/pull/6651)) ## 0.3.5 ### Enhancements - Add support for `--allow-insecure-host` (aliased to `--trusted-host`) ([#6591](https://github.com/astral-sh/uv/pull/6591)) - Read requirements from `requires.txt` when available ([#6655](https://github.com/astral-sh/uv/pull/6655)) - Respect `tool.uv.environments` in `pip compile --universal` ([#6663](https://github.com/astral-sh/uv/pull/6663)) - Use relative paths by default in `uv add` ([#6686](https://github.com/astral-sh/uv/pull/6686)) - Improve messages for empty solves and installs ([#6588](https://github.com/astral-sh/uv/pull/6588)) ### Bug fixes - Avoid reusing state across tool upgrades ([#6660](https://github.com/astral-sh/uv/pull/6660)) - Detect musl and error for musl Python builds ([#6643](https://github.com/astral-sh/uv/pull/6643)) - Ignore `send` errors in installer ([#6667](https://github.com/astral-sh/uv/pull/6667)) ### Documentation - Add development section to Docker guide and reference new example project ([#6666](https://github.com/astral-sh/uv/pull/6666)) - Add docs for `constraint-dependencies` and `override-dependencies` ([#6596](https://github.com/astral-sh/uv/pull/6596)) - Clarify package priority order in pip compatibility guide ([#6619](https://github.com/astral-sh/uv/pull/6619)) - Fix docs for disabling build isolation with `uv sync` ([#6674](https://github.com/astral-sh/uv/pull/6674)) - Improve consistency of directory lookup instructions in Docker ([#6665](https://github.com/astral-sh/uv/pull/6665)) - Improve lockfile concept documentation, add coverage for upgrades ([#6698](https://github.com/astral-sh/uv/pull/6698)) - Shift the order of some of the Docker guide content ([#6664](https://github.com/astral-sh/uv/pull/6664)) - Use `python` to highlight requirements and use more content tabs ([#6549](https://github.com/astral-sh/uv/pull/6549)) uv-0.9.17+ds1/changelogs/0.4.x.md000066400000000000000000001720451520155276700161530ustar00rootroot00000000000000# Changelog 0.4.x ## 0.4.0 This release adds first-class support for Python projects that are not designed as Python packages (e.g., web applications, data science projects, etc.). In doing so, it includes some breaking changes around uv's handling of projects. Previously, uv required that all projects could be built into distributable Python packages, and installed them into the virtual environment. Projects created by `uv init` always included a `[build-system]` definition and existing projects that did not define a `[build-system]` would use the legacy setuptools build backend by default. Most users are not developing libraries that need to be packaged and published to PyPI. Instead, they're building applications using web frameworks, or running collections of Python scripts in the project's root directory. In these cases, requiring a `[build-system]` was confusing and error-prone. In this release, uv changes the default behavior to orient around these common use cases. In summary, the major changes are: - uv no longer attempts to package and install projects that do not define a `[build-system]`. - While the project itself will not be installed into the virtual environment, its dependencies will still be included. - The previous behavior can be recovered by setting `package = true` in the `[tool.uv]` section of your `pyproject.toml`. - `uv init` no longer creates a `src/` directory or defines a `[build-system]` by default. - The previous behavior can be recovered with `uv init --lib` or `uv init --app --package`. - uv allows and recommends including `[project]` definitions in virtual workspace roots. - Previously, the uv required the `[project]` section to be omitted. - uv allows disabling packaging of projects, even if they define a `[build-system]`, by setting `package = false` in the `[tool.uv]` section of your `pyproject.toml`. See the latest documentation on [build systems in projects](http://docs.astral.sh/uv/concepts/projects/#build-systems) for more details. ### Enhancements - Add first-class support for non-packaged projects ([#6585](https://github.com/astral-sh/uv/pull/6585)) - Add `--app` and `--lib` options to `uv init` ([#6689](https://github.com/astral-sh/uv/pull/6689)) - Use `virtual` source label in lockfile for non-packaged dependencies ([#6728](https://github.com/astral-sh/uv/pull/6728)) - Read hash from URL fragment if `--hashes` are omitted ([#6731](https://github.com/astral-sh/uv/pull/6731)) - Support `{package}@{version}` in `uv tool install` ([#6762](https://github.com/astral-sh/uv/pull/6762)) - Publish additional Docker tags without patch version ([#6734](https://github.com/astral-sh/uv/pull/6734)) ### Bug fixes - Accept either strings or structs for hosts ([#6763](https://github.com/astral-sh/uv/pull/6763)) - Avoid including non-excluded members in parent workspaces ([#6735](https://github.com/astral-sh/uv/pull/6735)) - Avoid reading stale `.egg-info` from mutable sources ([#6714](https://github.com/astral-sh/uv/pull/6714)) - Avoid writing invalid PEP 723 scripts on `tool.uv.sources` ([#6706](https://github.com/astral-sh/uv/pull/6706)) - Compare virtual members when invalidating lockfile ([#6754](https://github.com/astral-sh/uv/pull/6754)) - Do not require workspace members to sync with `--frozen` ([#6737](https://github.com/astral-sh/uv/pull/6737)) - Implement deserialization for trusted host ([#6716](https://github.com/astral-sh/uv/pull/6716)) - Avoid showing duplicate paths in `uv python list` ([#6740](https://github.com/astral-sh/uv/pull/6740)) - Raise an error for unclosed script tags in PEP 723 scripts ([#6704](https://github.com/astral-sh/uv/pull/6704)) ### Documentation - Add dependabot and renovate documentation page ([#6236](https://github.com/astral-sh/uv/pull/6236)) - Bind to the host to allow connections in FastAPI Docker example ([#6753](https://github.com/astral-sh/uv/pull/6753)) - Fix some broken links ([#6705](https://github.com/astral-sh/uv/pull/6705)) - Update FastAPI guide for virtual projects and use `uv init` to create the `pyproject.toml` ([#6752](https://github.com/astral-sh/uv/pull/6752)) - Update project documentation for the application / library concepts ([#6718](https://github.com/astral-sh/uv/pull/6718)) - Update workspace documentation to remove legacy virtual projects ([#6720](https://github.com/astral-sh/uv/pull/6720)) ## 0.4.1 ### Enhancements - Add `uv export --format requirements-txt` ([#6778](https://github.com/astral-sh/uv/pull/6778)) - Allow `@` references in `uv tool install --from` ([#6842](https://github.com/astral-sh/uv/pull/6842)) - Normalize version specifiers by sorting ([#6333](https://github.com/astral-sh/uv/pull/6333)) - Respect the user's upper-bound in `requires-python` ([#6824](https://github.com/astral-sh/uv/pull/6824)) - Use Windows registry to discover Python on Windows directly ([#6761](https://github.com/astral-sh/uv/pull/6761)) - Hint at `--no-workspace` in `uv init` failures ([#6815](https://github.com/astral-sh/uv/pull/6815)) - Update to last PyPy releases ([#6784](https://github.com/astral-sh/uv/pull/6784)) ### Bug fixes - Avoid deadlocks when multiple uv processes lock resources ([#6790](https://github.com/astral-sh/uv/pull/6790)) - Expand tildes when matching against `PATH` ([#6829](https://github.com/astral-sh/uv/pull/6829)) - Fix `uv init --no-project` alias ([#6837](https://github.com/astral-sh/uv/pull/6837)) - Ignore pre-release segments when discovering via `requires-python` ([#6813](https://github.com/astral-sh/uv/pull/6813)) - Support inline optional tables in `uv add` and `uv remove` ([#6787](https://github.com/astral-sh/uv/pull/6787)) - Update default `hello.py` to pass `ruff format` ([#6811](https://github.com/astral-sh/uv/pull/6811)) - Avoid stripping root for user path display ([#6865](https://github.com/astral-sh/uv/pull/6865)) - Error when user-provided environments are disjoint with Python ([#6841](https://github.com/astral-sh/uv/pull/6841)) - Retain alphabetical sorting for `pyproject.toml` in `uv add` operations ([#6388](https://github.com/astral-sh/uv/pull/6388)))) ### Documentation - Add a link to the multiple index docs in the alternative index guide ([#6826](https://github.com/astral-sh/uv/pull/6826)) - Add docs for inline exclude newer in PEP 723 scripts ([#6831](https://github.com/astral-sh/uv/pull/6831)) - Enumerate available Docker tags ([#6768](https://github.com/astral-sh/uv/pull/6768)) - Omit `[pip]` section from configuration file docs ([#6814](https://github.com/astral-sh/uv/pull/6814)) - Update `project.urls` in `pyproject.toml` ([#6844](https://github.com/astral-sh/uv/pull/6844)) - Add docs for AWS CodeArtifact usage ([#6816](https://github.com/astral-sh/uv/pull/6816)) ### Other changes ## 0.4.2 ### Enhancements - Adding support for `.pyc` files in `uv run` ([#6886](https://github.com/astral-sh/uv/pull/6886)) - Treat missing `top_level.txt` as non-fatal ([#6881](https://github.com/astral-sh/uv/pull/6881)) ### Bug fixes - Fix `is_disjoint` check for supported environments ([#6902](https://github.com/astral-sh/uv/pull/6902)) - Remove dangling archives in `uv cache clean ${package}` ([#6915](https://github.com/astral-sh/uv/pull/6915)) - Error when discovered Python is incompatible with `--isolated` workspace ([#6885](https://github.com/astral-sh/uv/pull/6885)) - Warn when discovered Python is incompatible with PEP 723 script ([#6884](https://github.com/astral-sh/uv/pull/6884)) ## 0.4.3 ### Enhancements - Show build backend output when `--verbose` is provided ([#6903](https://github.com/astral-sh/uv/pull/6903)) - Allow `uv sync --frozen --package` without copying member `pyproject.toml` ([#6943](https://github.com/astral-sh/uv/pull/6943)) ### Bug fixes - Avoid panic with missing temporary directory ([#6929](https://github.com/astral-sh/uv/pull/6929)) - Avoid updating incorrect dependencies for sorted `uv add` ([#6939](https://github.com/astral-sh/uv/pull/6939)) - Use lower-bound semantics for all Python compatibility comparisons ([#6882](https://github.com/astral-sh/uv/pull/6882)) ## 0.4.4 ### Enhancements - Allow customizing the project environment path with `UV_PROJECT_ENVIRONMENT` ([#6834](https://github.com/astral-sh/uv/pull/6834)) - Warn when `VIRTUAL_ENV` is set but will not be respected in project commands ([#6864](https://github.com/astral-sh/uv/pull/6864)) - Add `--no-hashes` to `uv export` ([#6954](https://github.com/astral-sh/uv/pull/6954)) - Make HTTP headers title case for backward compatibility ([#6887](https://github.com/astral-sh/uv/pull/6887)) - Pin `.python-version` in `uv init` ([#6869](https://github.com/astral-sh/uv/pull/6869)) - Support `file://` URLs for `UV_PYTHON_INSTALL_MIRROR` ([#6950](https://github.com/astral-sh/uv/pull/6950)) - Introduce more docker tags for uv ([#6053](https://github.com/astral-sh/uv/pull/6053)) ### Bug fixes - Avoid canonicalizing the cache directory ([#6949](https://github.com/astral-sh/uv/pull/6949)) - Show all PyPy versions in `uv python list --all-versions` ([#6917](https://github.com/astral-sh/uv/pull/6917)) - Avoid incorrect `requires-python` marker simplifications ([#6268](https://github.com/astral-sh/uv/pull/6268)) ### Documentation - Add documentation for `UV_PROJECT_ENVIRONMENT` ([#6987](https://github.com/astral-sh/uv/pull/6987)) - Add optional dependencies section to the lockfile document ([#6982](https://github.com/astral-sh/uv/pull/6982)) - Document use of the `file://` scheme in Python installation mirrors ([#6984](https://github.com/astral-sh/uv/pull/6984)) - Fix outdated references to the help menu documentation in the first steps page ([#6980](https://github.com/astral-sh/uv/pull/6980)) - Show env option in CLI reference documentation ([#6863](https://github.com/astral-sh/uv/pull/6863)) - Add bind mount example to `docker.md` ([#6921](https://github.com/astral-sh/uv/pull/6921)) ## 0.4.5 ### Enhancements - Implement `uv build` ([#6895](https://github.com/astral-sh/uv/pull/6895)) - Add `--package` support to `uv build` ([#6990](https://github.com/astral-sh/uv/pull/6990)) - Prune unreachable packages from lockfile ([#6959](https://github.com/astral-sh/uv/pull/6959)) - Prune unreachable wheels from lockfile ([#6961](https://github.com/astral-sh/uv/pull/6961)) - Show build output by default in `uv build` ([#6912](https://github.com/astral-sh/uv/pull/6912)) - Support `uv build --wheel` from source distributions ([#6898](https://github.com/astral-sh/uv/pull/6898)) - Use the root project name for the project virtual environment prompt ([#7021](https://github.com/astral-sh/uv/pull/7021)) ### Bug fixes - Fix handling of inline optional dependencies in `uv add` ([#7023](https://github.com/astral-sh/uv/pull/7023)) - Reflect exit code in `uv tool run` and `uv run` ([#6994](https://github.com/astral-sh/uv/pull/6994)) - Revert `pyproject.toml` modifications on Ctrl-C ([#7024](https://github.com/astral-sh/uv/pull/7024)) - Rollback `pyproject.toml` changes on all errors ([#7022](https://github.com/astral-sh/uv/pull/7022)) - Use correct ordering semantics for narrowing upper-bounded Python requirements ([#7031](https://github.com/astral-sh/uv/pull/7031)) - Fix segfault in Windows trampolines ([#6955](https://github.com/astral-sh/uv/pull/6955)) - Remove unused `__future__.annotations` import in `_virtualenv.py` ([#6996](https://github.com/astral-sh/uv/pull/6996)) ### Documentation - Add documentation for `uv build` ([#6991](https://github.com/astral-sh/uv/pull/6991)) - Add note to `extra` and `all-extras` in `uv sync` help ([#7013](https://github.com/astral-sh/uv/pull/7013)) - Add project docs for `project.scripts` ([#7010](https://github.com/astral-sh/uv/pull/7010)) - Fix available Docker image tag rendering and shorten list ([#7017](https://github.com/astral-sh/uv/pull/7017)) - Touchup to the project environment config section ([#7038](https://github.com/astral-sh/uv/pull/7038)) - Clarify precedence of `uv.toml` ([#6986](https://github.com/astral-sh/uv/pull/6986)) - Fix available Docker tags for `-slim` variants ([#7041](https://github.com/astral-sh/uv/pull/7041)) ## 0.4.6 ### Enhancements - Accept `--build-constraint` in `uv build` ([#7085](https://github.com/astral-sh/uv/pull/7085)) - Add `--require-hashes` and `--verify-hashes` to `uv build` ([#7094](https://github.com/astral-sh/uv/pull/7094)) - Add `--show-version-specifiers` to `uv tool list` ([#7050](https://github.com/astral-sh/uv/pull/7050)) - Respect hashes in constraints files ([#7093](https://github.com/astral-sh/uv/pull/7093)) - Upgrade installer scripts ([#7092](https://github.com/astral-sh/uv/pull/7092)) - Allow specifying multiple packages in `uv tool upgrade` and `uninstall` ([#7037](https://github.com/astral-sh/uv/pull/7037)) - Sort by implementation in `uv python list` ([#6918](https://github.com/astral-sh/uv/pull/6918)) ### Bug fixes - Invalidate lockfile when member versions change ([#7102](https://github.com/astral-sh/uv/pull/7102)) - Strip fragments from direct source URLs in lockfile ([#7061](https://github.com/astral-sh/uv/pull/7061)) - Support `--no-build` and `--no-binary` in `uv sync` et al ([#7100](https://github.com/astral-sh/uv/pull/7100)) - Use distribution hash over registry hash ([#7060](https://github.com/astral-sh/uv/pull/7060)) - Fix inverted log message ([#7063](https://github.com/astral-sh/uv/pull/7063)) - Adjust Docker `ENTRYPOINT` and `CMD` for inherited images ([#7054](https://github.com/astral-sh/uv/pull/7054)) ### Documentation - Add winget to installers ([#7088](https://github.com/astral-sh/uv/pull/7088)) - Document how to disable path modifications during install ([#7090](https://github.com/astral-sh/uv/pull/7090)) - Document how to manually update locked package version ([#7083](https://github.com/astral-sh/uv/pull/7083)) - Document official `setup-uv` action ([#7056](https://github.com/astral-sh/uv/pull/7056)) - Update docs on `.python-version` file ([#7051](https://github.com/astral-sh/uv/pull/7051)) ## 0.4.7 ### Enhancements - Add `--no-emit-project` and friends to `uv export` ([#7110](https://github.com/astral-sh/uv/pull/7110)) - Add `--output-file` to `uv export` ([#7109](https://github.com/astral-sh/uv/pull/7109)) - Prune unused source distributions from the cache in `uv cache prune` ([#7112](https://github.com/astral-sh/uv/pull/7112)) - Take intersection of constraint and requirements hashes ([#7108](https://github.com/astral-sh/uv/pull/7108)) ### Performance - Skip metadata fetch for `--no-deps` and `pip sync` ([#7127](https://github.com/astral-sh/uv/pull/7127)) ### Bug fixes - Avoid panicking when encountering an invalid Python version during `uv python list` ([#7131](https://github.com/astral-sh/uv/pull/7131)) - Write trailing newline to `.python-version` files ([#7140](https://github.com/astral-sh/uv/pull/7140)) ## 0.4.8 ### Enhancements - Add support for dynamic cache keys ([#7136](https://github.com/astral-sh/uv/pull/7136)) - Allow `.dist-info` names with dashes for post releases ([#7208](https://github.com/astral-sh/uv/pull/7208)) - Use type hints in code from `uv init` ([#7225](https://github.com/astral-sh/uv/pull/7225)) - Treat `.tgz` the same as `.tar.gz` ([#7201](https://github.com/astral-sh/uv/pull/7201)) - Direct users towards `uv venv` to create a virtual environment ([#7188](https://github.com/astral-sh/uv/pull/7188)) - Improve error message for uv init already init-ed ([#7198](https://github.com/astral-sh/uv/pull/7198)) ### Performance - Avoid batch prefetching for un-optimized registries ([#7226](https://github.com/astral-sh/uv/pull/7226)) - Avoid iteration for singleton selections ([#7195](https://github.com/astral-sh/uv/pull/7195)) ### Bug fixes - Avoid extra newlines in debug logging for source builds ([#7174](https://github.com/astral-sh/uv/pull/7174)) - Prune unreachable packages from `--universal` output ([#7209](https://github.com/astral-sh/uv/pull/7209)) - Respect exclusion when collecting workspace members ([#7175](https://github.com/astral-sh/uv/pull/7175)) - Use path file instead of `sitecustomize.py` ([#7161](https://github.com/astral-sh/uv/pull/7161)) - Replace incorrect `--source` and `--binary` flags with correct `--sdist` and `--wheel` flags in `uv build` ([#7156](https://github.com/astral-sh/uv/pull/7156)) ### Documentation - Document support for `UV_INSTALL_DIR` ([#7107](https://github.com/astral-sh/uv/pull/7107)) - List all supported sdist formats ([#7168](https://github.com/astral-sh/uv/pull/7168)) ## 0.4.9 ### Enhancements - Add support for managed Python 3.13 ([#7263](https://github.com/astral-sh/uv/pull/7263)) - Upgrade managed CPython versions to latest patch releases ([#7263](https://github.com/astral-sh/uv/pull/7263)) - Allow setting a target version for `uv self update` ([#7252](https://github.com/astral-sh/uv/pull/7252)) - Create `py.typed` files during `uv init --lib` ([#7232](https://github.com/astral-sh/uv/pull/7232)) - Add a dedicated error for packages that fail due to `distutils` deprecation ([#7239](https://github.com/astral-sh/uv/pull/7239)) - Improve error message when requested Python version is unsupported ([#7269](https://github.com/astral-sh/uv/pull/7269)) - Add `uv run --no-sync` ([#7192](<(https://github.com/astral-sh/uv/pull/7192)>) ### Bug fixes - Avoid updating `pyproject.toml` offsets on non-add edits ([#7262](https://github.com/astral-sh/uv/pull/7262)) - Invalidate cache when `--config-settings` change ([#7139](https://github.com/astral-sh/uv/pull/7139)) - Remove workspace root for single-member workspace with `uv export` ([#7254](https://github.com/astral-sh/uv/pull/7254)) ## 0.4.10 ### Enhancements - Allow `uv tool upgrade --all` to continue on individual upgrade failure ([#7333](https://github.com/astral-sh/uv/pull/7333)) - Support globs as cache keys in `tool.uv.cache-keys` ([#7268](https://github.com/astral-sh/uv/pull/7268)) - Add Python package (`__main__.py`) support to `uv run` ([#7281](https://github.com/astral-sh/uv/pull/7281)) - Add zip application support to `uv run` ([#7289](https://github.com/astral-sh/uv/pull/7289)) - Add `--token` option to `self update` command ([#7279](https://github.com/astral-sh/uv/pull/7279)) ### Performance - Use `globwalk` for `cache-keys` matching ([#7337](https://github.com/astral-sh/uv/pull/7337)) ### Bug fixes - Always treat archive-like requirements as local files ([#7364](https://github.com/astral-sh/uv/pull/7364)) - Apply `--no-install` options when constructing resolution ([#7277](https://github.com/astral-sh/uv/pull/7277)) - Avoid clobbering existing `py.typed` files contents in `uv init` ([#7338](https://github.com/astral-sh/uv/pull/7338)) - Avoid enforcing platform compatibility when validating lockfile ([#7305](https://github.com/astral-sh/uv/pull/7305)) - Avoid installing transitive dev dependencies ([#7318](https://github.com/astral-sh/uv/pull/7318)) - Avoid selecting prerelease Python installations without opt-in ([#7300](https://github.com/astral-sh/uv/pull/7300)) - Fix PPC64 page size in binary builds. ([#7298](https://github.com/astral-sh/uv/pull/7298)) - Include pre-release Python versions in `uv python list` ([#7290](https://github.com/astral-sh/uv/pull/7290)) - Make version ID optional for source builds ([#7362](https://github.com/astral-sh/uv/pull/7362)) - Support relative paths in `uv add --script` ([#7301](https://github.com/astral-sh/uv/pull/7301)) ### Documentation - Fix documentation typos for `uv build --build-constraint` flag ([#7330](https://github.com/astral-sh/uv/pull/7330)) - Fix grammatical error in CLI docs ([#7353](https://github.com/astral-sh/uv/pull/7353)) ### Error messages - Add dedicated lock errors for wheel-only distributions ([#7307](https://github.com/astral-sh/uv/pull/7307)) - Avoid treating `.whl` sources as source distributions ([#7303](https://github.com/astral-sh/uv/pull/7303)) - Clarify Python requirement source for script incompatibilities ([#7339](https://github.com/astral-sh/uv/pull/7339)) ## 0.4.11 ### Enhancements - Add `--no-editable` support to `uv sync` and `uv export` ([#7371](https://github.com/astral-sh/uv/pull/7371)) - Add support for `--only-dev` to `uv sync` and `uv export` ([#7367](https://github.com/astral-sh/uv/pull/7367)) - Add support for remaining pip-supported file extensions ([#7387](https://github.com/astral-sh/uv/pull/7387)) - Generate shell completion for `uvx` ([#7388](https://github.com/astral-sh/uv/pull/7388)) - Include `uv export` command in `requirements.txt` output ([#7374](https://github.com/astral-sh/uv/pull/7374)) - Prune unzipped source distributions in `uv cache prune --ci` ([#7446](https://github.com/astral-sh/uv/pull/7446)) - Warn when trying to `uv sync` a package without build configuration ([#7420](https://github.com/astral-sh/uv/pull/7420)) - Support requests for pre-releases in the `--python` option ([#7335](https://github.com/astral-sh/uv/pull/7335)) ### Bug fixes - Avoid erroneous version warning for `.dist-info` directories ([#7444](https://github.com/astral-sh/uv/pull/7444)) - Avoid removing seed packages for `uv venv --seed` environments ([#7410](https://github.com/astral-sh/uv/pull/7410)) - Avoid unnecessary progress bar initializations ([#7412](https://github.com/astral-sh/uv/pull/7412)) - Error when `tool.uv.sources` contains duplicate package names ([#7383](https://github.com/astral-sh/uv/pull/7383)) - Include `--branch` et al when resolving unnamed URLs in `uv add` ([#7447](https://github.com/astral-sh/uv/pull/7447)) - Include `dev-dependencies` in `--no-sources` invocations ([#7408](https://github.com/astral-sh/uv/pull/7408)) - Include the parent interpreter in Python discovery when `--system` is used ([#7440](https://github.com/astral-sh/uv/pull/7440)) - Respect `--no-sources` in PEP 723 scripts ([#7409](https://github.com/astral-sh/uv/pull/7409)) - Respect `pyproject.toml` credentials from user-provided requirements ([#7474](https://github.com/astral-sh/uv/pull/7474)) - Use consistent PyPI cache bucket ([#7443](https://github.com/astral-sh/uv/pull/7443)) - Use unambiguous relative paths in `uv export` ([#7378](https://github.com/astral-sh/uv/pull/7378)) ### Documentation - Add documentation on platform-specific dependencies ([#7411](https://github.com/astral-sh/uv/pull/7411)) - Add documentation for passing installer options on Linux ([#6839](https://github.com/astral-sh/uv/pull/6839)) - Separate project data from configuration settings ([#7053](https://github.com/astral-sh/uv/pull/7053)) ### Error messages - Hint at missing `project.name` ([#6803](https://github.com/astral-sh/uv/pull/6803)) - Surface dedicated `project.name` error for workspaces ([#7399](https://github.com/astral-sh/uv/pull/7399)) - Remove duplicate warning for settings discovery errors ([#7384](https://github.com/astral-sh/uv/pull/7384)) ## 0.4.12 ### Enhancements - Allow users to provide pre-defined metadata for resolution ([#7442](https://github.com/astral-sh/uv/pull/7442)) - Invalidate existing tool environments on Python interpreter mismatch ([#7451](https://github.com/astral-sh/uv/pull/7451)) ### Bug fixes - Avoid fatal error when searching for egg-info with missing directory ([#7498](https://github.com/astral-sh/uv/pull/7498)) ### Documentation - Add note on cache growth for self-hosted GitHub runners ([#5757](https://github.com/astral-sh/uv/pull/5757)) ## 0.4.13 ### Enhancements - Add `socks` support ([#7503](https://github.com/astral-sh/uv/pull/7503)) - Avoid warning about bad Python interpreter links for empty project environment directories ([#7527](https://github.com/astral-sh/uv/pull/7527)) - Improve invalid environment warning messages ([#7544](https://github.com/astral-sh/uv/pull/7544)) - Use more verbose spelling of "virtualenv" during creation ([#7523](https://github.com/astral-sh/uv/pull/7523)) - Do not use a user-facing warning for "Waiting to acquire lock..." message ([#7502](https://github.com/astral-sh/uv/pull/7502)) ### Performance - Use a single buffer for hints on resolver errors ([#7497](https://github.com/astral-sh/uv/pull/7497)) ### Bug fixes - Allow Python pre-releases to be used if they are first on the `PATH` ([#7470](https://github.com/astral-sh/uv/pull/7470)) - Avoid deleting the project environment directory if it is not a virtual environment ([#7522](https://github.com/astral-sh/uv/pull/7522)) - Do not error if the `CACHEDIR.TAG` file exists but cannot be written to ([#7550](https://github.com/astral-sh/uv/pull/7550)) - Treat invalid platform as more compatible than invalid Python ([#7556](https://github.com/astral-sh/uv/pull/7556)) - Use portable paths when serializing sources ([#7504](https://github.com/astral-sh/uv/pull/7504)) - Compute resolver hints using the final reduced derivation tree ([#7546](https://github.com/astral-sh/uv/pull/7546)) - Bump the wheel and sdist cache versions ([#7560](https://github.com/astral-sh/uv/pull/7560)) - Heal cache entries with missing source distributions ([#7559](https://github.com/astral-sh/uv/pull/7559)) ### Rust libraries - Bump minimum supported Rust version from 1.80 -> 1.81 ### Documentation - Add `UV_LINK_MODE` to Docker caching example ([#7510](https://github.com/astral-sh/uv/pull/7510)) - Clarify behavior of of overrides in CLI reference ([#7537](https://github.com/astral-sh/uv/pull/7537)) ## 0.4.14 ### Breaking - Move uvx shell completion to `uvx --generate-shell-completion` ([#7511](https://github.com/astral-sh/uv/pull/7511)) ### Enhancements - Adjust messaging for frozen hint on resolution failure during `uv add` ([#7597](https://github.com/astral-sh/uv/pull/7597)) - Provide resolution hints in case of possible local name conflicts ([#7505](https://github.com/astral-sh/uv/pull/7505)) - Improve Docker image release tagging order and display on `ghcr.io` ([#7568](https://github.com/astral-sh/uv/pull/7568)) - Improve deserialization error messages ([#7598](https://github.com/astral-sh/uv/pull/7598)) ### Bug fixes - Allow system environments during project environment validity check ([#7585](https://github.com/astral-sh/uv/pull/7585)) - Avoid validating workspace members when `--no-sources` is provided ([#7599](https://github.com/astral-sh/uv/pull/7599)) - Fix handling of `sys.base_prefix` collision in interpreter identity check during tool installs ([#7596](https://github.com/astral-sh/uv/pull/7596)) - Make `uv cache prune` robust to unreadable rkyv entries ([#7561](https://github.com/astral-sh/uv/pull/7561)) - Revert "Remove duplicate warning for settings discovery errors (#7384)" ([#7594](https://github.com/astral-sh/uv/pull/7594)) ### Documentation - Fix `-` to `_` in packaged applications document ([#7571](https://github.com/astral-sh/uv/pull/7571)) ## 0.4.15 ### Bug fixes - Revert "Treat invalid platform as more compatible than invalid Python (#7556)" ([#7608](https://github.com/astral-sh/uv/pull/7608)) ### Documentation - Add the execution policy to powershell installs for single versions ([#7602](https://github.com/astral-sh/uv/pull/7602)) ## 0.4.16 ### Enhancements - Add `uv publish` ([#7475](https://github.com/astral-sh/uv/pull/7475)) - Add a `--project` argument to run a command from a project directory ([#7603](https://github.com/astral-sh/uv/pull/7603)) - Display Python implementation when creating environments ([#7652](https://github.com/astral-sh/uv/pull/7652)) - Implement trusted publishing for `uv publish` ([#7548](https://github.com/astral-sh/uv/pull/7548)) - Respect lockfile preferences for `--with` requirements ([#7627](https://github.com/astral-sh/uv/pull/7627)) - Unhide the `--directory` option ([#7653](https://github.com/astral-sh/uv/pull/7653)) - Allow requesting free-threaded Python interpreters ([#7431](https://github.com/astral-sh/uv/pull/7431)) - Show a dedicated PubGrub hint for `--unsafe-best-match` ([#7645](https://github.com/astral-sh/uv/pull/7645)) - Add resolver error checking for conflicting distributions ([#7595](https://github.com/astral-sh/uv/pull/7595)) ### Bug fixes - Avoid adding double-newlines for CRLF ([#7640](https://github.com/astral-sh/uv/pull/7640)) - Avoid retaining forks when `requires-python` range changes ([#7624](https://github.com/astral-sh/uv/pull/7624)) - Determine if pre-release Python downloads should be allowed using the version specifiers ([#7638](https://github.com/astral-sh/uv/pull/7638)) - Fix `link-mode=clone` for directories on Linux ([#7620](https://github.com/astral-sh/uv/pull/7620)) - Improve Python executable name discovery when using alternative implementations ([#7649](https://github.com/astral-sh/uv/pull/7649)) - Require opt-in to use alternative Python implementations ([#7650](https://github.com/astral-sh/uv/pull/7650)) - Use the first pre-release discovered when only pre-release Python versions are available ([#7666](https://github.com/astral-sh/uv/pull/7666)) ### Documentation - Document environment variable that disables printing of virtual environment name in prompt ([#7648](https://github.com/astral-sh/uv/pull/7648)) - Remove double whitespaces from the code ([#7623](https://github.com/astral-sh/uv/pull/7623)) - Use anchorlinks rather than permalinks ([#7626](https://github.com/astral-sh/uv/pull/7626)) ### Preview features - Add build backend scaffolding ([#7662](https://github.com/astral-sh/uv/pull/7662)) ## 0.4.17 ### Enhancements - Add `uv build --all` to build all packages in a workspace ([#7724](https://github.com/astral-sh/uv/pull/7724)) - Add support for `uv init --script` ([#7565](https://github.com/astral-sh/uv/pull/7565)) - Add support for upgrading build environment for installed tools (`uv tool upgrade --python`) ([#7605](https://github.com/astral-sh/uv/pull/7605)) - Initialize a Git repository in `uv init` ([#5476](https://github.com/astral-sh/uv/pull/5476)) - Respect `--quiet` flag in `uv build` ([#7674](https://github.com/astral-sh/uv/pull/7674)) - Add context message before listing available tools in `uvx` ([#7641](https://github.com/astral-sh/uv/pull/7641)) ### Bug fixes - Don't create Python bytecode files during interpreter discovery ([#7707](https://github.com/astral-sh/uv/pull/7707)) - Escape glob patterns in workspace member discovery ([#7709](https://github.com/astral-sh/uv/pull/7709)) - Avoid prefetching source distributions with unbounded lower-bound ranges ([#7683](https://github.com/astral-sh/uv/pull/7683)) ### Documentation - Add `uv build` and `uv publish` to features overview ([#7716](https://github.com/astral-sh/uv/pull/7716)) - Add documentation on cache versioning ([#7693](https://github.com/astral-sh/uv/pull/7693)) - Spell out the names of the Docker images for easier copy-paste ([#7706](https://github.com/astral-sh/uv/pull/7706)) - Document uv-with-Jupyter workflows ([#7625](https://github.com/astral-sh/uv/pull/7625)) - Note that `uv lock --upgrade-package` retains locked versions ([#7694](https://github.com/astral-sh/uv/pull/7694)) ## 0.4.18 ### Enhancements - Allow multiple source entries for each package in `tool.uv.sources` ([#7745](https://github.com/astral-sh/uv/pull/7745)) - Add `.gitignore` file to `uv build` output directory ([#7835](https://github.com/astral-sh/uv/pull/7835)) - Disable jemalloc on FreeBSD ([#7780](https://github.com/astral-sh/uv/pull/7780)) - Respect `PAGER` env var when paging in `uv help` command ([#5511](https://github.com/astral-sh/uv/pull/5511)) - Support `uv run -m foo` to run a module ([#7754](https://github.com/astral-sh/uv/pull/7754)) - Use a top-level output directory for `uv build` in workspaces ([#7813](https://github.com/astral-sh/uv/pull/7813)) - Update `uv init --package` command to match project name ([#7670](https://github.com/astral-sh/uv/pull/7670)) - Add a custom suggestion for `uv add dotenv` ([#7799](https://github.com/astral-sh/uv/pull/7799)) - Add detailed errors for `tool.uv.sources` deserialization failures ([#7823](https://github.com/astral-sh/uv/pull/7823)) - Improve error message copy for failed builds ([#7849](https://github.com/astral-sh/uv/pull/7849)) - Use `serde-untagged` to improve some untagged enum error messages ([#7822](https://github.com/astral-sh/uv/pull/7822)) - Use build failure hints for `dotenv` errors, rather than in `uv add` ([#7825](https://github.com/astral-sh/uv/pull/7825)) ### Configuration - Add `UV_NO_SYNC` environment variable ([#7752](https://github.com/astral-sh/uv/pull/7752)) ### Bug fixes - Accept `git+` prefix in `tool.uv.sources` ([#7847](https://github.com/astral-sh/uv/pull/7847)) - Allow spaces in path requirements ([#7767](https://github.com/astral-sh/uv/pull/7767)) - Avoid reusing cached downloaded binaries with `--no-binary` ([#7772](https://github.com/astral-sh/uv/pull/7772)) - Correctly trims values during wheel WHEEL file parsing ([#7770](https://github.com/astral-sh/uv/pull/7770)) - Fix `uv tree --invert` for platform dependencies ([#7808](https://github.com/astral-sh/uv/pull/7808)) - Fix encoding mismatch between python child process and uv ([#7757](https://github.com/astral-sh/uv/pull/7757)) - Reject self-dependencies in `uv add` ([#7766](https://github.com/astral-sh/uv/pull/7766)) - Respect `tool.uv.environments` for legacy virtual workspace roots ([#7824](https://github.com/astral-sh/uv/pull/7824)) - Retain empty extras on workspace members ([#7762](https://github.com/astral-sh/uv/pull/7762)) - Use file stem when parsing cached wheel names ([#7773](https://github.com/astral-sh/uv/pull/7773)) ### Rust API - Make `FlatDistributions` public ([#7833](https://github.com/astral-sh/uv/pull/7833)) ### Documentation - Fix table of contents sizing ([#7751](https://github.com/astral-sh/uv/pull/7751)) - GitLab Integration documentation ([#6857](https://github.com/astral-sh/uv/pull/6857)) - Update documentation to setup-uv@v3 ([#7807](https://github.com/astral-sh/uv/pull/7807)) - Use `uv publish` instead of twine in docs ([#7837](https://github.com/astral-sh/uv/pull/7837)) - Fix typo in `projects.md` ([#7784](https://github.com/astral-sh/uv/pull/7784)) ## 0.4.19 ### Enhancements - Add managed downloads for CPython 3.13.0rc3 and 3.12.7 ([#7880](https://github.com/astral-sh/uv/pull/7880)) - Display the target virtual environment path if non-default ([#7850](https://github.com/astral-sh/uv/pull/7850)) - Preserve case-insensitive sorts in `uv add` ([#7864](https://github.com/astral-sh/uv/pull/7864)) - Respect project upper bounds when filtering wheels on `requires-python` ([#7904](https://github.com/astral-sh/uv/pull/7904)) - Add `--script` to `uv run` to treat an input as PEP 723 regardless of extension ([#7739](https://github.com/astral-sh/uv/pull/7739)) - Improve legibility of build failure errors ([#7854](https://github.com/astral-sh/uv/pull/7854)) - Show interpreter source during Python discovery query errors ([#7928](https://github.com/astral-sh/uv/pull/7928)) ### Configuration - Add `UV_FIND_LINKS` environment variable for `--find-links` ([#7912](https://github.com/astral-sh/uv/pull/7912)) - Ignore empty string values for `UV_PYTHON` environment variable ([#7878](https://github.com/astral-sh/uv/pull/7878)) ### Bug fixes - Allow `py3x-none` tags in newer than Python 3.x ([#7867](https://github.com/astral-sh/uv/pull/7867)) - Allow self-dependencies in the `dev` section ([#7943](https://github.com/astral-sh/uv/pull/7943)) - Always ignore `cp2` wheels in resolution ([#7902](https://github.com/astral-sh/uv/pull/7902)) - Clear the publish progress bar on retry ([#7921](https://github.com/astral-sh/uv/pull/7921)) - Fix parsing of `gnueabi` libc variants in Python version requests ([#7975](https://github.com/astral-sh/uv/pull/7975)) - Simplify supported environments when comparing to lockfile ([#7894](https://github.com/astral-sh/uv/pull/7894)) - Trim commits when reading from Git refs ([#7922](https://github.com/astral-sh/uv/pull/7922)) - Use a higher HTTP read timeout when publishing packages ([#7923](https://github.com/astral-sh/uv/pull/7923)) - Remove the first empty line for `uv tree --package foo` ([#7885](https://github.com/astral-sh/uv/pull/7885)) ### Documentation - Add 3.13 support to the platform reference ([#7971](https://github.com/astral-sh/uv/pull/7971)) - Clarify project environment creation ([#7941](https://github.com/astral-sh/uv/pull/7941)) - Fix code block title in Gitlab integration docs ([#7861](https://github.com/astral-sh/uv/pull/7861)) - Fix project guide section on adding a Git dependency ([#7916](https://github.com/astral-sh/uv/pull/7916)) - Fix uninstallation command for Windows ([#7944](https://github.com/astral-sh/uv/pull/7944)) - Clearly specify the minimum supported Windows Server version ([#7946](https://github.com/astral-sh/uv/pull/7946)) ### Rust API - Remove unused `Sha256Reader` ([#7929](https://github.com/astral-sh/uv/pull/7929)) - Remove unnecessary `Deserialize` derives on settings ([#7856](https://github.com/astral-sh/uv/pull/7856)) ## 0.4.20 ### Enhancements - Add managed downloads for CPython 3.13.0 (final) ([#8010](https://github.com/astral-sh/uv/pull/8010)) - Python 3.13 is the default version for `uv python install` ([#8010](https://github.com/astral-sh/uv/pull/8010)) - Hint at wrong endpoint in `uv publish` failures ([#7872](https://github.com/astral-sh/uv/pull/7872)) - List available scripts when a command is not specified for `uv run` ([#7687](https://github.com/astral-sh/uv/pull/7687)) - Fill in `authors` field during `uv init` ([#7756](https://github.com/astral-sh/uv/pull/7756)) ### Documentation - Add snapshot testing to contribution guide ([#7882](https://github.com/astral-sh/uv/pull/7882)) - Fix and improve GitLab integration docs ([#8000](https://github.com/astral-sh/uv/pull/8000)) ## 0.4.21 ### Enhancements - Add support for managed installations of free-threaded Python ([#8100](https://github.com/astral-sh/uv/pull/8100)) - Add note about `uvx` to `uv tool run` short help ([#7695](https://github.com/astral-sh/uv/pull/7695)) - Enable HTTP/2 requests ([#8049](https://github.com/astral-sh/uv/pull/8049)) - Support `uv tree --no-dev` ([#8109](https://github.com/astral-sh/uv/pull/8109)) - Support PEP 723 metadata with `uv run -` ([#8111](https://github.com/astral-sh/uv/pull/8111)) - Support `pip install --exact` ([#8044](https://github.com/astral-sh/uv/pull/8044)) - Support `uv export --no-header` ([#8096](https://github.com/astral-sh/uv/pull/8096)) - Add Python 3.13 images to Docker publish ([#8105](https://github.com/astral-sh/uv/pull/8105)) - Support remote (`https://`) scripts in `uv run` ([#6375](https://github.com/astral-sh/uv/pull/6375)) - Allow comma value-delimited arguments in `uv run --with` ([#7909](https://github.com/astral-sh/uv/pull/7909)) ### Configuration - Support wildcards in `UV_INSECURE_HOST` ([#8052](https://github.com/astral-sh/uv/pull/8052)) ### Performance - Use shared index when fetching metadata in lock satisfaction routine ([#8147](https://github.com/astral-sh/uv/pull/8147)) ### Bug fixes - Add prerelease compatibility check to `uv python` CLI ([#8020](https://github.com/astral-sh/uv/pull/8020)) - Avoid deleting a project environment directory if we cannot tell if a `pyvenv.cfg` file exists ([#8012](https://github.com/astral-sh/uv/pull/8012)) - Avoid excluding valid wheels for exact `requires-python` bounds ([#8140](https://github.com/astral-sh/uv/pull/8140)) - Bump `netrc` crate to latest commit ([#8021](https://github.com/astral-sh/uv/pull/8021)) - Fix `uv python pin 3.13t` failure when parsing version for project requires check ([#8056](https://github.com/astral-sh/uv/pull/8056)) - Fix handling of != intersections in `requires-python` ([#7897](https://github.com/astral-sh/uv/pull/7897)) - Remove the newly created tool environment if sync failed ([#8038](https://github.com/astral-sh/uv/pull/8038)) - Respect dynamic extras in `uv lock` and `uv sync` ([#8091](https://github.com/astral-sh/uv/pull/8091)) - Treat resolver failures as fatal in lockfile validation ([#8083](https://github.com/astral-sh/uv/pull/8083)) - Use `git config --get` for author information for improved backwards compatibility ([#8101](https://github.com/astral-sh/uv/pull/8101)) - Use comma-separated values for `UV_FIND_LINKS` ([#8061](https://github.com/astral-sh/uv/pull/8061)) - Use shared resolver state between add and lock to avoid double Git update ([#8146](https://github.com/astral-sh/uv/pull/8146)) - Make `--relocatable` entrypoints robust to symlinking ([#8079](https://github.com/astral-sh/uv/pull/8079)) - Improve compatibility with VSCode PS1 prompt ([#8006](https://github.com/astral-sh/uv/pull/8006)) - Fix "Stream did not contain valid UTF-8" failures in Windows ([#8120](https://github.com/astral-sh/uv/pull/8120)) - Use `--with-requirements` in `uvx` error hint ([#8112](https://github.com/astral-sh/uv/pull/8112)) ### Documentation - Include `uvx` installation in Docker examples ([#8179](https://github.com/astral-sh/uv/pull/8179)) - Make the instructions for the Windows standalone installer consistent across README and documentation ([#8125](https://github.com/astral-sh/uv/pull/8125)) - Update pip compatibility guide to note transitive URL dependency support ([#8081](https://github.com/astral-sh/uv/pull/8081)) - Document `--reinstall` with `--exclude-newer` to ensure downgrades ([#6721](https://github.com/astral-sh/uv/pull/6721)) ## 0.4.22 ### Enhancements - Respect `[tool.uv.sources]` in build requirements ([#7172](https://github.com/astral-sh/uv/pull/7172)) ### Preview features - Add a dedicated `uv publish` error message for missing usernames ([#8045](https://github.com/astral-sh/uv/pull/8045)) - Support interactive input in `uv publish` ([#8158](https://github.com/astral-sh/uv/pull/8158)) - Use raw filenames in `uv publish` ([#8204](https://github.com/astral-sh/uv/pull/8204)) ### Performance - Reuse the result of `which git` ([#8224](https://github.com/astral-sh/uv/pull/8224)) ### Bug fixes - Avoid environment check optimization for `uv pip install --exact` ([#8219](https://github.com/astral-sh/uv/pull/8219)) - Do not use free-threaded interpreters without a free-threaded request ([#8191](https://github.com/astral-sh/uv/pull/8191)) - Don't recommend `--prerelease=allow` during build requirement resolution errors ([#8192](https://github.com/astral-sh/uv/pull/8192)) - Prefer optimized builds for free-threaded Python downloads ([#8196](https://github.com/astral-sh/uv/pull/8196)) - Retain old `python-build-standalone` releases ([#8216](https://github.com/astral-sh/uv/pull/8216)) - Run `uv build` builds in the source distribution bucket ([#8220](https://github.com/astral-sh/uv/pull/8220)) ## 0.4.23 This release introduces a revamped system for defining package indexes, as an alternative to the existing pip-style `--index-url` and `--extra-index-url` configuration options. You can now define named indexes in your `pyproject.toml` file using the `[[tool.uv.index]]` table: ```toml [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cpu" ``` Packages can be pinned to a specific index via `tool.uv.sources`, to ensure that a given package is installed from the correct index. For example, to ensure that `torch` is _always_ installed from the `pytorch` index: ```toml [tool.uv.sources] torch = { index = "pytorch" } [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cpu" ``` Indexes can also be marked as `explicit = true` to prevent packages from being installed from that index unless explicitly pinned. For example, to ensure that `torch` is installed from the `pytorch` index, but all other packages are installed from the default index: ```toml [tool.uv.sources] torch = { index = "pytorch" } [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cpu" explicit = true ``` To define an additional index outside a `pyproject.toml` file, use the `--index` command-line argument (or the `UV_INDEX` environment variable); to replace the default index (PyPI), use the `--default-index` command-line argument (or `UV_DEFAULT_INDEX`). These changes are entirely backwards-compatible with the deprecated `--index-url` and `--extra-index-url` options, which continue to work as before. See the [Index](https://docs.astral.sh/uv/concepts/indexes/) documentation for more. ### Enhancements - Add index URLs when provided via `uv add --index` or `--default-index` ([#7746](https://github.com/astral-sh/uv/pull/7746)) - Add support for named and explicit indexes ([#7481](https://github.com/astral-sh/uv/pull/7481)) - Add templates for popular build backends ([#7857](https://github.com/astral-sh/uv/pull/7857)) - Allow multiple pinned indexes in `tool.uv.sources` ([#7769](https://github.com/astral-sh/uv/pull/7769)) - Allow users to incorporate Git tags into dynamic cache keys ([#8259](https://github.com/astral-sh/uv/pull/8259)) - Pin named indexes in `uv add` ([#7747](https://github.com/astral-sh/uv/pull/7747)) - Respect named `--index` and `--default-index` values in `tool.uv.sources` ([#7910](https://github.com/astral-sh/uv/pull/7910)) - Update to latest PubGrub version ([#8245](https://github.com/astral-sh/uv/pull/8245)) - Enable environment variable authentication for named indexes ([#7741](https://github.com/astral-sh/uv/pull/7741)) - Avoid showing lower-bound warning outside of explicit lock and sync ([#8234](https://github.com/astral-sh/uv/pull/8234)) - Improve logging during lock errors ([#8258](https://github.com/astral-sh/uv/pull/8258)) - Improve styling of `requires-python` warnings ([#8240](https://github.com/astral-sh/uv/pull/8240)) - Show hint in resolution failure on `Forbidden` (`403`) or `Unauthorized` (`401`) ([#8264](https://github.com/astral-sh/uv/pull/8264)) - Update to latest `cargo-dist` version (includes new installer features) ([#8270](https://github.com/astral-sh/uv/pull/8270)) - Warn when patch version in `requires-python` is implicitly `0` ([#7959](https://github.com/astral-sh/uv/pull/7959)) - Add more context on client errors during range requests ([#8285](https://github.com/astral-sh/uv/pull/8285)) ### Bug fixes - Avoid writing duplicate index URLs with `--emit-index-url` ([#8226](https://github.com/astral-sh/uv/pull/8226)) - Fix error leading to out-of-bound panic in `uv-pep508` ([#8282](https://github.com/astral-sh/uv/pull/8282)) - Fix managed distributions of free-threaded Python on Windows ([#8268](https://github.com/astral-sh/uv/pull/8268)) - Fix selection of free-threaded interpreters during default Python discovery ([#8239](https://github.com/astral-sh/uv/pull/8239)) - Ignore sources in build requirements for non-source trees ([#8235](https://github.com/astral-sh/uv/pull/8235)) - Invalid cache when adding lower bound to lockfile ([#8230](https://github.com/astral-sh/uv/pull/8230)) - Respect index priority when storing credentials ([#8256](https://github.com/astral-sh/uv/pull/8256)) - Respect relative paths in `uv build` sources ([#8237](https://github.com/astral-sh/uv/pull/8237)) - Narrow what the pip3. logic drops from entry points. ([#8273](https://github.com/astral-sh/uv/pull/8273)) ### Documentation - Add some additional notes to `--index-url` docs ([#8267](https://github.com/astral-sh/uv/pull/8267)) - Add upgrade note to README ([#7937](https://github.com/astral-sh/uv/pull/7937)) - Remove note that "only a single source may be defined for each dependency" ([#8243](https://github.com/astral-sh/uv/pull/8243)) ## 0.4.24 ### Bug fixes - Fix Python executable name in Windows free-threaded Python distributions ([#8310](https://github.com/astral-sh/uv/pull/8310)) - Redact index credentials from lockfile sources ([#8307](https://github.com/astral-sh/uv/pull/8307)) - Respect `UV_INDEX_` rather than `UV_HTTP_BASIC_` as documented ([#8306](https://github.com/astral-sh/uv/pull/8306)) - Improve sources deserialization errors ([#8308](https://github.com/astral-sh/uv/pull/8308)) ### Documentation - Correct pytorch-to-torch reference in docs ([#8291](https://github.com/astral-sh/uv/pull/8291)) ## 0.4.25 ### Enhancements - Add support for `uv pip show --files` ([#8369](https://github.com/astral-sh/uv/pull/8369)) - Don't prefetch unreachable packages ([#8246](https://github.com/astral-sh/uv/pull/8246)) - Remove `tool.uv.sources` table if it is empty ([#8365](https://github.com/astral-sh/uv/pull/8365)) - Modify cache versioning to support backwards compatibility ([#8386](https://github.com/astral-sh/uv/pull/8386)) ### Configuration - Add support for `UV_FROZEN` and `UV_LOCKED` ([#8340](https://github.com/astral-sh/uv/pull/8340)) ### Bug fixes - Allow dashes and underscores in custom index names ([#8339](https://github.com/astral-sh/uv/pull/8339)) - Avoid panic when Git dependencies are included in fork markers ([#8388](https://github.com/astral-sh/uv/pull/8388)) - Check existing source by normalized name before `uv add` and `uv remove` ([#8359](https://github.com/astral-sh/uv/pull/8359)) - Fix bug where username from authentication cache could be ignored ([#8345](https://github.com/astral-sh/uv/pull/8345)) - Fix to respect comments positioning in pyproject.toml on change ([#8384](https://github.com/astral-sh/uv/pull/8384)) - Redact index sources in `uv.lock` ([#8333](https://github.com/astral-sh/uv/pull/8333)) - Use correct indentation when project table contains open bracket comment ([#8387](https://github.com/astral-sh/uv/pull/8387)) - Only remove a source from `[tool.uv.sources]` if it is no long being referenced ([#8366](https://github.com/astral-sh/uv/pull/8366)) - Modify `uv pip list` and `uv tree` to print to stdout regardless of `--quiet` flag ([#8392](https://github.com/astral-sh/uv/pull/8392)) ### Error messages - Improve help message for missing `self update` invocations ([#8337](https://github.com/astral-sh/uv/pull/8337)) - Log `.netrc` parsing errors ([#8364](https://github.com/astral-sh/uv/pull/8364)) - Remove trailing newlines in error messages ([#8322](https://github.com/astral-sh/uv/pull/8322)) - Use a dedicated message for incompatible Python versions in wheel ABI tags ([#8363](https://github.com/astral-sh/uv/pull/8363)) - Remove commands available in the top-level from the suggested subcommand error ([#8316](https://github.com/astral-sh/uv/pull/8316)) ### Release - Run release builds for `macos-x86_64` on `macos-14` runners ([#8327](https://github.com/astral-sh/uv/pull/8327)) ## 0.4.26 ### Enhancements - Allow static dependency metadata entries for direct URL requirements ([#7846](https://github.com/astral-sh/uv/pull/7846)) - Use reinstall report formatting for `uv python install --reinstall` ([#8487](https://github.com/astral-sh/uv/pull/8487)) - Add support for system-level `uv.toml` configuration ([#7851](https://github.com/astral-sh/uv/pull/7851)) ### Bug fixes - Apply `requires-python` narrowing with upper bounds ([#8403](https://github.com/astral-sh/uv/pull/8403)) - Avoid rewriting `[[tool.uv.index]]` entries when credentials are provided ([#8502](https://github.com/astral-sh/uv/pull/8502)) - Fix `uv add` comment handling for empty arrays ([#8504](https://github.com/astral-sh/uv/pull/8504)) - Replace dashes with underscores in index credential variables ([#8452](https://github.com/astral-sh/uv/pull/8452)) - Respect `--allow-insecure-host` in `uv publish` ([#8440](https://github.com/astral-sh/uv/pull/8440)) - Allow arbitrary `--package` includes in `uv tree` ([#8507](https://github.com/astral-sh/uv/pull/8507)) - Remove existing Python install after successful download in `uv python install` ([#8485](https://github.com/astral-sh/uv/pull/8485)) ### Documentation - Add docs example for URLs with `[tool.uv.dependency-metadata]` ([#8484](https://github.com/astral-sh/uv/pull/8484)) - Add help page for build failures ([#8286](https://github.com/astral-sh/uv/pull/8286)) - Fix `cache-keys` typo in `tags = true` ([#8422](https://github.com/astral-sh/uv/pull/8422)) - Add documentation examples for manual branch, rev, and tag Git dependencies ([#8497](https://github.com/astral-sh/uv/pull/8497)) ### Error messages - Improve error message for cache info serialization ([#8500](https://github.com/astral-sh/uv/pull/8500)) - Suggest `--from` command when executable is available for `uvx` ([#8473](https://github.com/astral-sh/uv/pull/8473)) - Support `--with-editable` in `uv tool install` ([#8472](https://github.com/astral-sh/uv/pull/8472)) ## 0.4.27 This release includes support for the `[dependency-groups]` table as recently standardized in [PEP 735](https://peps.python.org/pep-0735/). The table allows for declaration of optional dependency groups that are not published as part of the package metadata, unlike `[project.optional-dependencies]`. There are new `--group`, `--only-group`, and `--no-group` options throughout the uv interface. Previously, uv used a single `tool.uv.dev-dependencies` list for declaration of development dependencies. Now, uv supports declaring development dependencies in a standardized format and allows splitting development dependencies into multiple groups. For compatibility, and to simplify usage for people that do not need multiple groups, uv special-cases the group named `dev`. The `dev` group is equivalent to `tool.uv.dev-dependencies`. The contents of `tool.uv.dev-dependencies` will merged into the `dev` group in uv's resolver. The `--dev`, `--only-dev`, and `--no-dev` flags remain as aliases for the corresponding `--group` options. Support for `tool.uv.dev-dependencies` remains in this release, but will display warnings in a future release. uv syncs the `dev` group by default — this matches the existing behavior for `tool.uv.dev-dependencies`. The default groups can be changed with the `tool.uv.default-groups` setting. Thank you to Stephen Rosen who authored PEP 735. ### Enhancements - Support for PEP 735 ([#8272](https://github.com/astral-sh/uv/pull/8272)) - Add support for `--dry-run` mode in `uv lock` ([#7783](https://github.com/astral-sh/uv/pull/7783)) - Don't allow non-string email in authors ([#8520](https://github.com/astral-sh/uv/pull/8520)) - Enforce lockfile schema versions ([#8509](https://github.com/astral-sh/uv/pull/8509)) ### Bug fixes - Always attach URL to network errors ([#8444](https://github.com/astral-sh/uv/pull/8444)) - Fix dangling non-platform dependencies in `uv tree` ([#8532](https://github.com/astral-sh/uv/pull/8532)) - Prefer `lto` over `debug` free-threaded managed Python builds ([#8515](https://github.com/astral-sh/uv/pull/8515)) ### Documentation - Add `tool.uv.sources` to the "Settings" reference ([#8543](https://github.com/astral-sh/uv/pull/8543)) - Add reference to `uv build` and `uv publish` in the landing pages ([#8542](https://github.com/astral-sh/uv/pull/8542)) - Avoid duplicate `[tool.uv]` header in TOML examples ([#8545](https://github.com/astral-sh/uv/pull/8545)) - Document `.netrc` environment variable and path ([#8511](https://github.com/astral-sh/uv/pull/8511)) - Fix `.netrc` typo in authentication docs ([#8521](https://github.com/astral-sh/uv/pull/8521)) - Fix heading level of "Script support" on docs landing page ([#8544](https://github.com/astral-sh/uv/pull/8544)) - Move the installation configuration docs to a separate page ([#8546](https://github.com/astral-sh/uv/pull/8546)) - Update docs for `--publish-url` to avoid duplication. ([#8561](https://github.com/astral-sh/uv/pull/8561)) - Fix typo ([#8554](https://github.com/astral-sh/uv/pull/8554)) - Fix typo in description of `--strict` flag ([#8513](https://github.com/astral-sh/uv/pull/8513)) ## 0.4.28 ### Enhancements - Add support for requesting free-threaded builds via `+freethreaded` ([#8645](https://github.com/astral-sh/uv/pull/8645)) - Improve trusted publishing error messages ([#8633](https://github.com/astral-sh/uv/pull/8633)) - Remove unneeded `return` from Maturin project template ([#8604](https://github.com/astral-sh/uv/pull/8604)) - Skip Python interpreter discovery for `uv export` ([#8638](https://github.com/astral-sh/uv/pull/8638)) - Hint about missing trusted publishing permission ([#8632](https://github.com/astral-sh/uv/pull/8632)) ### Configuration - Add environment variable to disable progress output ([#8600](https://github.com/astral-sh/uv/pull/8600)) ### Bug fixes - Fork when minimum Python version increases ([#8628](https://github.com/astral-sh/uv/pull/8628)) - Ignore empty groups when validating lock ([#8598](https://github.com/astral-sh/uv/pull/8598)) - Remove duplicate word in error message ([#8589](https://github.com/astral-sh/uv/pull/8589)) - Support cyclic dependencies in `uv tree` ([#8564](https://github.com/astral-sh/uv/pull/8564)) - Update `uv init` to imply `--package` when using `--build-backend` ([#8593](https://github.com/astral-sh/uv/pull/8593)) - Restore use of `dev-dependencies` and `requires-dev` for lockfile compatibility ([#8599](https://github.com/astral-sh/uv/pull/8599)) ### Documentation - Clarify `requires-python` requirement for dependencies ([#8619](https://github.com/astral-sh/uv/pull/8619)) - Update CLI documentation for `--cache-dir` ([#8627](https://github.com/astral-sh/uv/pull/8627)) ## 0.4.29 ### Enhancements - Sort errors during display in `uv python install` ([#8684](https://github.com/astral-sh/uv/pull/8684)) - Update resolver to use disjointness checks instead of marker equality ([#8661](https://github.com/astral-sh/uv/pull/8661)) - Add `riscv64` to supported Python platform tags ([#8660](https://github.com/astral-sh/uv/pull/8660)) ### Bug fixes - Fix hard and soft float libc detection for managed Python distributions on ARM ([#8498](https://github.com/astral-sh/uv/pull/8498)) - Handle cycles in `uv pip tree` ([#8689](https://github.com/astral-sh/uv/pull/8689)) - Respect dependency group markers in `uv export` ([#8659](https://github.com/astral-sh/uv/pull/8659)) - Support transitive dependencies in Git workspaces ([#8665](https://github.com/astral-sh/uv/pull/8665)) - Use portable paths for subdirectories in lock URLs ([#8707](https://github.com/astral-sh/uv/pull/8707)) - Update `uv init --virtual` to imply `--no-package` ([#8595](https://github.com/astral-sh/uv/pull/8595)) ### Preview - Install versioned Python executables into the bin directory during `uv python install` (Unix only) ([#8458](https://github.com/astral-sh/uv/pull/8458)) ### Documentation - Clarify relationship between specifiers and `requires-python` range ([#8688](https://github.com/astral-sh/uv/pull/8688)) - Fix broken link in docs ([#8552](https://github.com/astral-sh/uv/pull/8552)) - Fix outdated documentation on `Requires-Python` ([#8679](https://github.com/astral-sh/uv/pull/8679)) - Add Google Artifact Registry index authentication guide ([#8579](https://github.com/astral-sh/uv/pull/8579)) ## 0.4.30 ### Enhancements - Add support for `.env` and custom env files in `uv run` ([#8811](https://github.com/astral-sh/uv/pull/8811)) - Add support for `--all-packages` in `uv run`, `uv sync`, and `uv export` ([#8742](https://github.com/astral-sh/uv/pull/8742), [#8741](https://github.com/astral-sh/uv/pull/8741), [#8739](https://github.com/astral-sh/uv/pull/8739)) - Allow use of `--frozen` with `--all-packages` in `uv sync` and `uv export` ([#8760](https://github.com/astral-sh/uv/pull/8760)) - Show full error chain on tool upgrade failures ([#8753](https://github.com/astral-sh/uv/pull/8753)) - Add `--check-url` to `uv publish` to check for existing distributions during upload ([#8531](https://github.com/astral-sh/uv/pull/8531)) - Suggest using `--check-url` when `--skip-existing` is used ([#8803](https://github.com/astral-sh/uv/pull/8803)) ### Bug fixes - Allow incompatible `requires-python` for source distributions with static metadata ([#8768](https://github.com/astral-sh/uv/pull/8768)) - Allow managed downloads with `--python-preference system` ([#8808](https://github.com/astral-sh/uv/pull/8808)) - Avoid error for `--group` defined in non-root workspace member ([#8734](https://github.com/astral-sh/uv/pull/8734)) - Avoid showing dependency group annotations on workspace members in tree ([#8730](https://github.com/astral-sh/uv/pull/8730)) - Do not error when the Python bin directory is missing on `uv python uninstall` ([#8725](https://github.com/astral-sh/uv/pull/8725)) - Include member groups when locking workspace ([#8736](https://github.com/astral-sh/uv/pull/8736)) - Fix bug where `python_version < '0'` could appear in a final resolution ([#8759](https://github.com/astral-sh/uv/pull/8759)) - Sanitize filenames during zip extraction ([#8732](https://github.com/astral-sh/uv/pull/8732)) - Switch to RFC 9110 compatible format for exclude newer requests ([#8752](https://github.com/astral-sh/uv/pull/8752)) ### Preview features - Add support for installing versioned Python executables on Windows ([#8663](https://github.com/astral-sh/uv/pull/8663)) - Improve interactions with existing Python executables during install ([#8733](https://github.com/astral-sh/uv/pull/8733)) ### Rust API - Extend `BaseClient` to accept extra middleware ([#8807](https://github.com/astral-sh/uv/pull/8807)) - Add `From` for `FlatDistributions` struct ([#8800](https://github.com/astral-sh/uv/pull/8800)) ### Documentation - Fix environment variable name in providing credentials section ([#8740](https://github.com/astral-sh/uv/pull/8740)) - Fix `add httpx` example with real git branch ([#8756](https://github.com/astral-sh/uv/pull/8756)) - Fix indentation in `projects.md` ([#8772](https://github.com/astral-sh/uv/pull/8772)) - Fix link to publish guide in `README` ([#8720](https://github.com/astral-sh/uv/pull/8720)) - Generate environment variables documentation from code ([#8493](https://github.com/astral-sh/uv/pull/8493)) - Improve and fix some documents ([#8749](https://github.com/astral-sh/uv/pull/8749)) - Improve environment variables document ([#8777](https://github.com/astral-sh/uv/pull/8777)) uv-0.9.17+ds1/changelogs/0.5.x.md000066400000000000000000002331751520155276700161560ustar00rootroot00000000000000# Changelog 0.5.x ## 0.5.0 Since the launch of Python version, project, and tool management capabilities back in August, we've seen extraordinary adoption of uv. We've been iterating rapidly: adding new features, fixing bugs, and improving the user experience. Despite moving quickly, stability and compatibility have been a major focus — we've made thirty releases since our last breaking change. Consequently, we've accumulated various changes that improve correctness and user experience, but could break some workflows. This release contains those changes; many have been marked as breaking out of an abundance of caution. We expect most users to be able to upgrade without making changes. ### Breaking - **Use base executable to set virtualenv Python path** ([#8481](https://github.com/astral-sh/uv/pull/8481)) Previously, uv canonicalized the path to the Python executable when setting the Python path in created virtual environments. This behavior had several undesirable effects: it would bypass stabilized version directories (as constructed by Homebrew) and it was not consistent with the Python standard library's behavior. Now, uv uses the `sys._base_executable` path. - **Use XDG (i.e. `~/.local/bin`) instead of the Cargo home directory in the installer** ([#8420](https://github.com/astral-sh/uv/pull/8420)) Previously, uv's installer used `$CARGO_HOME` or `~/.cargo/bin` for its target install directory. It's been a longstanding complaint that uv uses this directory, as there's no relationship to Cargo. Now, uv will be installed into `$XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, or `~/.local/bin` (in that order). Note that `$UV_INSTALL_DIR` can always be used to override the target directory. - **Discover and respect `.python-version` files in parent directories** ([#6370](https://github.com/astral-sh/uv/pull/6370)) Previously, uv only read `.python-version` files from the working directory. Now, uv will check parent directories for `.python-version` files; however uv will not search for `.python-version` files beyond project boundaries. The new behavior is better aligned with that of `pyenv` and Rye. - **Error when disallowed settings are defined in `uv.toml`** ([#8550](https://github.com/astral-sh/uv/pull/8550)) Some settings can only be defined in the `pyproject.toml`. Previously, uv would ignore these settings when present in the `uv.toml`. Now, uv will error to avoid confusion about why the settings are not respected. - **Implement PEP 440-compliant local version semantics** ([#8797](https://github.com/astral-sh/uv/pull/8797)) Previously, uv's implementation of local versions (e.g., `2.0+cpu`) was not compliant with the specification due to the technical complexity of implementing the local version semantics in the PubGrub algorithm. Thanks to the work of @ericmarkmartin, uv now has a spec-compliant implementation. Namely, uv will now allow a request for `torch==2.1.0` to install `torch@2.1.0+cpu` regardless of whether `torch@2.1.0` (without a local tag) actually exists. - **Treat the base Conda environment as a system environment** ([#7691](https://github.com/astral-sh/uv/pull/7691)) Previously, uv would not distinguish between the base and other Conda environments. Now, uv uses `CONDA_DEFAULT_ENV` and the names `base` and `default` to determine if an environment active via `CONDA_PREFIX` is the base environment. If the base environment is active, the `--system` flag must be used to mutate it. - **Do not allow pre-releases when the `!=` operator is used** ([#7974](https://github.com/astral-sh/uv/pull/7974)) Previously, uv would use the presence of a pre-release specifier in a version specifier as an opt-in to allow pre-release versions during resolution. The new behavior does not allow pre-releases when an inequals operator is used, e.g., `!= 2.0a1`. - **Prefer `USERPROFILE` over `FOLDERID_Profile` when selecting a home directory on Windows** ([#8048](https://github.com/astral-sh/uv/pull/8048)) This change is a side-effect of switching from the `directories` crate to `etcetera` for determining canonical system paths. If `USERPROFILE` is not set, the behavior will be unchanged. - **Improve interactions between color environment variables and CLI options** ([#8215](https://github.com/astral-sh/uv/pull/8215)) Previously, uv would respect the `FORCE_COLOR` and `NO_COLOR` environment variables over the `--color` flag. Now, when the `--color` flag is explicitly provided, uv will respect it over the environment variables. - **Make `allow-insecure-host` a global option** ([#8476](https://github.com/astral-sh/uv/pull/8476)) Previously, this option was only available in some parts of uv. Now, `--allow-insecure-host` can be provided to any command. For consistency, the `allow-insecure-host` setting has been removed from the `[tool.uv.pip]` configuration in favor of `[tool.uv]`. - **Only write `.python-version` files during `uv init` for workspace members if the version differs** ([#8897](https://github.com/astral-sh/uv/pull/8897)) Previously, uv would create a `.python-version` file for workspace members during `uv init`. Now, uv will only do so if the version differs from the `.python-version` file in the workspace root since uv will respect `.python-version` files in parent directories. ### Enhancements - Add `uv tree --outdated` ([#8893](https://github.com/astral-sh/uv/pull/8893)) - Add armv8l alias for armv7l to support arm 32-bit compatibility mode ([#8881](https://github.com/astral-sh/uv/pull/8881)) - Add support for `pip list --outdated` ([#8872](https://github.com/astral-sh/uv/pull/8872)) - Allow semicolons directly after direct URLs ([#8836](https://github.com/astral-sh/uv/pull/8836)) - Enable support for arbitrary git transports ([#8769](https://github.com/astral-sh/uv/pull/8769)) - Improve Python discovery source messages ([#8890](https://github.com/astral-sh/uv/pull/8890)) - Show dedicated error for trailing `;` on URL and path requirements ([#8835](https://github.com/astral-sh/uv/pull/8835)) - Add progress bar for `uv cache clean` ([#8857](https://github.com/astral-sh/uv/pull/8857)) - Warn on failure to query system configuration file ([#8829](https://github.com/astral-sh/uv/pull/8829)) ### Preview features - Add support for building basic source distributions with the experimental uv build backend ([#8886](https://github.com/astral-sh/uv/pull/8886)) ### Bug fixes - Respect dynamic version updates in `uv lock` ([#8867](https://github.com/astral-sh/uv/pull/8867)) - Respect fork markers in `--resolution-mode=lowest-direct` ([#8839](https://github.com/astral-sh/uv/pull/8839)) ### Documentation - Add further examples of git+https support ([#8841](https://github.com/astral-sh/uv/pull/8841)) - Add installer variables to environment reference ([#8874](https://github.com/astral-sh/uv/pull/8874)) - Add note on private classifier ([#8783](https://github.com/astral-sh/uv/pull/8783)) - Update pip-and-uv strictness example ([#8822](https://github.com/astral-sh/uv/pull/8822)) - Fix `uv python install` docs to use an existing PyPy version ([#8845](https://github.com/astral-sh/uv/pull/8845)) - Document how to mimic `--verbose` with `RUST_LOG` ([#8858](https://github.com/astral-sh/uv/pull/8858)) ## 0.5.1 ### Enhancements - Allow installation of manylinux wheels on `riscv64` ([#8934](https://github.com/astral-sh/uv/pull/8934)) ### Bug fixes - Build source distributions at top-level of cache ([#8905](https://github.com/astral-sh/uv/pull/8905)) - Allow non-registry dependencies in `uv pip list --outdated` ([#8939](https://github.com/astral-sh/uv/pull/8939)) - Compute superset of existing and required hashes when healing cache ([#8955](https://github.com/astral-sh/uv/pull/8955)) - Enable uv to replace and delete itself on Windows ([#8914](https://github.com/astral-sh/uv/pull/8914)) - Remove source distribution filename from cache ([#8907](https://github.com/astral-sh/uv/pull/8907)) - Respect `--index-url` in `uv pip list` ([#8942](https://github.com/astral-sh/uv/pull/8942)) - Respect comma-separated extras in `--with` ([#8946](https://github.com/astral-sh/uv/pull/8946)) ### Documentation - Add uninstall note for previous versions ([#8937](https://github.com/astral-sh/uv/pull/8937)) - Remove some missed references to `~/.cargo/bin` ([#8936](https://github.com/astral-sh/uv/pull/8936)) - Split README's install code block into 3 ([#8853](https://github.com/astral-sh/uv/pull/8853)) ## 0.5.2 ### Enhancements - Hide `--no-system` from `uv pip tree` CLI ([#9040](https://github.com/astral-sh/uv/pull/9040)) - Allow configuration of Python and PyPy install mirrors in `uv.toml` ([#8695](https://github.com/astral-sh/uv/pull/8695)) - Allow passing Python download mirrors to `uv python install` ([#8695](https://github.com/astral-sh/uv/pull/8695)) - Add support for specifying conflicting extras and dependency groups ([#8976](https://github.com/astral-sh/uv/pull/8976), [#9096](https://github.com/astral-sh/uv/pull/9096)) - Consistent colon usage in build failure errors ([#8994](https://github.com/astral-sh/uv/pull/8994)) - Show full derivation chain when encountering build failures ([#9108](https://github.com/astral-sh/uv/pull/9108)) - Show link we failed on parsing index pages ([#9118](https://github.com/astral-sh/uv/pull/9118)) - Remove duplicate log when searching for interpreters ([#9092](https://github.com/astral-sh/uv/pull/9092)) - Update uv development status classifier to "Stable" on PyPI ([#8943](https://github.com/astral-sh/uv/pull/8943)) - Use rich diagnostic formatting for early build failures ([#9041](https://github.com/astral-sh/uv/pull/9041)) - Use rich diagnostic formatting for install failures ([#9043](https://github.com/astral-sh/uv/pull/9043)) ### Performance - Avoid retraversing filesystem when testing exact glob matches ([#9022](https://github.com/astral-sh/uv/pull/9022)) ### Bug fixes - Allow `--no-build` to validate lock ([#9024](https://github.com/astral-sh/uv/pull/9024)) - Allow default indexes to be marked as explicit ([#8990](https://github.com/astral-sh/uv/pull/8990)) - Avoid creating `.venv` in `uv add --frozen` and `uv add --no-sync` ([#8980](https://github.com/astral-sh/uv/pull/8980)) - Avoid duplicating first-entry comments in `uv add` ([#9109](https://github.com/astral-sh/uv/pull/9109)) - Defer reporting of build failures in resolver ([#9098](https://github.com/astral-sh/uv/pull/9098)) - Fix references to `--resolution-strategy` in error message output ([#8971](https://github.com/astral-sh/uv/pull/8971)) - Ignore virtual environments in parent directories when choosing Python version for new projects ([#9075](https://github.com/astral-sh/uv/pull/9075)) - Forward SIGTERM to child processes in `uv run` ([#8933](https://github.com/astral-sh/uv/pull/8933)) - Prefer Python executable names that match the request over default names ([#9066](https://github.com/astral-sh/uv/pull/9066)) - Prefer compatible to incompatible distributions when packages exist on multiple indexes ([#8961](https://github.com/astral-sh/uv/pull/8961)) - Publish: Ignore non-matching files ([#8986](https://github.com/astral-sh/uv/pull/8986)) - Revert `uv.lock` changes when `uv add` fails ([#9030](https://github.com/astral-sh/uv/pull/9030)) - Show file extensions on available commands when not `.exe` ([#9099](https://github.com/astral-sh/uv/pull/9099)) - Sort by name, then specifiers in `uv add` ([#9097](https://github.com/astral-sh/uv/pull/9097)) - Split after specifiers in `--with` requirements ([#9089](https://github.com/astral-sh/uv/pull/9089)) - Support multiple extras in universal pip compile output ([#8960](https://github.com/astral-sh/uv/pull/8960)) ### Preview features - Build backend: Add tests for source tree -> source dist -> wheel conversions ([#9091](https://github.com/astral-sh/uv/pull/9091)) - Build backend: Switch to custom `glob-walkdir` implementation ([#9013](https://github.com/astral-sh/uv/pull/9013)) - Build backend: Add minimal wheel settings ([#9085](https://github.com/astral-sh/uv/pull/9085)) ### Documentation - Add wget instructions for systems without curl ([#8630](https://github.com/astral-sh/uv/pull/8630)) - Fix `.env` file example in docs ([#9064](https://github.com/astral-sh/uv/pull/9064)) - Fix reference to `--resolution` in docs ([#8968](https://github.com/astral-sh/uv/pull/8968)) - Fix typo in GitLab integration docs ([#9047](https://github.com/astral-sh/uv/pull/9047)) - Update format of environment variable reference ([#9018](https://github.com/astral-sh/uv/pull/9018)) - Use Python syntax for `value_type` consistently ([#9017](https://github.com/astral-sh/uv/pull/9017)) - Use `[[index]]` API in configuration example ([#9065](https://github.com/astral-sh/uv/pull/9065)) - Mention how to use extras ([#8972](https://github.com/astral-sh/uv/pull/8972)) - Add some words about specifying conflicting extras/groups ([#9120](https://github.com/astral-sh/uv/pull/9120)) ## 0.5.3 This release includes support for conflicting optional dependencies and dependency groups in the uv resolver, including the ability to specify dependency sources (like index assignment) on a per-extra or per-group basis. For example, you can now select CPU-only vs. GPU-enabled PyTorch builds at runtime by defining conflicting extras in a `pyproject.toml`, and assigning different extras to different PyTorch indexes: ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.12.0" [project.optional-dependencies] # Include `torch` whenever `--extra cpu` or `--extra gpu` is provided. cpu = ["torch>=2.5.1"] gpu = ["torch>=2.5.1"] [tool.uv] # But allow `cpu` and `gpu` to choose conflicting versions of `torch`. conflicts = [[{ extra = "cpu" }, { extra = "gpu" }]] [tool.uv.sources] torch = [ # With `--extra cpu`, pull PyTorch from the CPU-only index. { index = "pytorch-cpu", extra = "cpu", marker = "platform_system != 'Darwin'" }, # With `--extra gpu`, pull PyTorch from the GPU-enabled index. { index = "pytorch-gpu", extra = "gpu" }, ] [[tool.uv.index]] name = "pytorch-cpu" url = "https://download.pytorch.org/whl/cpu" explicit = true [[tool.uv.index]] name = "pytorch-gpu" url = "https://download.pytorch.org/whl/cu124" explicit = true ``` See the [PyTorch](https://docs.astral.sh/uv/guides/integration/pytorch/) documentation for more. ### Enhancements - Allow conflicting extras in explicit index assignments ([#9160](https://github.com/astral-sh/uv/pull/9160)) - Support overrides and constraints in PEP 723 scripts ([#9162](https://github.com/astral-sh/uv/pull/9162)) - Update `uv tool install --force` to imply `--reinstall-package ` ([#9074](https://github.com/astral-sh/uv/pull/9074)) - Turn `--verify-hashes` on by default ([#9170](https://github.com/astral-sh/uv/pull/9170)) ### Performance - Enable `zlib-rs` on all platforms ([#9202](https://github.com/astral-sh/uv/pull/9202)) ### Bug fixes - Allow apostrophe in virtual environment name ([#8984](https://github.com/astral-sh/uv/pull/8984)) - Automatically retry body errors when processing response ([#9213](https://github.com/astral-sh/uv/pull/9213)) - Detect nested workspace inside the current workspace and members with identical names ([#9094](https://github.com/astral-sh/uv/pull/9094)) - Only install the specified project with `--frozen --package` in legacy non-`[project]` workspaces ([#9215](https://github.com/astral-sh/uv/pull/9215)) - Respect `[[tool.uv.index]]` in PEP 723 scripts ([#9208](https://github.com/astral-sh/uv/pull/9208)) - Show derivation markers for resolutions with project name ([#9136](https://github.com/astral-sh/uv/pull/9136)) - Sort distributions when computing hash ([#9185](https://github.com/astral-sh/uv/pull/9185)) - Include trampolines in source distributions on Windows ([#9172](https://github.com/astral-sh/uv/pull/9172)) ### Documentation - Add `--index =` syntax to index documentation ([#9139](https://github.com/astral-sh/uv/pull/9139)) - Add documentation for using uv with PyTorch ([#9210](https://github.com/astral-sh/uv/pull/9210)) ### Error messages - Add a dedicated error for `include = "dev"` with `tool.uv.dev-dependencies` ([#9173](https://github.com/astral-sh/uv/pull/9173)) - Avoid showing disjoint marker error with `true` ([#9169](https://github.com/astral-sh/uv/pull/9169)) - Improve error message when `git` is not found ([#9206](https://github.com/astral-sh/uv/pull/9206)) - Include extras and dependency groups in derivation chains ([#9113](https://github.com/astral-sh/uv/pull/9113)) - Include version constraints in derivation chains ([#9112](https://github.com/astral-sh/uv/pull/9112)) ## 0.5.4 ### Enhancements - Accept either singular or plural values for CLI requirements ([#9196](https://github.com/astral-sh/uv/pull/9196)) - Add `--all-groups` to `uv sync`, `uv run`, `uv export`, and `uv tree` ([#8892](https://github.com/astral-sh/uv/pull/8892)) - Add a progress bar to `uv tree --outdated` and `uv pip list --outdated` ([#9284](https://github.com/astral-sh/uv/pull/9284)) - Add retries for Python downloads ([#9274](https://github.com/astral-sh/uv/pull/9274)) - Use exponential backoff for publish retries ([#9276](https://github.com/astral-sh/uv/pull/9276)) - Add manylinux target triples up to glibc 2.40 ([#9234](https://github.com/astral-sh/uv/pull/9234)) ### Performance - Parallelize network requests in `uv tree --outdated` ([#9280](https://github.com/astral-sh/uv/pull/9280)) - Use `zlib-rs` on all platforms ([#9264](https://github.com/astral-sh/uv/pull/9264)) ### Bug fixes - Avoid validating extra and group sources in `build-system.requires` ([#9273](https://github.com/astral-sh/uv/pull/9273)) - Catch retries with wrapped `reqwest` errors ([#9253](https://github.com/astral-sh/uv/pull/9253)) - Sort hashes in `uv export` output ([#9237](https://github.com/astral-sh/uv/pull/9237)) - Strip `--index` and `--default-index` from command header ([#9288](https://github.com/astral-sh/uv/pull/9288)) ### Documentation - Add breadcrumbs to the documentation ([#9242](https://github.com/astral-sh/uv/pull/9242)) - Add minimum version to PyTorch guide ([#9247](https://github.com/astral-sh/uv/pull/9247)) - Add support for anchor redirects with client-side js ([#9212](https://github.com/astral-sh/uv/pull/9212)) - Improve content on project configuration ([#9235](https://github.com/astral-sh/uv/pull/9235)) - Improve the project creation documentation ([#9236](https://github.com/astral-sh/uv/pull/9236)) - Move the integration guides into the "Guides" section as a collapsed group ([#9245](https://github.com/astral-sh/uv/pull/9245)) - Reorganize the project concept documentation ([#9121](https://github.com/astral-sh/uv/pull/9121)) - Use the full screen height for the main content to stabilize the nav ([#9153](https://github.com/astral-sh/uv/pull/9153)) ### Error messages - Add dedicated warning for empty stdin ([#9256](https://github.com/astral-sh/uv/pull/9256)) ## 0.5.5 ### Enhancements - Add aliases for build backend requests ([#9294](https://github.com/astral-sh/uv/pull/9294)) - Avoid displaying empty paths ([#9312](https://github.com/astral-sh/uv/pull/9312)) - Allow constraints in `uv tool upgrade` ([#9375](https://github.com/astral-sh/uv/pull/9375)) - Remove conflict between `--no-sync` and `--frozen` in `uv run` ([#9400](https://github.com/astral-sh/uv/pull/9400)) - Respect dependency sources in overrides and constraints ([#9455](https://github.com/astral-sh/uv/pull/9455)) - Show an interpreter-focused message for `--target` and `--prefix` ([#9373](https://github.com/astral-sh/uv/pull/9373)) - Add `--no-extra` flag and setting ([#9387](https://github.com/astral-sh/uv/pull/9387)) - Add `uv export --prune` ([#9389](https://github.com/astral-sh/uv/pull/9389)) - Add dedicated error message for musl install attempts ([#9430](https://github.com/astral-sh/uv/pull/9430)) - Add various grammar changes to conflict error messages ([#9369](https://github.com/astral-sh/uv/pull/9369)) - Annotate default groups in conflict error messages ([#9368](https://github.com/astral-sh/uv/pull/9368)) - Report marker diagnostics during parsing, rather than evaluation ([#9338](https://github.com/astral-sh/uv/pull/9338)) - Use consistent formatting for build system errors ([#9340](https://github.com/astral-sh/uv/pull/9340)) - Use rich diagnostics for build failures ([#9335](https://github.com/astral-sh/uv/pull/9335)) ### Preview features - Improve build backend excludes ([#9281](https://github.com/astral-sh/uv/pull/9281)) - Include PEP 639 `license-files` metadata during `uv publish` ([#9442](https://github.com/astral-sh/uv/pull/9442)) ### Performance - Initialize rayon lazily ([#9435](https://github.com/astral-sh/uv/pull/9435)) - Migrate to PubGrub's arena for package names ([#9448](https://github.com/astral-sh/uv/pull/9448)) ### Bug fixes - Allow dependency groups to include the containing package ([#9385](https://github.com/astral-sh/uv/pull/9385)) - Allow syncing to empty virtual environment directories ([#9427](https://github.com/astral-sh/uv/pull/9427)) - Allow system Python discovery with `--target` and `--prefix` ([#9371](https://github.com/astral-sh/uv/pull/9371)) - Don't warn when `--output-file` is empty ([#9417](https://github.com/astral-sh/uv/pull/9417)) - Fix Python interpreter discovery on non-glibc hosts ([#9005](https://github.com/astral-sh/uv/pull/9005)) - Fix `tool.uv.dependency-metadata.[].version` schema ([#9468](https://github.com/astral-sh/uv/pull/9468)) - Only respect preferences across the same indexes ([#9302](https://github.com/astral-sh/uv/pull/9302)) - Re-compile when `--compile` is passed to an install operation ([#9378](https://github.com/astral-sh/uv/pull/9378)) - Remove `--upgrade`, `--no-upgrade`, and `--upgrade-package` from `uv tool upgrade` ([#9318](https://github.com/astral-sh/uv/pull/9318)) - Remove dev dependencies in `--all-groups --no-dev` ([#9300](https://github.com/astral-sh/uv/pull/9300)) - Surface extras and group conflicts in `uv export` ([#9365](https://github.com/astral-sh/uv/pull/9365)) - Treat deprecated aliases as equivalent in marker algebra ([#9342](https://github.com/astral-sh/uv/pull/9342)) - Treat less compatible tags as lower priority in resolver ([#9339](https://github.com/astral-sh/uv/pull/9339)) ### Documentation - Avoid referencing `scikit-build` (instead of `scikit-build-core`) ([#9320](https://github.com/astral-sh/uv/pull/9320)) - Expand entry points documentation ([#9329](https://github.com/astral-sh/uv/pull/9329)) - Fix example `pyproject.toml` in project concept documentation ([#9298](https://github.com/astral-sh/uv/pull/9298)) - Fix header level of "Conflicting dependencies" page ([#9330](https://github.com/astral-sh/uv/pull/9330)) - Touch-up the extension module guide ([#9293](https://github.com/astral-sh/uv/pull/9293)) - Update the dependencies documentation ([#9359](https://github.com/astral-sh/uv/pull/9359)) - Reference `--no-progress` option in related environment variable ([#9357](https://github.com/astral-sh/uv/pull/9357)) ## 0.5.6 ### Enhancements - Add `--dry-run` to `uv pip uninstall` ([#9557](https://github.com/astral-sh/uv/pull/9557)) - Allow `--constraints` and `--overrides` in `uv tool install` ([#9547](https://github.com/astral-sh/uv/pull/9547)) - Display removed Python executables on uninstall ([#9459](https://github.com/astral-sh/uv/pull/9459)) - Warn when keyring has no password for `uv publish` ([#8827](https://github.com/astral-sh/uv/pull/8827)) - Add suggested action when `.python-version` pin is incompatible with the project ([#9590](https://github.com/astral-sh/uv/pull/9590)) - Improve error messages for mismatches in `tool.uv.sources` ([#9482](https://github.com/astral-sh/uv/pull/9482)) - Use constraints in trace rather than irrelevant `requires-python` ([#9529](https://github.com/astral-sh/uv/pull/9529)) ### Preview features - Add `uv python install --default` ([#8650](https://github.com/astral-sh/uv/pull/8650)) - Fix Python executable installation when multiple patch versions are requested ([#9607](https://github.com/astral-sh/uv/pull/9607)) - Build backend: Revamp `include` / `exclude` ([#9525](https://github.com/astral-sh/uv/pull/9525)) - Build backend: Add fast path ([#9556](https://github.com/astral-sh/uv/pull/9556)) - Build backend: Add functions to collect file list ([#9602](https://github.com/astral-sh/uv/pull/9602)) - Build backend: Default excludes ([#9552](https://github.com/astral-sh/uv/pull/9552)) - Build backend: Refactoring before list ([#9558](https://github.com/astral-sh/uv/pull/9558)) - Build backend: Warn when visiting over 10k files ([#9523](https://github.com/astral-sh/uv/pull/9523)) ### Configuration - Make `check-url` available in configuration files ([#9032](https://github.com/astral-sh/uv/pull/9032)) ### Performance - Avoid adding non-extra package with extra dependencies ([#9540](https://github.com/astral-sh/uv/pull/9540)) - Avoid cloning `String` in marker evaluation ([#9598](https://github.com/astral-sh/uv/pull/9598)) ### Rust API - `uv-pep508`: Add more methods for simplifying `extra`-related expressions ([#9469](https://github.com/astral-sh/uv/pull/9469)) ### Bug fixes - Allow `file:` URLs to include package names ([#9493](https://github.com/astral-sh/uv/pull/9493)) - Avoid using IDs across PubGrub states ([#9538](https://github.com/astral-sh/uv/pull/9538)) - Consistently enforce requested-vs.-built metadata when retrieving wheels ([#9484](https://github.com/astral-sh/uv/pull/9484)) - Do not show empty version specifier in `uv tool list` ([#9605](https://github.com/astral-sh/uv/pull/9605)) - Include Git member information when getting metadata from cache ([#9388](https://github.com/astral-sh/uv/pull/9388)) - Include base installation directory in uv run PATH ([#9585](https://github.com/astral-sh/uv/pull/9585)) - Insert backslash when appending to system drive ([#9488](https://github.com/astral-sh/uv/pull/9488)) - Normalize paths when lowering Git dependencies ([#9595](https://github.com/astral-sh/uv/pull/9595)) - Omit origin when comparing requirements ([#9570](https://github.com/astral-sh/uv/pull/9570)) - Override `manylinux_compatible` with `--python-platform` ([#9526](https://github.com/astral-sh/uv/pull/9526)) - Pass extra when evaluating lockfile markers ([#9539](https://github.com/astral-sh/uv/pull/9539)) - Propagate markers for recursive extras in resolver ([#9509](https://github.com/astral-sh/uv/pull/9509)) - Respect path dependencies within Git dependencies ([#9594](https://github.com/astral-sh/uv/pull/9594)) - Support recursive extras with marker in `pip compile -r pyproject.toml` ([#9535](https://github.com/astral-sh/uv/pull/9535)) - Don't emit unpinned warning for proxy packages ([#9497](https://github.com/astral-sh/uv/pull/9497)) - Fix `--refresh-package` flag mentioned as `--refresh-dependency` ([#9486](https://github.com/astral-sh/uv/pull/9486)) - Handle Windows AV/EDR file locks during script installations ([#9543](https://github.com/astral-sh/uv/pull/9543)) - Re-enable conflicting extra/group tests and fix regression from #9540 ([#9582](https://github.com/astral-sh/uv/pull/9582)) ### Documentation - Add missing word to docs for `run.md` ([#9527](https://github.com/astral-sh/uv/pull/9527)) - Add policies reference section and license document ([#9367](https://github.com/astral-sh/uv/pull/9367)) - Fix typo in entry point docs ([#9491](https://github.com/astral-sh/uv/pull/9491)) - Fix up version in prior uninstall instructions ([#9485](https://github.com/astral-sh/uv/pull/9485)) - Mention `uv pip` behavior in build system note ([#9586](https://github.com/astral-sh/uv/pull/9586)) - Update build failures document ([#9584](https://github.com/astral-sh/uv/pull/9584)) - Correct wording for multiple sources section ([#9504](https://github.com/astral-sh/uv/pull/9504)) ## 0.5.7 ### Enhancements - Ignore dynamic version in source dist ([#9549](https://github.com/astral-sh/uv/pull/9549)) - Improve build frontend error handling ([#9611](https://github.com/astral-sh/uv/pull/9611)) - Un-hide `uv build --no-build-logs` option ([#9642](https://github.com/astral-sh/uv/pull/9642)) - Flag version mismatch between sdist and wheel during `uv build` ([#9633](https://github.com/astral-sh/uv/pull/9633)) - Improve message when updater receipt is for a different uv executable ([#9487](https://github.com/astral-sh/uv/pull/9487)) - Add environment variable to disable writing installer metadata files ([#8877](https://github.com/astral-sh/uv/pull/8877)) - Add managed downloads for the latest CPython releases: `3.9.21`, `3.10.16`, `3.11.11`, `3.12.8`, and `3.13.1` ([#9696](https://github.com/astral-sh/uv/pull/9696)) ### Preview features - Build backend: Add hint on import with preview disabled ([#9691](https://github.com/astral-sh/uv/pull/9691)) - Build backend: Add direct builds to the resolver and installer ([#9621](https://github.com/astral-sh/uv/pull/9621)) - Build backend: Add integration test for scripts ([#9635](https://github.com/astral-sh/uv/pull/9635)) - Build backend: Add template to `uv init` ([#9661](https://github.com/astral-sh/uv/pull/9661)) - Build backend: Add `--list` option ([#9610](https://github.com/astral-sh/uv/pull/9610)) ### Bug fixes - Create missing parent directories for output file of `uv export` / `uv pip compile` ([#9648](https://github.com/astral-sh/uv/pull/9648)) - Fix missing display of non-freethreaded Python 3.13 in `python list` ([#9669](https://github.com/astral-sh/uv/pull/9669)) - Implement `Ord` and `PartialOrd` without origin for `Requirement` ([#9624](https://github.com/astral-sh/uv/pull/9624)) - Include more sources to avoid lowest bound warning ([#9644](https://github.com/astral-sh/uv/pull/9644)) - Respect build tag priority in `uv.lock` ([#9677](https://github.com/astral-sh/uv/pull/9677)) ### Documentation - Add `build-essentials` note to build failures doc ([#9641](https://github.com/astral-sh/uv/pull/9641)) - Add entry-point for distroless image in GitLab documentation ([#9093](https://github.com/astral-sh/uv/pull/9093)) - Add documentation for `uv python pin` without a `REQUEST` argument ([#9631](https://github.com/astral-sh/uv/pull/9631)) - Add a link to `uv python pin` reference docs ([#9630](https://github.com/astral-sh/uv/pull/9630)) ## 0.5.8 **This release does not include the `powerpc64le-unknown-linux-musl` target due to a build issue. See [#9793](https://github.com/astral-sh/uv/issues/9793) for details. If this change affects you, please file an issue with your use-case.** ### Enhancements - Omit empty resolution markers in lockfile ([#9738](https://github.com/astral-sh/uv/pull/9738)) - Add `--install-dir` to to `uv python install` and `uninstall` commands ([#7920](https://github.com/astral-sh/uv/pull/7920)) - Add `--show-urls` and `--only-downloads` to `uv python list` ([#8062](https://github.com/astral-sh/uv/pull/8062)) - Add `uv python list --all-arches` ([#9782](https://github.com/astral-sh/uv/pull/9782)) - Add `uv run --gui-script` flag for running Python scripts with `pythonw.exe` ([#9152](https://github.com/astral-sh/uv/pull/9152)) - Allow `--gui-script` on Unix ([#9787](https://github.com/astral-sh/uv/pull/9787)) - Allow download of Python distribution variants optimized for newer x86_64 microarchitectures ([#9781](https://github.com/astral-sh/uv/pull/9781)) - Allow execution of `pyw` files on Unix ([#9759](https://github.com/astral-sh/uv/pull/9759)) - Allow users to specify URLs in `project.dependencies` and `tool.uv.sources` ([#9718](https://github.com/astral-sh/uv/pull/9718)) - Encode mutually-incompatible pairs of markers ([#9444](https://github.com/astral-sh/uv/pull/9444)) - Improve the error message when a Python install request is not valid ([#9783](https://github.com/astral-sh/uv/pull/9783)) - Preserve directory-level standalone build symlinks ([#9723](https://github.com/astral-sh/uv/pull/9723)) - Add support for `uv publish --index ` ([#9694](https://github.com/astral-sh/uv/pull/9694)) - Reframe `--locked` and `--frozen` as `--check` operations for `uv lock` ([#9662](https://github.com/astral-sh/uv/pull/9662)) - Rename Python install scratch directory from `.cache` -> `.temp` ([#9756](https://github.com/astral-sh/uv/pull/9756)) - Enable `uv tool uninstall uv` on Windows ([#8963](https://github.com/astral-sh/uv/pull/8963)) - Improve self-dependency hint to make shadowing clear ([#9716](https://github.com/astral-sh/uv/pull/9716)) - Refactor unavailable metadata to shrink the resolver ([#9769](https://github.com/astral-sh/uv/pull/9769)) - Show 'depends on itself' for proxy packages ([#9717](https://github.com/astral-sh/uv/pull/9717)) - Show a dedicated error for missing subdirectories ([#9761](https://github.com/astral-sh/uv/pull/9761)) - Show a dedicated hint for missing `git+` prefixes ([#9789](https://github.com/astral-sh/uv/pull/9789)) ### Performance - Eagerly error when parsing `pyproject.toml` requirements ([#9704](https://github.com/astral-sh/uv/pull/9704)) - Use copy-on-write when normalizing paths ([#9710](https://github.com/astral-sh/uv/pull/9710)) ### Bug fixes - Avoid enforcing non-conflicts in `uv export` ([#9751](https://github.com/astral-sh/uv/pull/9751)) - Don't drop comments between items in TOML tables ([#9784](https://github.com/astral-sh/uv/pull/9784)) - Don't fail with `--no-build` when static metadata is available ([#9785](https://github.com/astral-sh/uv/pull/9785)) - Don't filter non-patch registry version ([#9736](https://github.com/astral-sh/uv/pull/9736)) - Don't read metadata from stale `.egg-info` files ([#9760](https://github.com/astral-sh/uv/pull/9760)) - Enforce correctness of self-dependencies ([#9705](https://github.com/astral-sh/uv/pull/9705)) - Fix projects's typo in resolver error messages ([#9708](https://github.com/astral-sh/uv/pull/9708)) - Ignore `.` prefixed directories during managed Python installation discovery ([#9786](https://github.com/astral-sh/uv/pull/9786)) - Improve handling of invalid virtual environments during interpreter discovery ([#8086](https://github.com/astral-sh/uv/pull/8086)) - Normalize relative paths when `--project` is specified ([#9709](https://github.com/astral-sh/uv/pull/9709)) - Respect self-constraints on recursive extras ([#9714](https://github.com/astral-sh/uv/pull/9714)) - Respect user settings for tracing coloring ([#9733](https://github.com/astral-sh/uv/pull/9733)) - Retry on tar extraction errors ([#9753](https://github.com/astral-sh/uv/pull/9753)) - Add conflict markers to the lock file ([#9370](https://github.com/astral-sh/uv/pull/9370)) - De-duplicate resolution markers ([#9780](https://github.com/astral-sh/uv/pull/9780)) - Avoid 403 error hint for PyTorch URLs ([#9750](https://github.com/astral-sh/uv/pull/9750)) - Avoid treating non-existent `--find-links` as relative URLs ([#9720](https://github.com/astral-sh/uv/pull/9720)) - Omit Windows Store `python3.13.exe` et al ([#9679](https://github.com/astral-sh/uv/pull/9679)) - Replace executables with broken symlinks during `uv python install` ([#9706](https://github.com/astral-sh/uv/pull/9706)) ### Documentation - Fix build failure links ([#9740](https://github.com/astral-sh/uv/pull/9740)) ## 0.5.9 ### Enhancements - Fork version selection based on `requires-python` requirements ([#9827](https://github.com/astral-sh/uv/pull/9827)) - Patch `sysconfig` data at install time ([#9857](https://github.com/astral-sh/uv/pull/9857)) - Remove `-isysroot` when patching sysconfig ([#9860](https://github.com/astral-sh/uv/pull/9860)) ### Configuration - Introduce a `--fork-strategy` preference mode ([#9868](https://github.com/astral-sh/uv/pull/9868)) - Add support for `UV_OFFLINE` ([#9795](https://github.com/astral-sh/uv/pull/9795)) ### Bug fixes - Avoid `panic!()` when current directory does not exist ([#9876](https://github.com/astral-sh/uv/pull/9876)) - Avoid reusing interpreter metadata when running under Rosetta ([#9846](https://github.com/astral-sh/uv/pull/9846)) - Avoid trailing slash when deserializing from lockfile ([#9848](https://github.com/astral-sh/uv/pull/9848)) - Fix bug in terms when collapsing unavailable versions in resolver errors ([#9877](https://github.com/astral-sh/uv/pull/9877)) - Fix suggestion to use `uv help python` on invalid install requests ([#9820](https://github.com/astral-sh/uv/pull/9820)) - Skip root when assessing prefix viability ([#9823](https://github.com/astral-sh/uv/pull/9823)) - Avoid spurious 'Upgraded tool environment' in `uv tool upgrade` ([#9870](https://github.com/astral-sh/uv/pull/9870)) ### Rust API - Upgrade minimum Rust version to 1.83 ([#9815](https://github.com/astral-sh/uv/pull/9815)) ### Documentation - Document the `--fork-strategy` setting ([#9887](https://github.com/astral-sh/uv/pull/9887)) ### Preview features - Build backend: Allow underscores in entrypoints ([#9825](https://github.com/astral-sh/uv/pull/9825)) ## 0.5.10 ### Enhancements - Improve backtracking behavior when packages conflict repeatedly ([#9843](https://github.com/astral-sh/uv/pull/9843)) - Patch Python `sysconfig` values such as `AR` at `ar` install time ([#9905](https://github.com/astral-sh/uv/pull/9905)) - Patch Python `sysconfig` values such as `clang` to `cc` at install time ([#9916](https://github.com/astral-sh/uv/pull/9916)) - Skip `--native-tls` in `pip compile` header ([#9913](https://github.com/astral-sh/uv/pull/9913)) - Add resolver error hint for no-binary and no-build failures ([#9948](https://github.com/astral-sh/uv/pull/9948)) - Improve build error messages ([#9660](https://github.com/astral-sh/uv/pull/9660)) - Reduce redundant Python version incompatibilities in resolver error message ([#9957](https://github.com/astral-sh/uv/pull/9957)) - Reduce redundant enumeration of all package versions in some resolver errors ([#9885](https://github.com/astral-sh/uv/pull/9885)) - Improve display of ranges when pre-releases are not allowed ([#9944](https://github.com/astral-sh/uv/pull/9944)) - Improve error messages for `uv remove` ([#9959](https://github.com/astral-sh/uv/pull/9959)) - Improve phrasing for single term incompatibilities ([#9953](https://github.com/astral-sh/uv/pull/9953)) - Improve styling of `uv remove` dependency hints ([#9960](https://github.com/astral-sh/uv/pull/9960)) - Omit trailing zeros on Python requirements inferred from versions ([#9952](https://github.com/astral-sh/uv/pull/9952)) - Show a concise error message for missing `version` field ([#9912](https://github.com/astral-sh/uv/pull/9912)) - Use the build options value to improve hints for no wheel / source distribution errors ([#9950](https://github.com/astral-sh/uv/pull/9950)) ### Bug fixes - Allow multiple disjoint URLs in overrides ([#9893](https://github.com/astral-sh/uv/pull/9893)) - Include explicit indexes in publish index choice ([#9932](https://github.com/astral-sh/uv/pull/9932)) - Fix Python interpreter detection for 32-bit operating systems on 64-bit hosts ([#9970](https://github.com/astral-sh/uv/pull/9970)) ### Documentation - Fix typo "operation system" ([#9971](https://github.com/astral-sh/uv/pull/9971)) - Clarify uninstallation docs ([#9938](https://github.com/astral-sh/uv/pull/9938)) - Add a note to say that dependencies between workspace members are editable ([#9363](https://github.com/astral-sh/uv/pull/9363)) - Correctly document default value of `fork-strategy` setting ([#9931](https://github.com/astral-sh/uv/pull/9931)) - Use double quotes for Windows support in examples ([#9946](https://github.com/astral-sh/uv/pull/9946)) - Remove `pypy` from top-level pin example ([#9896](https://github.com/astral-sh/uv/pull/9896)) - Update references to `python-build-standalone` to reflect the transferred project ([#9977](https://github.com/astral-sh/uv/pull/9977)) - Use a different Ruff version in documentation ([#9943](https://github.com/astral-sh/uv/pull/9943)) - Change example so it works as-is on `powershell` and `cmd.exe` ([#9903](https://github.com/astral-sh/uv/pull/9903)) - Clarify best practice for Python matrix strategy in GitHub Actions ([#9454](https://github.com/astral-sh/uv/pull/9454)) - Add documentation for `uv-lock` and `uv-export` pre-commit hooks ([#9872](https://github.com/astral-sh/uv/pull/9872)) ### Preview features - Build backend: Fix pre-PEP 639 license files ([#9965](https://github.com/astral-sh/uv/pull/9965)) ## 0.5.11 ### Enhancements - Normalize `platform_system` to `sys_platform` ([#9949](https://github.com/astral-sh/uv/pull/9949)) - Improve retry mechanisms on Windows for `copy_atomic` and `write_atomic` ([#10026](https://github.com/astral-sh/uv/pull/10026)) - Add nuance to prefetch logging ([#9984](https://github.com/astral-sh/uv/pull/9984)) - Update to [`python-build-standalone 20241219`](https://github.com/astral-sh/python-build-standalone/releases/tag/20241219) ### Preview features - Build backend: Preserve executable bits for scripts in distributions ([#10027](https://github.com/astral-sh/uv/pull/10027)) - Build backend: Handle case where `metadata_directory` already contains `dist-info` directory ([#10005](https://github.com/astral-sh/uv/pull/10005)) ### Performance - Batch resolver pre-fetches per fork ([#10029](https://github.com/astral-sh/uv/pull/10029)) ### Bug fixes - Allow `--script` to be provided with `uv run -` ([#10035](https://github.com/astral-sh/uv/pull/10035)) - Allow `uv run` arguments when reading from `stdin` ([#10034](https://github.com/astral-sh/uv/pull/10034)) - Prefer higher Python lower-bounds when forking ([#10007](https://github.com/astral-sh/uv/pull/10007)) - Remove references to deprecated `first-match` ([#10036](https://github.com/astral-sh/uv/pull/10036)) ### Documentation - Add `uv python install --preview` to the documentation ([#10010](https://github.com/astral-sh/uv/pull/10010)) - Fix `uv python install --default` note about multiple requests ([#10011](https://github.com/astral-sh/uv/pull/10011)) - Fix typo in Caching docs ([#10032](https://github.com/astral-sh/uv/pull/10032)) - Remove remaining references to deprecated `first-match` ([#10038](https://github.com/astral-sh/uv/pull/10038)) - Supplement missing separators for `UV_INSTALL_DIR` directions on Windows ([#9507](https://github.com/astral-sh/uv/pull/9507)) ## 0.5.12 ### Enhancements - Support `uv export` for non-project workspaces ([#10144](https://github.com/astral-sh/uv/pull/10144)) - Set glibc versions for standalone installers ([#10142](https://github.com/astral-sh/uv/pull/10142)) - Allow environment variables to be included in cache keys ([#10170](https://github.com/astral-sh/uv/pull/10170)) ### Preview features - Include extras in `uv-build` `Requires-Dist` metadata ([#10110](https://github.com/astral-sh/uv/pull/10110)) - Use `shutil.which` for the build backend ([#10028](https://github.com/astral-sh/uv/pull/10028)) ### Bug fixes - Always write slash paths to RECORD file ([#10164](https://github.com/astral-sh/uv/pull/10164)) - Add support for subdirectories in direct URLs in `uv.lock` ([#10068](https://github.com/astral-sh/uv/pull/10068)) - Avoid duplicating backslashes in sysconfig parser ([#10063](https://github.com/astral-sh/uv/pull/10063)) - Avoid erroring when subdirectories are provided in `uv add` ([#10095](https://github.com/astral-sh/uv/pull/10095)) - Backtrack to non-local versions when wheels are missing platform support ([#10046](https://github.com/astral-sh/uv/pull/10046)) - Fix mirror script to handle newer metadata format ([#10050](https://github.com/astral-sh/uv/pull/10050)) - Preserve sort when deciding on requirement placement ([#10078](https://github.com/astral-sh/uv/pull/10078)) - Remove redundant alias in `uv init` CLI ([#10124](https://github.com/astral-sh/uv/pull/10124)) - Respect sources credentials in non-project workspaces ([#10125](https://github.com/astral-sh/uv/pull/10125)) - Show non-project dependencies in `uv tree` ([#10149](https://github.com/astral-sh/uv/pull/10149)) - Strip fragment when storing direct URL ([#10093](https://github.com/astral-sh/uv/pull/10093)) - Include hashes for local source archives ([#10080](https://github.com/astral-sh/uv/pull/10080)) ### Documentation - Fix invalid syntax in some sources examples ([#10127](https://github.com/astral-sh/uv/pull/10127)) ## 0.5.13 ### Bug fixes - Avoid enforcing URL check on initial publish ([#10182](https://github.com/astral-sh/uv/pull/10182)) - Fix incorrect mismatched constraints reference ([#10184](https://github.com/astral-sh/uv/pull/10184)) - Revert "Update `reqwest` (#10178)" ([#10187](https://github.com/astral-sh/uv/pull/10187)) ## 0.5.14 ### Enhancements - Add `--exact` flag to `uv run` ([#10198](https://github.com/astral-sh/uv/pull/10198)) - Add `--outdated` support to `uv pip tree` ([#10199](https://github.com/astral-sh/uv/pull/10199)) - Add a required version setting to uv ([#10248](https://github.com/astral-sh/uv/pull/10248)) - Add loongarch64 to supported Python platform tags ([#10223](https://github.com/astral-sh/uv/pull/10223)) - Add manylinux2014 aliases for `--python-platform` ([#10217](https://github.com/astral-sh/uv/pull/10217)) - Add support for Python interpreters on ARMv5TE platforms ([#10234](https://github.com/astral-sh/uv/pull/10234)) - Add support for optional `--description` in `uv init` ([#10209](https://github.com/astral-sh/uv/pull/10209)) - Ignore empty or missing hrefs in Simple HTML ([#10276](https://github.com/astral-sh/uv/pull/10276)) - Patch pkgconfig files after Python install ([#10189](https://github.com/astral-sh/uv/pull/10189)) ### Performance - Actually use jemalloc as alternative allocator ([#10269](https://github.com/astral-sh/uv/pull/10269)) - Parse URLs lazily in resolver ([#10259](https://github.com/astral-sh/uv/pull/10259)) - Use `BTreeMap::range` to avoid iterating over unnecessary versions ([#10266](https://github.com/astral-sh/uv/pull/10266)) ### Bug fixes - Accept directories with space names in `uv init` ([#10246](https://github.com/astral-sh/uv/pull/10246)) - Avoid forking on version in non-universal resolutions ([#10274](https://github.com/astral-sh/uv/pull/10274)) - Avoid stripping query parameters from URLs ([#10253](https://github.com/astral-sh/uv/pull/10253)) - Consider workspace dependencies to be 'direct' ([#10197](https://github.com/astral-sh/uv/pull/10197)) - Detect cyclic dependencies during builds ([#10258](https://github.com/astral-sh/uv/pull/10258)) - Guard against self-deletion in `uv venv` and `uv tool` ([#10206](https://github.com/astral-sh/uv/pull/10206)) - Respect static metadata for already-installed distributions ([#10242](https://github.com/astral-sh/uv/pull/10242)) ## 0.5.15 ### Python The managed Python distributions have been updated, including: - Python 3.14.0a3 support on macOS and Linux - Performance improvements - Fixes to SQLite feature detection See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250106) for more details. ### Enhancements - Respect `FORCE_COLOR` environment variable ([#10315](https://github.com/astral-sh/uv/pull/10315)) ### Performance - Avoid generating unused hashes during `uv lock` ([#10307](https://github.com/astral-sh/uv/pull/10307)) - Visit source distributions before wheels ([#10291](https://github.com/astral-sh/uv/pull/10291)) ### Bug fixes - Avoid downgrading packages when `--upgrade` is provided ([#10097](https://github.com/astral-sh/uv/pull/10097)) - Extract supported architectures from wheel tags ([#10179](https://github.com/astral-sh/uv/pull/10179)) - Redact new index credentials in `uv add` ([#10329](https://github.com/astral-sh/uv/pull/10329)) ### Documentation - Clarify `exclude-newer` only allows full timestamps in settings documentation ([#9135](https://github.com/astral-sh/uv/pull/9135)) - Tweak script `--no-project` comment ([#10331](https://github.com/astral-sh/uv/pull/10331)) - Update copyright year ([#10297](https://github.com/astral-sh/uv/pull/10297)) - Add instructions for installing with Scoop ([#10332](https://github.com/astral-sh/uv/pull/10332)) ## 0.5.16 ### Enhancements - Accept full requirements in `uv remove` ([#10338](https://github.com/astral-sh/uv/pull/10338)) ### Performance - Avoid over-counting versions in batch prefetcher ([#10350](https://github.com/astral-sh/uv/pull/10350)) - Deactivate tracing for version-choosing ([#10351](https://github.com/astral-sh/uv/pull/10351)) - Force a niche into `VersionSmall` ([#10385](https://github.com/astral-sh/uv/pull/10385)) - Optimize `requirements_for_extra` ([#10348](https://github.com/astral-sh/uv/pull/10348)) - Re-enable `zlib-ng` on x86 platforms ([#10365](https://github.com/astral-sh/uv/pull/10365)) - Re-enable zlib-ng on all platforms (except s390x, PowerPC, and FreeBSD) ([#10370](https://github.com/astral-sh/uv/pull/10370)) - Remove `[u64; 4]` from small version to move `Arc` to full version ([#10345](https://github.com/astral-sh/uv/pull/10345)) - Shrink `Dist` from 352 to 288 bytes ([#10389](https://github.com/astral-sh/uv/pull/10389)) - Speed up file pins by removing nested hash map ([#10346](https://github.com/astral-sh/uv/pull/10346)) - Buffer file reads in `serde_json::from_reader` ([#10341](https://github.com/astral-sh/uv/pull/10341)) ### Bug fixes - Avoid enforcing project-level required version for `uv self` ([#10374](https://github.com/astral-sh/uv/pull/10374)) - Fix Ruff linting warnings from generated template files for extension modules ([#10371](https://github.com/astral-sh/uv/pull/10371)) ### Documentation - Add AWS Lambda integration guide ([#10278](https://github.com/astral-sh/uv/pull/10278)) ## 0.5.17 This release includes support for generating lockfiles from scripts based on inline metadata, as defined in PEP 723. By default, scripts remain unlocked, and must be locked explicitly with `uv lock --script /path/to/script.py`, which will generate a lockfile adjacent to the script (e.g., `script.py.lock`). Once generated, the lockfile will be respected (and updated, if necessary) across `uv run --script`, `uv add --script`, and `uv remove --script` invocations. This release also includes support for `uv export --script` and `uv tree --script`. Both commands support PEP 723 scripts with and without accompanying lockfiles. ### Enhancements - Add support for locking PEP 723 scripts ([#10135](https://github.com/astral-sh/uv/pull/10135)) - Respect PEP 723 script lockfiles in `uv run` ([#10136](https://github.com/astral-sh/uv/pull/10136)) - Update PEP 723 lockfile in `uv add --script` ([#10145](https://github.com/astral-sh/uv/pull/10145)) - Update PEP 723 lockfile in `uv remove --script` ([#10162](https://github.com/astral-sh/uv/pull/10162)) - Add `--script` support to `uv export` for PEP 723 scripts ([#10160](https://github.com/astral-sh/uv/pull/10160)) - Add `--script` support to `uv tree` for PEP 723 scripts ([#10159](https://github.com/astral-sh/uv/pull/10159)) - Add `ls` alias to `uv {tool, python, pip} list` ([#10240](https://github.com/astral-sh/uv/pull/10240)) - Allow reading `--with-requirements` from stdin in `uv add` and `uv run` ([#10447](https://github.com/astral-sh/uv/pull/10447)) - Warn-and-ignore for unsupported `requirements.txt` options ([#10420](https://github.com/astral-sh/uv/pull/10420)) ### Preview features - Add remaining Python type annotations to build backend ([#10434](https://github.com/astral-sh/uv/pull/10434)) ### Performance - Avoid allocating for names in the PEP 508 parser ([#10476](https://github.com/astral-sh/uv/pull/10476)) - Fetch concurrently for non-first-match index strategies ([#10432](https://github.com/astral-sh/uv/pull/10432)) - Remove unnecessary `.to_string()` call ([#10419](https://github.com/astral-sh/uv/pull/10419)) - Respect sentinels in package prioritization ([#10443](https://github.com/astral-sh/uv/pull/10443)) - Use `ArcStr` for marker values ([#10453](https://github.com/astral-sh/uv/pull/10453)) - Use `ArcStr` for package, extra, and group names ([#10475](https://github.com/astral-sh/uv/pull/10475)) - Use `matches!` rather than `contains` in `requirements.txt` parsing ([#10423](https://github.com/astral-sh/uv/pull/10423)) - Use faster disjointness check for markers ([#10439](https://github.com/astral-sh/uv/pull/10439)) - Pre-compute PEP 508 markers from universal markers ([#10472](https://github.com/astral-sh/uv/pull/10472)) ### Bug fixes - Fix `UV_FIND_LINKS` delimiter to split on commas ([#10477](https://github.com/astral-sh/uv/pull/10477)) - Improve `uv tool list` output when tool environment is broken ([#10409](https://github.com/astral-sh/uv/pull/10409)) - Only track markers for compatible versions ([#10457](https://github.com/astral-sh/uv/pull/10457)) - Respect `requires-python` when installing tools ([#10401](https://github.com/astral-sh/uv/pull/10401)) - Visit proxy packages eagerly ([#10441](https://github.com/astral-sh/uv/pull/10441)) - Improve shell compatibility of `venv` activate scripts ([#10397](https://github.com/astral-sh/uv/pull/10397)) - Read publish username from URL ([#10469](https://github.com/astral-sh/uv/pull/10469)) ### Documentation - Add Lambda layer instructions to AWS Lambda guide ([#10411](https://github.com/astral-sh/uv/pull/10411)) - Add `uv lock --script` to the docs ([#10414](https://github.com/astral-sh/uv/pull/10414)) - Use Windows-specific instructions in Jupyter guide ([#10446](https://github.com/astral-sh/uv/pull/10446)) ## 0.5.18 ### Bug fixes - Avoid forking for identical markers ([#10490](https://github.com/astral-sh/uv/pull/10490)) - Avoid panic in `uv remove` when only comments exist ([#10484](https://github.com/astral-sh/uv/pull/10484)) - Revert "improve shell compatibility of venv activate scripts (#10397)" ([#10497](https://github.com/astral-sh/uv/pull/10497)) ## 0.5.19 ### Enhancements - Filter wheels from lockfile based on architecture ([#10584](https://github.com/astral-sh/uv/pull/10584)) - Omit dynamic versions from the lockfile ([#10622](https://github.com/astral-sh/uv/pull/10622)) - Add support for `pip freeze --path` ([#10488](https://github.com/astral-sh/uv/pull/10488)) - Reduce verbosity of inline-metadata message when using `uv run ` ([#10588](https://github.com/astral-sh/uv/pull/10588)) - Add opt-in Git LFS support ([#10335](https://github.com/astral-sh/uv/pull/10335)) - Recommend `--native-tls` on SSL errors ([#10605](https://github.com/astral-sh/uv/pull/10605)) - Show expected and available ABI tags in resolver errors ([#10527](https://github.com/astral-sh/uv/pull/10527)) - Show target Python version in error messages ([#10582](https://github.com/astral-sh/uv/pull/10582)) - Add `--output-format=json` support to `uv python list` ([#10596](https://github.com/astral-sh/uv/pull/10596)) ### Python The managed Python distributions have been updated, including: - Python 3.14 support on Windows - Python 3.14.0a4 support - 64-bit RISC-V Linux support - Bundled `libedit` updated from 20210910-3.1 -> 20240808-3.1 - Bundled `tcl/tk` updated from 8.6.12 -> 8.6.14 (for all Python versions on Unix, only for Python 3.14 on Windows) See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250115) for more details. ### Performance - Avoid allocating when stripping source distribution extension ([#10625](https://github.com/astral-sh/uv/pull/10625)) - Reduce `WheelFilename` to 48 bytes ([#10583](https://github.com/astral-sh/uv/pull/10583)) - Reduce distribution size to 200 bytes ([#10601](https://github.com/astral-sh/uv/pull/10601)) - Remove `import re` from entrypoint wrapper scripts ([#10627](https://github.com/astral-sh/uv/pull/10627)) - Shrink size of platform tag enum ([#10546](https://github.com/astral-sh/uv/pull/10546)) - Use `ArcStr` in verbatim URL ([#10600](https://github.com/astral-sh/uv/pull/10600)) - Use `memchr` for wheel parsing ([#10620](https://github.com/astral-sh/uv/pull/10620)) ### Bug fixes - Avoid reading symlinks during `uv python install` on Windows ([#10639](https://github.com/astral-sh/uv/pull/10639)) - Correct Pyston tag format ([#10580](https://github.com/astral-sh/uv/pull/10580)) - Provide `pyproject.toml` path for parse errors in `uv venv` ([#10553](https://github.com/astral-sh/uv/pull/10553)) - Don't treat `setuptools` and `wheel` as seed packages in uv sync on Python 3.12 ([#10572](https://github.com/astral-sh/uv/pull/10572)) - Fix git-tag cache-key reader in case of slashes (#10467) ([#10500](https://github.com/astral-sh/uv/pull/10500)) - Include build tag in rendered wheel filenames ([#10599](https://github.com/astral-sh/uv/pull/10599)) - Patch embedded install path for Python dylib on macOS during `python install` ([#10629](https://github.com/astral-sh/uv/pull/10629)) - Read cached registry distributions when `--config-settings` are present ([#10578](https://github.com/astral-sh/uv/pull/10578)) - Show resolver hints for packages with markers ([#10607](https://github.com/astral-sh/uv/pull/10607)) ### Documentation - Add meta titles to documents in guides, excluding integration documents ([#10539](https://github.com/astral-sh/uv/pull/10539)) - Remove `build-system` from example workspace rot ([#10636](https://github.com/astral-sh/uv/pull/10636)) ### Preview features - Make build backend type annotations more generic ([#10549](https://github.com/astral-sh/uv/pull/10549)) ## 0.5.20 ### Bug fixes - Avoid failing when deserializing unknown tags ([#10655](https://github.com/astral-sh/uv/pull/10655)) ## 0.5.21 ### Enhancements - Avoid building dynamic versions when validating lockfile ([#10703](https://github.com/astral-sh/uv/pull/10703)) ### Configuration - Add `UV_VENV_SEED` environment variable ([#10715](https://github.com/astral-sh/uv/pull/10715)) ### Performance - Store unsupported tags in wheel filename ([#10665](https://github.com/astral-sh/uv/pull/10665)) ### Bug fixes - Avoid attempting to patch macOS dylib for non-macOS installs ([#10721](https://github.com/astral-sh/uv/pull/10721)) - Avoid narrowing `requires-python` marker with disjunctions ([#10704](https://github.com/astral-sh/uv/pull/10704)) - Respect environment variable credentials for indexes outside root ([#10688](https://github.com/astral-sh/uv/pull/10688)) - Respect preferences for explicit index dependencies from `requirements.txt` ([#10690](https://github.com/astral-sh/uv/pull/10690)) - Sort preferences by environment, then index ([#10700](https://github.com/astral-sh/uv/pull/10700)) - Ignore permission errors when looking for user-level configuration file ([#10697](https://github.com/astral-sh/uv/pull/10697)) ### Documentation - Add `SyntaxWarning` compatibility note to bytecode compilation docs ([#10701](https://github.com/astral-sh/uv/pull/10701)) - Add `MACOSX_DEPLOYMENT_TARGET` to the `--python-platform` documentation ([#10698](https://github.com/astral-sh/uv/pull/10698)) ## 0.5.22 ### Enhancements - Include version and contact information in GitHub User Agent ([#10785](https://github.com/astral-sh/uv/pull/10785)) ### Performance - Add fast-path for recursive extras in dynamic validation ([#10823](https://github.com/astral-sh/uv/pull/10823)) - Fetch `pyproject.toml` from GitHub API ([#10765](https://github.com/astral-sh/uv/pull/10765)) - Remove allocation in Git SHA truncation ([#10801](https://github.com/astral-sh/uv/pull/10801)) - Skip GitHub fast path when full commit is already known ([#10800](https://github.com/astral-sh/uv/pull/10800)) ### Bug fixes - Add fallback to build backend when `Requires-Dist` mismatches ([#10797](https://github.com/astral-sh/uv/pull/10797)) - Avoid deserialization error for paths above the root ([#10789](https://github.com/astral-sh/uv/pull/10789)) - Avoid respecting preferences from other indexes ([#10782](https://github.com/astral-sh/uv/pull/10782)) - Disable the distutils setuptools shim during interpreter query ([#10819](https://github.com/astral-sh/uv/pull/10819)) - Omit variant when detecting compatible Python installs ([#10722](https://github.com/astral-sh/uv/pull/10722)) - Remove TOCTOU errors in Git clone ([#10758](https://github.com/astral-sh/uv/pull/10758)) - Validate metadata under GitHub fast path ([#10796](https://github.com/astral-sh/uv/pull/10796)) - Include conflict markers in fork markers ([#10818](https://github.com/astral-sh/uv/pull/10818)) ### Error messages - Add tag incompatibility hints to sync failures ([#10739](https://github.com/astral-sh/uv/pull/10739)) - Improve log when distutils is missing ([#10713](https://github.com/astral-sh/uv/pull/10713)) - Show non-critical Python discovery errors if no other interpreter is found ([#10716](https://github.com/astral-sh/uv/pull/10716)) - Use colors for lock errors ([#10736](https://github.com/astral-sh/uv/pull/10736)) ### Documentation - Add testing instructions to the AWS Lambda guide ([#10805](https://github.com/astral-sh/uv/pull/10805)) ## 0.5.23 ### Enhancements - Add `--refresh` to `uv venv` ([#10834](https://github.com/astral-sh/uv/pull/10834)) - Add `--no-default-groups` command-line flag ([#10618](https://github.com/astral-sh/uv/pull/10618)) ### Bug fixes - Sort extras and groups when comparing lockfile requirements ([#10856](https://github.com/astral-sh/uv/pull/10856)) - Include `commit_id` and `requested_revision` in `direct_url.json` ([#10862](https://github.com/astral-sh/uv/pull/10862)) - Invalidate lockfile when static versions change ([#10858](https://github.com/astral-sh/uv/pull/10858)) - Make GitHub fast path errors non-fatal ([#10859](https://github.com/astral-sh/uv/pull/10859)) - Remove warnings for `--frozen` and `--locked` in `uv run --script` ([#10840](https://github.com/astral-sh/uv/pull/10840)) - Resolve `find-links` paths relative to the configuration file ([#10827](https://github.com/astral-sh/uv/pull/10827)) - Respect visitation order for proxy packages ([#10833](https://github.com/astral-sh/uv/pull/10833)) - Treat version mismatch errors as non-fatal in fast paths ([#10860](https://github.com/astral-sh/uv/pull/10860)) - Mark `--locked` and `--upgrade` are conflicting ([#10836](https://github.com/astral-sh/uv/pull/10836)) - Relax error checking around unconditional enabling of conflicting extras ([#10875](https://github.com/astral-sh/uv/pull/10875)) ### Documentation - Reduce ambiguity in conflicting extras example ([#10877](https://github.com/astral-sh/uv/pull/10877)) - Update pre-commit documentation ([#10756](https://github.com/astral-sh/uv/pull/10756)) ### Error messages - Error when workspace contains conflicting Python requirements ([#10841](https://github.com/astral-sh/uv/pull/10841)) - Improve uvx error message when uv is missing ([#9745](https://github.com/astral-sh/uv/pull/9745)) ## 0.5.24 ### Enhancements - Improve determinism of resolution by always setting package priorities ([#10853](https://github.com/astral-sh/uv/pull/10853)) - Upgrade to `cargo-dist` 0.28.0; improves several installer behaviors ([#10884](https://github.com/astral-sh/uv/pull/10884)) ### Performance - Remove dependencies clone in resolver ([#10880](https://github.com/astral-sh/uv/pull/10880)) - Use Hashbrown's raw entry API to reduce hashes and clone in resolver priority determination ([#10881](https://github.com/astral-sh/uv/pull/10881)) ### Bug fixes - Allow fallback to Python download on non-critical discovery errors ([#10908](https://github.com/astral-sh/uv/pull/10908)) ### Preview features - Register managed Python version with the Windows Registry (PEP 514) ([#10634](https://github.com/astral-sh/uv/pull/10634)) ### Documentation - Improve documentation for some environment variables ([#10887](https://github.com/astral-sh/uv/pull/10887)) - Add git subdirectory example ([#10894](https://github.com/astral-sh/uv/pull/10894)) ## 0.5.25 ### Enhancements - Allow installation of manylinux wheels on loongarch64 ([#10927](https://github.com/astral-sh/uv/pull/10927)) - Allow optional `=` for editables in `requirements.txt` ([#10954](https://github.com/astral-sh/uv/pull/10954)) - Add Windows aarch64 to the release binaries ([#10885](https://github.com/astral-sh/uv/pull/10885)) ### Bug fixes - Use spec-compliant (`128+n`) exit codes for `uv run` and `uv tool run` on Unix ([#10781](https://github.com/astral-sh/uv/pull/10781)) - Fix best-interpreter lookups when there is an invalid interpreter in the `PATH` ([#11030](https://github.com/astral-sh/uv/pull/11030)) - Guard against concurrent cache writes on Windows ([#11007](https://github.com/astral-sh/uv/pull/11007)) - Prioritize package preferences with greater package versions ([#10963](https://github.com/astral-sh/uv/pull/10963)) - Reject `--editable` flag on non-directory requirements ([#10994](https://github.com/astral-sh/uv/pull/10994)) - Respect `--no-sources` for `uv pip install` workspace discovery ([#11003](https://github.com/astral-sh/uv/pull/11003)) - Set `JEMALLOC_SYS_WITH_LG_PAGE=16` in ARM Docker builds ([#10943](https://github.com/astral-sh/uv/pull/10943)) - Update `riscv64` Python downloads to allow install on `riscv64gc` ([#10937](https://github.com/astral-sh/uv/pull/10937)) - Fix file persist retries on Windows ([#11008](https://github.com/astral-sh/uv/pull/11008)) - Fix incorrect error message when specifying `tool.uv.sources.(package).workspace` with other options ([#11013](https://github.com/astral-sh/uv/pull/11013)) - Improve SIGINT handling in `uv run` ([#11009](https://github.com/astral-sh/uv/pull/11009)) ### Documentation - Add `SECURITY` policy ([#11035](https://github.com/astral-sh/uv/pull/11035)) - Add `Requires-Python` upper bound behavior to the docs ([#10964](https://github.com/astral-sh/uv/pull/10964)) - Add a troubleshooting section and reproducible example guide ([#10947](https://github.com/astral-sh/uv/pull/10947)) - Add documentation for `uv add -r` ([#10926](https://github.com/astral-sh/uv/pull/10926)) - Amend `requires-python` rules in resolver documentation ([#10993](https://github.com/astral-sh/uv/pull/10993)) - Reference workspaces in `--no-sources` documentation ([#10995](https://github.com/astral-sh/uv/pull/10995)) - Update documentation for activating virtual environments in different shell ([#11000](https://github.com/astral-sh/uv/pull/11000)) - Add Docker SHA pinning tip ([#10955](https://github.com/astral-sh/uv/pull/10955)) ## 0.5.26 ### Enhancements - Add support for `uvx python` ([#11076](https://github.com/astral-sh/uv/pull/11076)) - Allow `--no-dev --invert` in `uv tree` ([#11068](https://github.com/astral-sh/uv/pull/11068)) - Update `uv python install --reinstall` to reinstall all previous versions ([#11072](https://github.com/astral-sh/uv/pull/11072)) - Consistently write log messages with capitalized first word ([#11111](https://github.com/astral-sh/uv/pull/11111)) - Suggest `--build-backend` when `--backend` is passed to `uv init` ([#10958](https://github.com/astral-sh/uv/pull/10958)) - Improve retry trace message ([#11108](https://github.com/astral-sh/uv/pull/11108)) ### Performance - Remove unnecessary UTF-8 conversion in hash parsing ([#11110](https://github.com/astral-sh/uv/pull/11110)) ### Bug fixes - Ignore non-hash fragments in HTML API responses ([#11107](https://github.com/astral-sh/uv/pull/11107)) - Avoid resolving symbolic links when querying Python interpreters ([#11083](https://github.com/astral-sh/uv/pull/11083)) - Avoid sharing state between universal and non-universal resolves ([#11051](https://github.com/astral-sh/uv/pull/11051)) - Error when `--script` is passing a non-PEP 723 script ([#11118](https://github.com/astral-sh/uv/pull/11118)) - Make metadata deserialization failures non-fatal in the cache ([#11105](https://github.com/astral-sh/uv/pull/11105)) - Mark metadata as dynamic when reading from built wheel cache ([#11046](https://github.com/astral-sh/uv/pull/11046)) - Propagate credentials for `/simple` to `/...` endpoints ([#11074](https://github.com/astral-sh/uv/pull/11074)) - Fix conflicting extra bug during `uv sync` ([#11075](https://github.com/astral-sh/uv/pull/11075)) ### Documentation - Add PyTorch XPU instructions to the PyTorch guide ([#11109](https://github.com/astral-sh/uv/pull/11109)) - Add docs for signal handling ([#11041](https://github.com/astral-sh/uv/pull/11041)) - Explain build frontend vs. build backend ([#11094](https://github.com/astral-sh/uv/pull/11094)) - Fix formatting of `RUST_LOG` documentation ([#10053](https://github.com/astral-sh/uv/pull/10053)) - Fix typo in `--no-deps` description ([#11073](https://github.com/astral-sh/uv/pull/11073)) - Reflow CLI documentation comments ([#11040](https://github.com/astral-sh/uv/pull/11040)) - Shorten "Using existing Python versions" nav item so it fits on one line ([#11077](https://github.com/astral-sh/uv/pull/11077)) - Some minor touch-ups to the Python install guide ([#11116](https://github.com/astral-sh/uv/pull/11116)) - Update Dependabot tracking issue link ([#11054](https://github.com/astral-sh/uv/pull/11054)) - Update documentation for running in a container ([#11052](https://github.com/astral-sh/uv/pull/11052)) - Upgrade PyTorch version in documentation ([#11114](https://github.com/astral-sh/uv/pull/11114)) - Use `sys_platform` in lieu of `platform_system` in PyTorch docs ([#11113](https://github.com/astral-sh/uv/pull/11113)) - Use positive (rather than negative) markers in PyTorch examples ([#11112](https://github.com/astral-sh/uv/pull/11112)) - Fix unnecessary backslashes in brackets ([#11059](https://github.com/astral-sh/uv/pull/11059)) - Suggest setting copy link mode in GitLab integration guide ([#11067](https://github.com/astral-sh/uv/pull/11067)) ## 0.5.27 ### Enhancements - Avoid setting permissions during tar extraction ([#11191](https://github.com/astral-sh/uv/pull/11191)) - Remove warnings for missing lower bounds ([#11195](https://github.com/astral-sh/uv/pull/11195)) - Update PubGrub to set-based outdated priority tracking ([#11169](https://github.com/astral-sh/uv/pull/11169)) - Improve error messages for `uv pip install` with `--extra` or `--all-extras` and invalid sources ([#11193](https://github.com/astral-sh/uv/pull/11193)) - Sign Docker images using GitHub attestations ([#8685](https://github.com/astral-sh/uv/pull/8685)) ### Preview features - Don't expand self-referential extras in the build backend ([#11142](https://github.com/astral-sh/uv/pull/11142)) ### Performance - Filter discovered Python executables by source before querying ([#11143](https://github.com/astral-sh/uv/pull/11143)) - Optimize exclusion computation for markers ([#11158](https://github.com/astral-sh/uv/pull/11158)) - Use Astral-maintained `tokio-tar` fork ([#11174](https://github.com/astral-sh/uv/pull/11174)) - Remove unneeded `.clone()` ([#11127](https://github.com/astral-sh/uv/pull/11127)) ### Bug fixes - Fix relative paths in bytecode compilation ([#11177](https://github.com/astral-sh/uv/pull/11177)) - Percent-decode URLs in canonical comparisons ([#11088](https://github.com/astral-sh/uv/pull/11088)) - Respect concurrency limits in parallel index fetch ([#11182](https://github.com/astral-sh/uv/pull/11182)) - Use wire JSON schema for conflict items ([#11196](https://github.com/astral-sh/uv/pull/11196)) - Use explicit `_GLibCVersion` tuple in uv-python crate ([#11122](https://github.com/astral-sh/uv/pull/11122)) ### Documentation - Add Git SHA locking behavior to docs ([#11125](https://github.com/astral-sh/uv/pull/11125)) - Add best-practice flags to `pip install` example in troubleshooting guide ([#11194](https://github.com/astral-sh/uv/pull/11194)) - Set `VIRTUAL_ENV` in Jupyter kernels ([#11155](https://github.com/astral-sh/uv/pull/11155)) - Add instructions for deactivating an environment ([#11200](https://github.com/astral-sh/uv/pull/11200)) ## 0.5.28 ### Bug fixes - Allow discovering virtual environments from the first interpreter found on the `PATH` ([#11218](https://github.com/astral-sh/uv/pull/11218)) - Clear ephemeral overlays when running tools ([#11141](https://github.com/astral-sh/uv/pull/11141)) - Disable SSL in Git commands for `--allow-insecure-host` ([#11210](https://github.com/astral-sh/uv/pull/11210)) - Fix hardlinks in tar unpacking ([#11221](https://github.com/astral-sh/uv/pull/11221)) - Set base executable when returning virtual environment ([#11209](https://github.com/astral-sh/uv/pull/11209)) - Use base Python for cached environments ([#11208](https://github.com/astral-sh/uv/pull/11208)) ### Documentation - Add documentation on verifying Docker image attestations ([#11140](https://github.com/astral-sh/uv/pull/11140)) - Add `last updated` to documentation ([#11164](https://github.com/astral-sh/uv/pull/11164)) ## 0.5.29 ### Enhancements - Add `--bare` option to `uv init` ([#11192](https://github.com/astral-sh/uv/pull/11192)) - Add support for respecting `VIRTUAL_ENV` in project commands via `--active` ([#11189](https://github.com/astral-sh/uv/pull/11189)) - Allow the project `VIRTUAL_ENV` warning to be silenced with `--no-active` ([#11251](https://github.com/astral-sh/uv/pull/11251)) ### Python The managed Python distributions have been updated, including: - CPython 3.12.9 - CPython 3.13.2 - pkg-config files are now relocatable See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250205) for more details. ### Bug fixes - Always use base Python discovery logic for cached environments ([#11254](https://github.com/astral-sh/uv/pull/11254)) - Use a flock to avoid concurrent initialization of project environments ([#11259](https://github.com/astral-sh/uv/pull/11259)) - Fix handling of `--all-groups` and `--no-default-groups` flags ([#11224](https://github.com/astral-sh/uv/pull/11224)) ### Documentation - Minor touchups to the Docker provenance docs ([#11252](https://github.com/astral-sh/uv/pull/11252)) - Move content from the `mkdocs.public.yml` into the template ([#11246](https://github.com/astral-sh/uv/pull/11246)) ## 0.5.30 ### Python The managed PyPy distributions have been updated for PyPy v7.3.18, which includes: - PyPy3.10, which updates the standard library from Python 3.10.14 to 3.10.19 - PyPy3.11, which adds beta support for Python 3.11.11 See the [PyPy release](https://pypy.org/posts/2025/02/pypy-v7318-release.html) for more details. ### Enhancements - Add `uv sync --dry-run` ([#11299](https://github.com/astral-sh/uv/pull/11299)) - Ignore `#egg` fragment in HTML Simple API response ([#11340](https://github.com/astral-sh/uv/pull/11340)) ### Configuration - Add `NO_BINARY` and `NO_BINARY_PACKAGE` environment variables ([#11399](https://github.com/astral-sh/uv/pull/11399)) ### Performance - Avoid re-cloning name when populating ambiguous set ([#11401](https://github.com/astral-sh/uv/pull/11401)) - Optimize flattening in large workspaces ([#11313](https://github.com/astral-sh/uv/pull/11313)) ### Bug fixes - Allow dynamic packages to be overloaded ([#11400](https://github.com/astral-sh/uv/pull/11400)) - Fix credential caching for index roots when URL ends in `simple/` ([#11336](https://github.com/astral-sh/uv/pull/11336)) - Fix marker merging for requirements.txt for psycopg ([#11298](https://github.com/astral-sh/uv/pull/11298)) - Set 777 permissions on locked files ([#11328](https://github.com/astral-sh/uv/pull/11328)) - Support extras in `@` requests for tools ([#11335](https://github.com/astral-sh/uv/pull/11335)) - Upgrade `astral-tokio-tar` to v0.5.1 ([#11359](https://github.com/astral-sh/uv/pull/11359)) - Avoid missing logging for no-op upgrade events ([#11301](https://github.com/astral-sh/uv/pull/11301)) - Use refined specifiers when logging narrowed Python range ([#11334](https://github.com/astral-sh/uv/pull/11334)) - Don't use popup-generating `eprintln` in trampoline warnings ([#11295](https://github.com/astral-sh/uv/pull/11295)) - Patch pkg-config files to be relocatable ([#11291](https://github.com/astral-sh/uv/pull/11291)) - Fix a case of duplicate `torch` packages when using conflicting extras ([#11323](https://github.com/astral-sh/uv/pull/11323)) ### Documentation - Add docs for `uv tool install --editable` ([#11280](https://github.com/astral-sh/uv/pull/11280)) - Fix broken anchors in README and docs index ([#11338](https://github.com/astral-sh/uv/pull/11338)) ## 0.5.31 ### Enhancements - Add `uv sync --script` ([#11361](https://github.com/astral-sh/uv/pull/11361)) - Allow PEP 508 requirements in tool requests ([#11337](https://github.com/astral-sh/uv/pull/11337)) - Allow source distributions to produce wheels with `+local` suffixes ([#11429](https://github.com/astral-sh/uv/pull/11429)) - Bring parity to `uvx` and `uv tool install` requests ([#11345](https://github.com/astral-sh/uv/pull/11345)) - Use a stable directory for local, remote, and stdin script virtual environments ([#11347](https://github.com/astral-sh/uv/pull/11347), [#11364](https://github.com/astral-sh/uv/pull/11364)) - Detect infinite recursion in `uv run` ([#11386](https://github.com/astral-sh/uv/pull/11386)) ### Python The managed Python distributions have been updated, including: - CPython 3.14.0a5, which includes a new [tail calling interpreter](https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-tail-call) for a significant performance improvement - The bundled OpenSSL version was updated from 3.0.15 to 3.0.16 which fixes a [security advisory](https://openssl-library.org/news/secadv/20241016.txt) See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250212) for more details. ### Bug fixes - Fix cross-drive script installation ([#11167](https://github.com/astral-sh/uv/pull/11167)) - Add indexes in priority order ([#11451](https://github.com/astral-sh/uv/pull/11451)) - Allow `--python ` requests to match existing environments if `sys.executable` is the same file ([#11290](https://github.com/astral-sh/uv/pull/11290)) - Avoid comparing to system site packages in `--dry-run` mode ([#11427](https://github.com/astral-sh/uv/pull/11427)) - Prefer running executables in the environment with `` over `/__main__.py` ([#11431](https://github.com/astral-sh/uv/pull/11431)) - Retry local clones without hardlinks if they fail ([#11421](https://github.com/astral-sh/uv/pull/11421)) ### Documentation - Update alternative-indexes.md to use `UV_INDEX` instead of `UV_EXTRA_INDEX_URL` ([#11381](https://github.com/astral-sh/uv/pull/11381)) - Update scripts guide to include using package indexes ([#11443](https://github.com/astral-sh/uv/pull/11443)) uv-0.9.17+ds1/changelogs/0.6.x.md000066400000000000000000001123711520155276700161510ustar00rootroot00000000000000# Changelog 0.6.x ## 0.6.0 There have been 31 releases and 1135 pull requests since [0.5.0](https://github.com/astral-sh/uv/releases/tag/0.5.0), our last release with breaking changes. As before, we've accumulated various changes that improve correctness and user experience, but could break some workflows. This release contains those changes; many have been marked as breaking out of an abundance of caution. We expect most users to be able to upgrade without making changes. ### Breaking changes - **Create `main.py` instead of `hello.py` in `uv init`** ([#10369](https://github.com/astral-sh/uv/pull/10369)) Previously, `uv init` created a `hello.py` sample file. Now, `uv init` will create `main.py` instead — which aligns with expectations from user feedback. The `--bare` option can be used to avoid creating the file altogether. - **Respect `UV_PYTHON` in `uv python install`** ([#11487](https://github.com/astral-sh/uv/pull/11487)) Previously, `uv python install` did not read this environment variable; now it does. We believe this matches user expectations, however, this will take priority over `.python-version` files which could be considered breaking. - **Set `UV` to the uv executable path** ([#11326](https://github.com/astral-sh/uv/pull/11326)) When uv spawns a subprocess, it will now have the `UV` environment variable set to the `uv` binary path. This change is breaking if you are setting the `UV` environment variable yourself, as we will overwrite its value. Additionally, this change requires marking the uv Rust entrypoint (`uv::main`) as `unsafe` to avoid unsoundness — this is only relevant if you are invoking uv using Rust. See the [Rust documentation](https://doc.rust-lang.org/std/env/fn.set_var.html#safety) for details about the safety of updating a process' environment. - **Error on non-existent extras, e.g., in `uv sync`** ([#11426](https://github.com/astral-sh/uv/pull/11426)) Previously, uv would silently ignore non-existent extras requested on the command-line (e.g., via `uv sync --extra foo`). This is _generally_ correct behavior when resolving requests for package extras, because an extra may be present on one compatible version of a package but not another. However, this flexibility doesn't need to apply to the local project and it's less surprising to error here. - **Error on missing dependency groups when `--frozen` is provided** ([#11499](https://github.com/astral-sh/uv/pull/11499)) Previously, uv would not validate that the requested dependency groups were present in the lockfile when the `--frozen` flag was used. Now, an error will be raised if a requested dependency group is not present. - **Change `-p` to a `--python` alias in `uv pip compile`** ([#11486](https://github.com/astral-sh/uv/pull/11486)) In `uv pip compile`, `-p` was an alias for `--python-version` while everywhere else in uv's interface it is an alias for `--python`. Additionally, `uv pip compile` did not respect the `UV_PYTHON` environment variable. Now, the semantics of this flag have been updated for parity with the rest of the CLI. However, `--python-version` is unique: if we cannot find an interpreter with the given version, we will not fail. Instead, we'll use an alternative interpreter and override its version tags with the requested version during package resolution. This behavior is retained here for backwards compatibility, `--python ` / `-p ` will not fail if the version cannot be found. However, if a specific interpreter is requested, e.g., with `--python ` or `--python pypy`, and cannot be found — uv will exit with an error. The breaking changes here are that `UV_PYTHON` is respected and `--python ` will no longer fail if the version cannot be found. - **Bump `alpine` default tag to 3.21 for derived Docker images** ([#11157](https://github.com/astral-sh/uv/pull/11157)) Alpine 3.21 was released in Dec 2024 and is used in the official Alpine-based Python images. Our `uv:python3.x-alpine` images have been using 3.21 since uv v0.5.8. However, now the `uv:alpine` image will use 3.21 instead of 3.20 and `uv:alpine3.20` will no longer be updated. - **Use files instead of junctions on Windows** ([#11269](https://github.com/astral-sh/uv/pull/11269)) Previously, we used junctions for atomic replacement of cache entries on Windows. Now, we use a file with a pointer to the cache entry instead. This resolves various edge-case behaviors with junctions. These files are only intended to be consumed by uv and the cache version has been bumped. We do not think this change will affect workflows. ### Stabilizations - **`uv publish` is no longer in preview** ([#11032](https://github.com/astral-sh/uv/pull/11032)) This does not come with any behavior changes. You will no longer see an experimental warning when using `uv publish`. See the linked pull request for a report on the stabilization. ### Enhancements - Support `--active` for PEP 723 script environments ([#11433](https://github.com/astral-sh/uv/pull/11433)) - Add `revision` to the lockfile to allow backwards-compatible metadata changes ([#11500](https://github.com/astral-sh/uv/pull/11500)) ### Bug fixes - Avoid reading metadata from `.egg-info` files ([#11395](https://github.com/astral-sh/uv/pull/11395)) - Include archive bucket version in archive pointers ([#11306](https://github.com/astral-sh/uv/pull/11306)) - Omit lockfile version when additional fields are dynamic ([#11468](https://github.com/astral-sh/uv/pull/11468)) - Respect executable name in `uvx --from tool@latest` ([#11465](https://github.com/astral-sh/uv/pull/11465)) ### Documentation - The `CHANGELOG.md` is now split into separate files for each "major" version to fix rendering ([#11510](https://github.com/astral-sh/uv/pull/11510)) ## 0.6.1 ### Enhancements - Allow users to mark platforms as "required" for wheel coverage ([#10067](https://github.com/astral-sh/uv/pull/10067)) - Warn for builds in non-build and workspace root pyproject.toml ([#11394](https://github.com/astral-sh/uv/pull/11394)) ### Bug fixes - Add `--all` to `uvx --reinstall` message ([#11535](https://github.com/astral-sh/uv/pull/11535)) - Fallback to `GET` on HTTP 400 when attempting to use range requests for wheel download ([#11539](https://github.com/astral-sh/uv/pull/11539)) - Prefer local variants in preference selection ([#11546](https://github.com/astral-sh/uv/pull/11546)) - Respect verbatim executable name in `uvx` ([#11524](https://github.com/astral-sh/uv/pull/11524)) ### Documentation - Add documentation for required environments ([#11542](https://github.com/astral-sh/uv/pull/11542)) - Note that `main.py` used to be `hello.py` ([#11519](https://github.com/astral-sh/uv/pull/11519)) ## 0.6.2 ### Enhancements - Add support for constraining build dependencies with `tool.uv.build-constraint-dependencies` ([#11585](https://github.com/astral-sh/uv/pull/11585)) - Sort dependency group keys when adding new group ([#11591](https://github.com/astral-sh/uv/pull/11591)) ### Performance - Use an `Arc` for index URLs ([#11586](https://github.com/astral-sh/uv/pull/11586)) ### Bug fixes - Allow use of x86-64 Python on ARM Windows ([#11625](https://github.com/astral-sh/uv/pull/11625)) - Fix an issue where conflict markers could instigate a very large lock file ([#11293](https://github.com/astral-sh/uv/pull/11293)) - Fix duplicate packages with multiple conflicting extras declared ([#11513](https://github.com/astral-sh/uv/pull/11513)) - Respect color settings for log messages ([#11604](https://github.com/astral-sh/uv/pull/11604)) - Eagerly reject unsupported Git schemes ([#11514](https://github.com/astral-sh/uv/pull/11514)) ### Documentation - Add documentation for specifying Python versions in tool commands ([#11598](https://github.com/astral-sh/uv/pull/11598)) ## 0.6.3 ### Enhancements - Allow quotes around command-line options in `requirement.txt files` ([#11644](https://github.com/astral-sh/uv/pull/11644)) - Initialize PEP 723 script in `uv lock --script` ([#11717](https://github.com/astral-sh/uv/pull/11717)) ### Configuration - Accept multiple `.env` files in `UV_ENV_FILE` ([#11665](https://github.com/astral-sh/uv/pull/11665)) ### Performance - Reduce overhead in converting resolutions ([#11660](https://github.com/astral-sh/uv/pull/11660)) - Use `SmallString` on `Hashes` ([#11756](https://github.com/astral-sh/uv/pull/11756)) - Use a `Box` for `Yanked` on `File` ([#11755](https://github.com/astral-sh/uv/pull/11755)) - Use a `SmallString` for the `Yanked` enum ([#11715](https://github.com/astral-sh/uv/pull/11715)) - Use boxed slices for hash vector ([#11714](https://github.com/astral-sh/uv/pull/11714)) - Use install concurrency for bytecode compilation too ([#11615](https://github.com/astral-sh/uv/pull/11615)) ### Bug fixes - Avoid installing duplicate dependencies across conflicting groups ([#11653](https://github.com/astral-sh/uv/pull/11653)) - Check subdirectory existence after cache heal ([#11719](https://github.com/astral-sh/uv/pull/11719)) - Include uppercase platforms for Windows wheels ([#11681](https://github.com/astral-sh/uv/pull/11681)) - Respect existing PEP 723 script settings in `uv add` ([#11716](https://github.com/astral-sh/uv/pull/11716)) - Reuse refined interpreter to create tool environment ([#11680](https://github.com/astral-sh/uv/pull/11680)) - Skip removed directories during bytecode compilation ([#11633](https://github.com/astral-sh/uv/pull/11633)) - Support conflict markers in `uv export` ([#11643](https://github.com/astral-sh/uv/pull/11643)) - Treat lockfile as outdated if (empty) extras are added ([#11702](https://github.com/astral-sh/uv/pull/11702)) - Display path separators as backslashes on Windows ([#11667](https://github.com/astral-sh/uv/pull/11667)) - Display the built file name instead of the canonicalized name in `uv build` ([#11593](https://github.com/astral-sh/uv/pull/11593)) - Fix message when there are no buildable packages ([#11722](https://github.com/astral-sh/uv/pull/11722)) - Re-allow HTTP schemes for Git dependencies ([#11687](https://github.com/astral-sh/uv/pull/11687)) ### Documentation - Add anchor links to arguments and options in the CLI reference ([#11754](https://github.com/astral-sh/uv/pull/11754)) - Add link to environment marker specification ([#11748](https://github.com/astral-sh/uv/pull/11748)) - Fix missing a closing bracket in the `cache-keys` setting ([#11669](https://github.com/astral-sh/uv/pull/11669)) - Remove the last edited date from documentation pages ([#11753](https://github.com/astral-sh/uv/pull/11753)) - Fix readme typo ([#11742](https://github.com/astral-sh/uv/pull/11742)) ## 0.6.4 ### Enhancements - Upgrade pypy3.10 to v7.3.19 ([#11814](https://github.com/astral-sh/uv/pull/11814)) - Allow configuring log verbosity from the CLI (i.e., `-vvv`) ([#11758](https://github.com/astral-sh/uv/pull/11758)) - Warn when duplicate index names found in single file ([#11824](https://github.com/astral-sh/uv/pull/11824)) ### Bug fixes - Always store registry index on resolution packages ([#11815](https://github.com/astral-sh/uv/pull/11815)) - Avoid error on relative paths in `uv tool uninstall` ([#11889](https://github.com/astral-sh/uv/pull/11889)) - Avoid silently dropping errors in directory enumeration ([#11890](https://github.com/astral-sh/uv/pull/11890)) - Disable interactive git terminal prompts during fetches ([#11744](https://github.com/astral-sh/uv/pull/11744)) - Discover Windows registry (PEP 514) Python versions across 32/64-bit ([#11801](https://github.com/astral-sh/uv/pull/11801)) - Don't panic on Ctrl-C in confirm prompt ([#11706](https://github.com/astral-sh/uv/pull/11706)) - Fix non-directory in workspace on Windows ([#11833](https://github.com/astral-sh/uv/pull/11833)) - Make interpreter caching robust to OS upgrades ([#11875](https://github.com/astral-sh/uv/pull/11875)) - Respect `include-system-site-packages` in layered environments ([#11873](https://github.com/astral-sh/uv/pull/11873)) - Suggest `uv tool update-shell` in PowerShell ([#11846](https://github.com/astral-sh/uv/pull/11846)) - Update code page to `65001` before setting environment variables in virtual environments ([#11831](https://github.com/astral-sh/uv/pull/11831)) - Use hash instead of full wheel name in wheels bucket ([#11738](https://github.com/astral-sh/uv/pull/11738)) - Fix version string truncation while generating cache_key ([#11830](https://github.com/astral-sh/uv/pull/11830)) - Explicitly handle ctrl-c in confirmation prompt instead of using a signal handler ([#11897](https://github.com/astral-sh/uv/pull/11897)) ### Performance - Avoid cloning to string when creating cache path ([#11772](https://github.com/astral-sh/uv/pull/11772)) - Avoid redundant clones in version containment check ([#11767](https://github.com/astral-sh/uv/pull/11767)) - Avoid string allocation when enumerating tool names ([#11910](https://github.com/astral-sh/uv/pull/11910)) - Avoid using owned `String` for package name constructors ([#11768](https://github.com/astral-sh/uv/pull/11768)) - Avoid using owned `String` in deserializers ([#11764](https://github.com/astral-sh/uv/pull/11764)) - Migrate to `zlib-rs` (again) ([#11894](https://github.com/astral-sh/uv/pull/11894)) - Remove unnecessary clones when adding package names ([#11771](https://github.com/astral-sh/uv/pull/11771)) - Skip unquote allocation for non-quoted strings ([#11813](https://github.com/astral-sh/uv/pull/11813)) - Use `SmallString` for filenames and URLs ([#11765](https://github.com/astral-sh/uv/pull/11765)) - Use a Boxed slice for version specifiers ([#11766](https://github.com/astral-sh/uv/pull/11766)) - Use matches over contains for extra value parsing ([#11770](https://github.com/astral-sh/uv/pull/11770)) ### Documentation - Avoid fallback to PyPI in mixed CPU/CUDA example ([#11115](https://github.com/astral-sh/uv/pull/11115)) - Docs: Clarify that setting cache-keys overrides defaults ([#11895](https://github.com/astral-sh/uv/pull/11895)) - Document our MSRV policy ([#11898](https://github.com/astral-sh/uv/pull/11898)) - Fix reference to macOS cache path ([#11845](https://github.com/astral-sh/uv/pull/11845)) - Fix typo in `no_default_groups` documentation and changelog ([#11928](https://github.com/astral-sh/uv/pull/11928)) - Update the "Locking and syncing" page ([#11647](https://github.com/astral-sh/uv/pull/11647)) - Update alternative indexes documentation to use new interface ([#10826](https://github.com/astral-sh/uv/pull/10826)) ## 0.6.5 ### Enhancements - Allow `--constraints` and `--overrides` in `uvx` ([#10207](https://github.com/astral-sh/uv/pull/10207)) - Allow overrides in `satisfies` check for `uv tool run` ([#11994](https://github.com/astral-sh/uv/pull/11994)) - Allow users to set `package = true` on `tool.uv.sources` ([#12014](https://github.com/astral-sh/uv/pull/12014)) - Add support for Windows legacy scripts via `uv run` ([#11888](https://github.com/astral-sh/uv/pull/11888)) - Return error when running uvx with a `.py` script ([#11623](https://github.com/astral-sh/uv/pull/11623)) - Warn user on use of `uvx run` ([#11992](https://github.com/astral-sh/uv/pull/11992)) ### Configuration - Add `NO_BUILD` and `NO_BUILD_PACKAGE` environment variables ([#11968](https://github.com/astral-sh/uv/pull/11968)) ### Performance - Allow overrides in all satisfies checks ([#11995](https://github.com/astral-sh/uv/pull/11995)) - Respect markers on constraints when validating current environment ([#11976](https://github.com/astral-sh/uv/pull/11976)) ### Bug fixes - Compare major-minor specifiers when filtering interpreters ([#11952](https://github.com/astral-sh/uv/pull/11952)) - Fix system site packages detection default ([#11956](https://github.com/astral-sh/uv/pull/11956)) - Invalidate lockfile when empty dependency groups are added or removed ([#12010](https://github.com/astral-sh/uv/pull/12010)) - Remove prepended sys.path ([#11954](https://github.com/astral-sh/uv/pull/11954)) - Fix PyPy Python version label ([#11965](https://github.com/astral-sh/uv/pull/11965)) - Fix error message suggesting `--user` instead of `--username` ([#11947](https://github.com/astral-sh/uv/pull/11947)) ### Preview - Move the uv build backend into a separate, minimal `uv_build` package ([#11446](https://github.com/astral-sh/uv/pull/11446)) ## 0.6.6 ### Python - Add support for dynamic musl Python distributions on x86-64 Linux ([#12121](https://github.com/astral-sh/uv/pull/12121)) - Allow the experimental JIT to be enabled at runtime on Python 3.13 and 3.14 on Linux - Upgrade the build toolchain to LLVM 20, improving performance See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250311) for more details. ### Enhancements - Add `--marker` flag to `uv add` ([#12012](https://github.com/astral-sh/uv/pull/12012)) - Allow overriding module name for uv build backend ([#11884](https://github.com/astral-sh/uv/pull/11884)) - Sync latest Python releases ([#12120](https://github.com/astral-sh/uv/pull/12120)) - Use 'Upload' instead of 'Download' in publish reporter ([#12029](https://github.com/astral-sh/uv/pull/12029)) - Add `[index].authenticate` allowing authentication to be required on an index ([#11896](https://github.com/astral-sh/uv/pull/11896)) - Add support for Windows legacy scripts in `uv tool run` ([#12079](https://github.com/astral-sh/uv/pull/12079)) - Propagate conflicting dependency groups when using `include-group` ([#12005](https://github.com/astral-sh/uv/pull/12005)) - Show ambiguous requirements when `uv add` failed ([#12106](https://github.com/astral-sh/uv/pull/12106)) ### Performance - Cache workspace discovery ([#12096](https://github.com/astral-sh/uv/pull/12096)) - Insert dependencies into fork state prior to fetching metadata ([#12057](https://github.com/astral-sh/uv/pull/12057)) - Remove some allocations from `uv-auth` ([#12077](https://github.com/astral-sh/uv/pull/12077)) ### Bug fixes - Avoid considering `PATH` updated when the `export` is commented in the shellrc ([#12043](https://github.com/astral-sh/uv/pull/12043)) - Fix `uv publish` retry on network failures ([#12041](https://github.com/astral-sh/uv/pull/12041)) - Use a sized stream in `uv publish` to comply with WSGI PyPI server constraints ([#12111](https://github.com/astral-sh/uv/pull/12111)) - Fix `uv python install --reinstall` when the version was not previously installed ([#12124](https://github.com/astral-sh/uv/pull/12124)) ### Preview features - Fix `uv_build` invocation ([#12058](https://github.com/astral-sh/uv/pull/12058)) ### Documentation - Quote versions string in `python-versions.md` ([#12112](https://github.com/astral-sh/uv/pull/12112)) - Fix tool concept page headings ([#12053](https://github.com/astral-sh/uv/pull/12053)) - Update the `[index].authenticate` docs ([#12102](https://github.com/astral-sh/uv/pull/12102)) - Update versioning policy ([#11666](https://github.com/astral-sh/uv/pull/11666)) ## 0.6.7 ### Python - Add CPython 3.14.0a6 - Fix regression where extension modules would use wrong `CXX` compiler on Linux - Enable FTS3 enhanced query syntax for SQLite See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250317) for more details. ### Enhancements - Add support for `-c` constraints in `uv add` ([#12209](https://github.com/astral-sh/uv/pull/12209)) - Add support for `--global` default version in `uv python pin` ([#12115](https://github.com/astral-sh/uv/pull/12115)) - Always reinstall local source trees passed to `uv pip install` ([#12176](https://github.com/astral-sh/uv/pull/12176)) - Render token claims on publish permission error ([#12135](https://github.com/astral-sh/uv/pull/12135)) - Add pip-compatible `--group` flag to `uv pip install` and `uv pip compile` ([#11686](https://github.com/astral-sh/uv/pull/11686)) ### Preview features - Avoid creating duplicate directory entries in built wheels ([#12206](https://github.com/astral-sh/uv/pull/12206)) - Allow overriding module names for editable builds ([#12137](https://github.com/astral-sh/uv/pull/12137)) ### Performance - Avoid replicating core-metadata field on `File` struct ([#12159](https://github.com/astral-sh/uv/pull/12159)) ### Bug fixes - Add `src` to default cache keys ([#12062](https://github.com/astral-sh/uv/pull/12062)) - Discard insufficient fork markers ([#10682](https://github.com/astral-sh/uv/pull/10682)) - Ensure `python pin --global` creates parent directories if missing ([#12180](https://github.com/astral-sh/uv/pull/12180)) - Fix GraalPy abi tag parsing and discovery ([#12154](https://github.com/astral-sh/uv/pull/12154)) - Remove extraneous script packages in `uv sync --script` ([#12158](https://github.com/astral-sh/uv/pull/12158)) - Remove redundant `activate.bat` output ([#12160](https://github.com/astral-sh/uv/pull/12160)) - Avoid subsequent index hint when no versions are available on the first index ([#9332](https://github.com/astral-sh/uv/pull/9332)) - Error on lockfiles with incoherent wheel versions ([#12235](https://github.com/astral-sh/uv/pull/12235)) ### Rust API - Update `BaseClientBuild` to accept custom proxies ([#12232](https://github.com/astral-sh/uv/pull/12232)) ### Documentation - Make testpypi index explicit in example snippet ([#12148](https://github.com/astral-sh/uv/pull/12148)) - Reverse and format the archived changelogs ([#12099](https://github.com/astral-sh/uv/pull/12099)) - Use consistent commas around i.e. and e.g. ([#12157](https://github.com/astral-sh/uv/pull/12157)) - Fix typos in MRE docs ([#12198](https://github.com/astral-sh/uv/pull/12198)) - Fix double space typo ([#12171](https://github.com/astral-sh/uv/pull/12171)) ## 0.6.8 ### Enhancements - Add support for enabling all groups by default with `default-groups = "all"` ([#12289](https://github.com/astral-sh/uv/pull/12289)) - Add simpler `--managed-python` and `--no-managed-python` flags for toggling Python preferences ([#12246](https://github.com/astral-sh/uv/pull/12246)) ### Performance - Avoid allocations for default cache keys ([#12063](https://github.com/astral-sh/uv/pull/12063)) ### Bug fixes - Allow local version mismatches when validating lockfile ([#12285](https://github.com/astral-sh/uv/pull/12285)) - Allow owned string when deserializing `requires-python` ([#12278](https://github.com/astral-sh/uv/pull/12278)) - Make cache errors non-fatal in `Planner::build` ([#12281](https://github.com/astral-sh/uv/pull/12281)) ## 0.6.9 ### Enhancements - Use `keyring --mode creds` when `authenticate = "always"` ([#12316](https://github.com/astral-sh/uv/pull/12316)) - Fail with specific error message when no password is present and `authenticate = "always"` ([#12313](https://github.com/astral-sh/uv/pull/12313)) ### Bug fixes - Add boolish value parser for `UV_MANAGED_PYTHON` flags ([#12345](https://github.com/astral-sh/uv/pull/12345)) - Make deserialization non-fatal when assessing source tree revisions ([#12319](https://github.com/astral-sh/uv/pull/12319)) - Use resolver-returned wheel over alternate cached wheel ([#12301](https://github.com/astral-sh/uv/pull/12301)) ### Documentation - Add experimental `--torch-backend` to the PyTorch guide ([#12317](https://github.com/astral-sh/uv/pull/12317)) - Fix `#keyring-provider` references in alternative index docs ([#12315](https://github.com/astral-sh/uv/pull/12315)) - Fix `--directory` path in examples ([#12165](https://github.com/astral-sh/uv/pull/12165)) ### Preview changes - Automatically infer the PyTorch index via `--torch-backend=auto` ([#12070](https://github.com/astral-sh/uv/pull/12070)) ## 0.6.10 ### Enhancements - Add `uv sync --check` flag ([#12342](https://github.com/astral-sh/uv/pull/12342)) - Add support for Python version requests in `uv python list` ([#12375](https://github.com/astral-sh/uv/pull/12375)) - Support `.env` files in `uv tool run` ([#12386](https://github.com/astral-sh/uv/pull/12386)) - Support `python find --script` ([#11891](https://github.com/astral-sh/uv/pull/11891)) ### Preview features - Check all compatible torch indexes when `--torch-backend` is enabled ([#12385](https://github.com/astral-sh/uv/pull/12385)) ### Performance - Use a boxed slice for extras and groups ([#12391](https://github.com/astral-sh/uv/pull/12391)) - Use small string for index name type ([#12355](https://github.com/astral-sh/uv/pull/12355)) ### Bug fixes - Allow virtual packages with `--no-build` ([#12314](https://github.com/astral-sh/uv/pull/12314)) - Ignore `--find-links` entries for pinned indexes ([#12396](https://github.com/astral-sh/uv/pull/12396)) - Omit wheels from lockfile based on `--exclude-newer` ([#12299](https://github.com/astral-sh/uv/pull/12299)) - Retain end-of-line comment position when adding dependency ([#12360](https://github.com/astral-sh/uv/pull/12360)) - Omit fragment when querying for wheels in Simple HTML API ([#12384](https://github.com/astral-sh/uv/pull/12384)) - Error on missing argument in `requirements.txt` ([#12354](https://github.com/astral-sh/uv/pull/12354)) - Support modules with different casing in build backend ([#12240](https://github.com/astral-sh/uv/pull/12240)) - Add authentication policy support for `pip` commands ([#12470](https://github.com/astral-sh/uv/pull/12470)) ## 0.6.11 ### Enhancements - Add dependents ("via ..." comments) in `uv export` command ([#12350](https://github.com/astral-sh/uv/pull/12350)) - Bump least-recent non-EOL macOS version to 13.0 ([#12518](https://github.com/astral-sh/uv/pull/12518)) - Support `--find-links`-style "flat" indexes in `[[tool.uv.index]]` ([#12407](https://github.com/astral-sh/uv/pull/12407)) - Distinguish between `-q` and `-qq` ([#12300](https://github.com/astral-sh/uv/pull/12300)) ### Configuration - Support `UV_PROJECT` environment to set project directory. ([#12327](https://github.com/astral-sh/uv/pull/12327)) ### Performance - Use a boxed slice for various requirement types ([#12514](https://github.com/astral-sh/uv/pull/12514)) ### Bug fixes - Add a newline after metadata when initializing scripts with other metadata blocks ([#12501](https://github.com/astral-sh/uv/pull/12501)) - Avoid writing empty `requires-python` to script blocks ([#12517](https://github.com/astral-sh/uv/pull/12517)) - Respect build constraints in `uv sync` ([#12502](https://github.com/astral-sh/uv/pull/12502)) - Respect transitive dependencies in `uv tree --only-group` ([#12560](https://github.com/astral-sh/uv/pull/12560)) ## 0.6.12 ### Enhancements - Report the queried executable path in `uv python list` ([#12628](https://github.com/astral-sh/uv/pull/12628)) - Improve archive unpack error messages ([#12627](https://github.com/astral-sh/uv/pull/12627)) ### Bug fixes - Respect `authenticate` when using `explicit = true` ([#12631](https://github.com/astral-sh/uv/pull/12631)) - Normalize extra and group names in `uv add` and `uv remove` ([#12586](https://github.com/astral-sh/uv/pull/12586)) - Enforce CRC-32 checks when unpacking archives ([#12623](https://github.com/astral-sh/uv/pull/12623)) - Fix parsing of `python-platform` in settings files ([#12592](https://github.com/astral-sh/uv/pull/12592)) ### Documentation - Add note about `uv build` to `package = false` ([#12608](https://github.com/astral-sh/uv/pull/12608)) - Add index fallback note to `authenticate = always` documentation ([#12498](https://github.com/astral-sh/uv/pull/12498)) - Fix invalid 'kind' reference in flat index docs ([#12583](https://github.com/astral-sh/uv/pull/12583)) ## 0.6.13 ### Enhancements - Add `--show-version` to `uv python find` ([#12376](https://github.com/astral-sh/uv/pull/12376)) - Remove `--no-config` warning from `uv pip compile` and `uv pip sync` ([#12642](https://github.com/astral-sh/uv/pull/12642)) - Skip repeated directories in `PATH` when searching for Python interpreters ([#12367](https://github.com/astral-sh/uv/pull/12367)) - Unset `SCRIPT_PATH` in relocatable activation script ([#12672](https://github.com/astral-sh/uv/pull/12672)) - Add `UV_PYTHON_DOWNLOADS_JSON_URL` to set custom managed python sources ([#10939](https://github.com/astral-sh/uv/pull/10939)) - Reject `pyproject.toml` files in `uv pip compile -o` ([#12673](https://github.com/astral-sh/uv/pull/12673)) - Respect the `--offline` flag for Git operations ([#12619](https://github.com/astral-sh/uv/pull/12619)) ### Bug fixes - Warn instead of error if CRC appears to be missing ([#12722](https://github.com/astral-sh/uv/pull/12722)) - Avoid infinite loop in `uv export` with conflicts ([#12726](https://github.com/astral-sh/uv/pull/12726)) ### Rust API - Update MSRV to 1.84 ([#12670](https://github.com/astral-sh/uv/pull/12670)) ## 0.6.14 ### Python versions The following Python versions have been added: - CPython 3.13.3 - CPython 3.12.10 - CPython 3.11.12 - CPython 3.10.17 - CPython 3.9.22 See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250409) for more details. ### Enhancements - Add `uv-build` and `uv_build` aliases to `uv init --build-backend` ([#12776](https://github.com/astral-sh/uv/pull/12776)) - Emit dedicated error message for Conda `environment.yml` files ([#12669](https://github.com/astral-sh/uv/pull/12669)) ### Preview features - Build backend: Check module dir exists for sdist build ([#12779](https://github.com/astral-sh/uv/pull/12779)) - Build backend: Fix sdist with long directories ([#12764](https://github.com/astral-sh/uv/pull/12764)) ### Performance - Avoid querying GitHub on repeated install invocations ([#12767](https://github.com/astral-sh/uv/pull/12767)) ### Bug fixes - Error when `tool.uv.sources` is set in system-level configuration file ([#12757](https://github.com/astral-sh/uv/pull/12757)) - Split workspace members onto their own lines in `uv init` ([#12756](https://github.com/astral-sh/uv/pull/12756)) ### Documentation - Add lockfile note about PEP 751 ([#12732](https://github.com/astral-sh/uv/pull/12732)) - Extend the reference documentation for `uv pip sync` ([#12683](https://github.com/astral-sh/uv/pull/12683)) - Fix mismatched pip interface header / nav titles ([#12640](https://github.com/astral-sh/uv/pull/12640)) ## 0.6.15 This release includes preliminary support for the `pylock.toml` file format, as standardized in [PEP 751](https://peps.python.org/pep-0751/). `pylock.toml` is an alternative resolution output format intended to replace `requirements.txt` (e.g., in the context of `uv pip compile`, whereby a "locked" `requirements.txt` file is generated from a set of input requirements). `pylock.toml` is standardized and tool-agnostic, such that in the future, `pylock.toml` files generated by uv could be installed by other tools, and vice versa. As of this release, `pylock.toml` is supported in the following commands: - To export a `uv.lock` to the `pylock.toml` format, run: `uv export -o pylock.toml` - To generate a `pylock.toml` file from a set of requirements, run: `uv pip compile -o pylock.toml requirements.in` - To install from a `pylock.toml` file, run: `uv pip sync pylock.toml` or `uv pip install -r pylock.toml` ### Enhancements - Add PEP 751 support to `uv pip compile` ([#13019](https://github.com/astral-sh/uv/pull/13019)) - Add `uv export` support for PEP 751 ([#12955](https://github.com/astral-sh/uv/pull/12955)) - Accept `requirements.txt` (verbatim) as a format on the CLI ([#12957](https://github.com/astral-sh/uv/pull/12957)) - Add `UV_NO_EDITABLE` environment variable to set `--no-editable` on all invocations ([#12773](https://github.com/astral-sh/uv/pull/12773)) - Add `pylock.toml` to `uv pip install` and `uv pip sync` ([#12992](https://github.com/astral-sh/uv/pull/12992)) - Add a brief sleep before sending `SIGINT` to child processes ([#13018](https://github.com/astral-sh/uv/pull/13018)) - Add upload time to `uv.lock` ([#12968](https://github.com/astral-sh/uv/pull/12968)) - Allow updating Git sources by name ([#12897](https://github.com/astral-sh/uv/pull/12897)) - Cache `which git` in `uv init` ([#12893](https://github.com/astral-sh/uv/pull/12893)) - Enable `--dry-run` with `--locked` / `--frozen` for `uv sync` ([#12778](https://github.com/astral-sh/uv/pull/12778)) - Infer output type in `uv export` ([#12958](https://github.com/astral-sh/uv/pull/12958)) - Make `uv init` resilient against broken git ([#12895](https://github.com/astral-sh/uv/pull/12895)) - Respect build constraints for `uv run --with` dependencies ([#12882](https://github.com/astral-sh/uv/pull/12882)) - Split UV_INDEX on all whitespace ([#12820](https://github.com/astral-sh/uv/pull/12820)) - Support build constraints in `uv tool` and PEP723 scripts. ([#12842](https://github.com/astral-sh/uv/pull/12842)) - Use suffix from `uvx` binary when searching for uv binary ([#12923](https://github.com/astral-sh/uv/pull/12923)) - Update version formatting to use cyan color ([#12943](https://github.com/astral-sh/uv/pull/12943)) - Add debug logs for version file search ([#12951](https://github.com/astral-sh/uv/pull/12951)) - Fix `SourceNotAllowed` error message during Python discovery ([#13012](https://github.com/astral-sh/uv/pull/13012)) - Obfuscate password in credentials debug messages ([#12944](https://github.com/astral-sh/uv/pull/12944)) - Obfuscate possible tokens in URL logs ([#12969](https://github.com/astral-sh/uv/pull/12969)) - Validate that PEP 751 entries don't include multiple sources ([#12993](https://github.com/astral-sh/uv/pull/12993)) ### Preview features - Build backend: Add reference docs and schema ([#12803](https://github.com/astral-sh/uv/pull/12803)) ### Bug fixes - Align supported `config-settings` with example in docs ([#12947](https://github.com/astral-sh/uv/pull/12947)) - Ensure virtual environment is compatible with interpreter on sync ([#12884](https://github.com/astral-sh/uv/pull/12884)) - Fix `PythonDownloadRequest` parsing for partial keys ([#12925](https://github.com/astral-sh/uv/pull/12925)) - Fix pre-release exclusive comparison operator in `uv-pep440` ([#12836](https://github.com/astral-sh/uv/pull/12836)) - Forward additional signals to the child process in `uv run` ([#13017](https://github.com/astral-sh/uv/pull/13017)) - Omit PEP 751 version for source trees ([#13030](https://github.com/astral-sh/uv/pull/13030)) - Patch `CC` and `CCX` entries in sysconfig for cross-compiled `aarch64` Python distributions ([#12239](https://github.com/astral-sh/uv/pull/12239)) - Properly handle authentication for HTTP 302 redirect URLs ([#12920](https://github.com/astral-sh/uv/pull/12920)) - Set 4MB stack size for all threads, introduce `UV_STACK_SIZE` ([#12839](https://github.com/astral-sh/uv/pull/12839)) - Show PyPy downloads during `uv python list` ([#12915](https://github.com/astral-sh/uv/pull/12915)) - Add `subdirectory` to Direct URL for local directories ([#12971](https://github.com/astral-sh/uv/pull/12971)) - Prefer stable releases over pre-releases in `uv python install` ([#12194](https://github.com/astral-sh/uv/pull/12194)) - Write requested Python variant to pin file in `uv init` ([#12870](https://github.com/astral-sh/uv/pull/12870)) ### Documentation - Fix CLI reference with code block ([#12807](https://github.com/astral-sh/uv/pull/12807)) - Fix lockfile note ([#12793](https://github.com/astral-sh/uv/pull/12793)) - Fix typo in a reference ([#12858](https://github.com/astral-sh/uv/pull/12858)) - Improve docs for `uv python list --only-downloads` and `--only-installed` ([#12916](https://github.com/astral-sh/uv/pull/12916)) - Update note on lack of musl distributions to ARM-only ([#12825](https://github.com/astral-sh/uv/pull/12825)) - Add section on shebangs for scripts ([#11553](https://github.com/astral-sh/uv/pull/11553)) - Display aliases for long and short args in the CLI reference ([#12824](https://github.com/astral-sh/uv/pull/12824)) - Fix highlight line in explicit index documentation ([#12887](https://github.com/astral-sh/uv/pull/12887)) - Add explicit source (matching PyTorch guide) ([#12844](https://github.com/astral-sh/uv/pull/12844)) - Fix link to issue ([#12823](https://github.com/astral-sh/uv/pull/12823)) - Fix grammatical error in FastAPI guide ([#12908](https://github.com/astral-sh/uv/pull/12908)) - Add `--locked` to `uv sync` in GitHub Actions guide ([#12819](https://github.com/astral-sh/uv/pull/12819)) - Improve formatting for `"all"` `default-groups` setting documentation ([#12963](https://github.com/astral-sh/uv/pull/12963)) - Replace `--frozen` with `--locked` in Docker integration guide ([#12818](https://github.com/astral-sh/uv/pull/12818)) ## 0.6.16 ### Bug fixes - Revert "Properly handle authentication for 302 redirect URLs" ([#13041](https://github.com/astral-sh/uv/pull/13041)) ## 0.6.17 ### Preview features - Add PyTorch v2.7.0 to GPU backend ([#13072](https://github.com/astral-sh/uv/pull/13072)) ### Bug fixes - Avoid panic for invalid Python versions ([#13077](https://github.com/astral-sh/uv/pull/13077)) - Block scripts from overwriting `python` ([#13051](https://github.com/astral-sh/uv/pull/13051)) - Check distribution names to handle invalid redirects ([#12917](https://github.com/astral-sh/uv/pull/12917)) - Check for mismatched package and distribution names on resolver thread ([#13088](https://github.com/astral-sh/uv/pull/13088)) - Fix panic with invalid last character in PEP 508 name ([#13105](https://github.com/astral-sh/uv/pull/13105)) - Reject `requires-python` even if not listed on the index page ([#13086](https://github.com/astral-sh/uv/pull/13086)) uv-0.9.17+ds1/changelogs/0.7.x.md000066400000000000000000001224441520155276700161540ustar00rootroot00000000000000# Changelog 0.7.x ## 0.7.0 This release contains various changes that improve correctness and user experience, but could break some workflows; many changes have been marked as breaking out of an abundance of caution. We expect most users to be able to upgrade without making changes. ### Breaking changes - **Update `uv version` to display and update project versions ([#12349](https://github.com/astral-sh/uv/pull/12349))** Previously, `uv version` displayed uv's version. Now, `uv version` will display or update the project's version. This interface was [heavily requested](https://github.com/astral-sh/uv/issues/6298) and, after much consideration, we decided that transitioning the top-level command was the best option. Here's a brief example: ```console $ uv init example Initialized project `example` at `./example` $ cd example $ uv version example 0.1.0 $ uv version --bump major example 0.1.0 => 1.0.0 $ uv version --short 1.0.0 ``` If used outside of a project, uv will fallback to showing its own version still: ```console $ uv version warning: failed to read project: No `pyproject.toml` found in current directory or any parent directory running `uv self version` for compatibility with old `uv version` command. this fallback will be removed soon, pass `--preview` to make this an error. uv 0.7.0 (4433f41c9 2025-04-29) ``` As described in the warning, `--preview` can be used to error instead: ```console $ uv version --preview error: No `pyproject.toml` found in current directory or any parent directory ``` The previous functionality of `uv version` was moved to `uv self version`. - **Avoid fallback to subsequent indexes on authentication failure ([#12805](https://github.com/astral-sh/uv/pull/12805))** When using the `first-index` strategy (the default), uv will stop searching indexes for a package once it is found on a single index. Previously, uv considered a package as "missing" from an index during authentication failures, such as an HTTP 401 or HTTP 403 (normally, missing packages are represented by an HTTP 404). This behavior was motivated by unusual responses from some package indexes, but reduces the safety of uv's index strategy when authentication fails. Now, uv will consider an authentication failure as a stop-point when searching for a package across indexes. The `index.ignore-error-codes` option can be used to recover the existing behavior, e.g.: ```toml [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cpu" ignore-error-codes = [401, 403] ``` Since PyTorch's indexes always return a HTTP 403 for missing packages, uv special-cases indexes on the `pytorch.org` domain to ignore that error code by default. - **Require the command in `uvx ` to be available in the Python environment ([#11603](https://github.com/astral-sh/uv/pull/11603))** Previously, `uvx` would attempt to execute a command even if it was not provided by a Python package. For example, if we presume `foo` is an empty Python package which provides no command, `uvx foo` would invoke the `foo` command on the `PATH` (if present). Now, uv will error early if the `foo` executable is not provided by the requested Python package. This check is not enforced when `--from` is used, so patterns like `uvx --from foo bash -c "..."` are still valid. uv also still allows `uvx foo` where the `foo` executable is provided by a dependency of `foo` instead of `foo` itself, as this is fairly common for packages which depend on a dedicated package for their command-line interface. - **Use index URL instead of package URL for keyring credential lookups ([#12651](https://github.com/astral-sh/uv/pull/12651))** When determining credentials for querying a package URL, uv previously sent the full URL to the `keyring` command. However, some keyring plugins expect to receive the _index URL_ (which is usually a parent of the package URL). Now, uv requests credentials for the index URL instead. This behavior matches `pip`. - **Remove `--version` from subcommands ([#13108](https://github.com/astral-sh/uv/pull/13108))** Previously, uv allowed the `--version` flag on arbitrary subcommands, e.g., `uv run --version`. However, the `--version` flag is useful for other operations since uv is a package manager. Consequently, we've removed the `--version` flag from subcommands — it is only available as `uv --version`. - **Omit Python 3.7 downloads from managed versions ([#13022](https://github.com/astral-sh/uv/pull/13022))** Python 3.7 is EOL and not formally supported by uv; however, Python 3.7 was previously available for download on a subset of platforms. - **Reject non-PEP 751 TOML files in install, compile, and export commands ([#13120](https://github.com/astral-sh/uv/pull/13120), [#13119](https://github.com/astral-sh/uv/pull/13119))** Previously, uv treated arbitrary `.toml` files passed to commands (e.g., `uv pip install -r foo.toml` or `uv pip compile -o foo.toml`) as `requirements.txt`-formatted files. Now, uv will error instead. If using PEP 751 lockfiles, use the standardized format for custom names instead, e.g., `pylock.foo.toml`. - **Ignore arbitrary Python requests in version files ([#12909](https://github.com/astral-sh/uv/pull/12909))** uv allows arbitrary strings to be used for Python version requests, in which they are treated as an executable name to search for in the `PATH`. However, using this form of request in `.python-version` files is non-standard and conflicts with `pyenv-virtualenv` which writes environment names to `.python-version` files. In this release, uv will now ignore requests that are arbitrary strings when found in `.python-version` files. - **Error on unknown dependency object specifiers ([12811](https://github.com/astral-sh/uv/pull/12811))** The `[dependency-groups]` entries can include "object specifiers", e.g. `set-phasers-to = ...` in: ```toml [dependency-groups] foo = ["pyparsing"] bar = [{set-phasers-to = "stun"}] ``` However, the only current spec-compliant object specifier is `include-group`. Previously, uv would ignore unknown object specifiers. Now, uv will error. - **Make `--frozen` and `--no-sources` conflicting options ([#12671](https://github.com/astral-sh/uv/pull/12671))** Using `--no-sources` always requires a new resolution and `--frozen` will always fail when used with it. Now, this conflict is encoded in the CLI options for clarity. - **Treat empty `UV_PYTHON_INSTALL_DIR` and `UV_TOOL_DIR` as unset ([#12907](https://github.com/astral-sh/uv/pull/12907), [#12905](https://github.com/astral-sh/uv/pull/12905))** Previously, these variables were treated as set to the current working directory when set to an empty string. Now, uv will ignore these variables when empty. This matches uv's behavior for other environment variables which configure directories. ### Enhancements - Disallow mixing requirements across PyTorch indexes ([#13179](https://github.com/astral-sh/uv/pull/13179)) - Add optional managed Python archive download cache ([#12175](https://github.com/astral-sh/uv/pull/12175)) - Add `poetry-core` as a `uv init` build backend option ([#12781](https://github.com/astral-sh/uv/pull/12781)) - Show tag hints when failing to find a compatible wheel in `pylock.toml` ([#13136](https://github.com/astral-sh/uv/pull/13136)) - Report Python versions in `pyvenv.cfg` version mismatch ([#13027](https://github.com/astral-sh/uv/pull/13027)) ### Bug fixes - Avoid erroring on omitted wheel-only packages in `pylock.toml` ([#13132](https://github.com/astral-sh/uv/pull/13132)) - Fix display name for `uvx --version` ([#13109](https://github.com/astral-sh/uv/pull/13109)) - Restore handling of authentication when encountering redirects ([#13050](https://github.com/astral-sh/uv/pull/13050)) - Respect build options (`--no-binary` et al) in `pylock.toml` ([#13134](https://github.com/astral-sh/uv/pull/13134)) - Use `upload-time` rather than `upload_time` in `uv.lock` ([#13176](https://github.com/astral-sh/uv/pull/13176)) ### Documentation - Changed `fish` completions append `>>` to overwrite `>` ([#13130](https://github.com/astral-sh/uv/pull/13130)) - Add `pylock.toml` mentions where relevant ([#13115](https://github.com/astral-sh/uv/pull/13115)) - Add ROCm example to the PyTorch guide ([#13200](https://github.com/astral-sh/uv/pull/13200)) - Upgrade PyTorch guide to CUDA 12.8 and PyTorch 2.7 ([#13199](https://github.com/astral-sh/uv/pull/13199)) ## 0.7.1 ### Enhancement - Add support for BLAKE2b-256 ([#13204](https://github.com/astral-sh/uv/pull/13204)) ### Bugfix - Revert fix handling of authentication when encountering redirects ([#13215](https://github.com/astral-sh/uv/pull/13215)) ## 0.7.2 ### Enhancements - Improve trace log for retryable errors ([#13228](https://github.com/astral-sh/uv/pull/13228)) - Use "error" instead of "warning" for self-update message ([#13229](https://github.com/astral-sh/uv/pull/13229)) - Error when `uv version` is used with project-specific flags but no project is found ([#13203](https://github.com/astral-sh/uv/pull/13203)) ### Bug fixes - Fix incorrect virtual environment invalidation for pre-release Python versions ([#13234](https://github.com/astral-sh/uv/pull/13234)) - Fix patching of `clang` in managed Python sysconfig ([#13237](https://github.com/astral-sh/uv/pull/13237)) - Respect `--project` in `uv version` ([#13230](https://github.com/astral-sh/uv/pull/13230)) ## 0.7.3 ### Enhancements - Add `--dry-run` support to `uv self update` ([#9829](https://github.com/astral-sh/uv/pull/9829)) - Add `--show-with` to `uv tool list` to list packages included by `--with` ([#13264](https://github.com/astral-sh/uv/pull/13264)) - De-duplicate fetched index URLs ([#13205](https://github.com/astral-sh/uv/pull/13205)) - Support more zip compression formats: bzip2, lzma, xz, zstd ([#13285](https://github.com/astral-sh/uv/pull/13285)) - Add support for downloading GraalPy ([#13172](https://github.com/astral-sh/uv/pull/13172)) - Improve error message when a virtual environment Python symlink is broken ([#12168](https://github.com/astral-sh/uv/pull/12168)) - Use `fs_err` for paths in symlinking errors ([#13303](https://github.com/astral-sh/uv/pull/13303)) - Minify and embed managed Python JSON at compile time ([#12967](https://github.com/astral-sh/uv/pull/12967)) ### Preview features - Build backend: Make preview default and add configuration docs ([#12804](https://github.com/astral-sh/uv/pull/12804)) - Build backend: Allow escaping in globs ([#13313](https://github.com/astral-sh/uv/pull/13313)) - Build backend: Make builds reproducible across operating systems ([#13171](https://github.com/astral-sh/uv/pull/13171)) ### Configuration - Add `python-downloads-json-url` option for `uv.toml` to configure custom Python installations via JSON URL ([#12974](https://github.com/astral-sh/uv/pull/12974)) ### Bug fixes - Check nested IO errors for retries ([#13260](https://github.com/astral-sh/uv/pull/13260)) - Accept `musllinux_1_0` as a valid platform tag ([#13289](https://github.com/astral-sh/uv/pull/13289)) - Fix discovery of pre-release managed Python versions in range requests ([#13330](https://github.com/astral-sh/uv/pull/13330)) - Respect locked script preferences in `uv run --with` ([#13283](https://github.com/astral-sh/uv/pull/13283)) - Retry streaming downloads on broken pipe errors ([#13281](https://github.com/astral-sh/uv/pull/13281)) - Treat already-installed base environment packages as preferences in `uv run --with` ([#13284](https://github.com/astral-sh/uv/pull/13284)) - Avoid enumerating sources in errors for path Python requests ([#13335](https://github.com/astral-sh/uv/pull/13335)) - Avoid re-creating virtual environment with `--no-sync` ([#13287](https://github.com/astral-sh/uv/pull/13287)) ### Documentation - Remove outdated description of index strategy ([#13326](https://github.com/astral-sh/uv/pull/13326)) - Update "Viewing the version" docs ([#13241](https://github.com/astral-sh/uv/pull/13241)) ## 0.7.4 ### Enhancements - Add more context to external errors ([#13351](https://github.com/astral-sh/uv/pull/13351)) - Align indentation of long arguments ([#13394](https://github.com/astral-sh/uv/pull/13394)) - Preserve order of dependencies which are sorted naively ([#13334](https://github.com/astral-sh/uv/pull/13334)) - Align progress bars by largest name length ([#13266](https://github.com/astral-sh/uv/pull/13266)) - Reinstall local packages in `uv add` ([#13462](https://github.com/astral-sh/uv/pull/13462)) - Rename `--raw-sources` to `--raw` ([#13348](https://github.com/astral-sh/uv/pull/13348)) - Show 'Downgraded' when `self update` is used to install an older version ([#13340](https://github.com/astral-sh/uv/pull/13340)) - Suggest `uv self update` if required uv version is newer ([#13305](https://github.com/astral-sh/uv/pull/13305)) - Add 3.14 beta images to uv Docker images ([#13390](https://github.com/astral-sh/uv/pull/13390)) - Add comma after "i.e." in Conda environment error ([#13423](https://github.com/astral-sh/uv/pull/13423)) - Be more precise in unpinned packages warning ([#13426](https://github.com/astral-sh/uv/pull/13426)) - Fix detection of sorted dependencies when include-group is used ([#13354](https://github.com/astral-sh/uv/pull/13354)) - Fix display of HTTP responses in trace logs for retry of errors ([#13339](https://github.com/astral-sh/uv/pull/13339)) - Log skip reasons during Python installation key interpreter match checks ([#13472](https://github.com/astral-sh/uv/pull/13472)) - Redact credentials when displaying URLs ([#13333](https://github.com/astral-sh/uv/pull/13333)) ### Bug fixes - Avoid erroring on `pylock.toml` dependency entries ([#13384](https://github.com/astral-sh/uv/pull/13384)) - Avoid panics for cannot-be-a-base URLs ([#13406](https://github.com/astral-sh/uv/pull/13406)) - Ensure cached realm credentials are applied if no password is found for index URL ([#13463](https://github.com/astral-sh/uv/pull/13463)) - Fix `.tgz` parsing to respect true extension ([#13382](https://github.com/astral-sh/uv/pull/13382)) - Fix double self-dependency ([#13366](https://github.com/astral-sh/uv/pull/13366)) - Reject `pylock.toml` in `uv add -r` ([#13421](https://github.com/astral-sh/uv/pull/13421)) - Retain dot-separated wheel tags during cache prune ([#13379](https://github.com/astral-sh/uv/pull/13379)) - Retain trailing comments after PEP 723 metadata block ([#13460](https://github.com/astral-sh/uv/pull/13460)) ### Documentation - Use "export" instead of "install" in `uv export` arguments ([#13430](https://github.com/astral-sh/uv/pull/13430)) - Remove extra newline ([#13461](https://github.com/astral-sh/uv/pull/13461)) ### Preview features - Build backend: Normalize glob paths ([#13465](https://github.com/astral-sh/uv/pull/13465)) ## 0.7.5 ### Bug fixes - Support case-sensitive module discovery in the build backend ([#13468](https://github.com/astral-sh/uv/pull/13468)) - Bump Simple cache bucket to v16 ([#13498](https://github.com/astral-sh/uv/pull/13498)) - Don't error when the script is too short for the buffer ([#13488](https://github.com/astral-sh/uv/pull/13488)) - Add missing word in "script not supported" error ([#13483](https://github.com/astral-sh/uv/pull/13483)) ## 0.7.6 ### Python - Add Python 3.14 on musl - Add free-threaded Python on musl - Add Python 3.14.0a7 - Statically link `libpython` into the interpreter on Linux for a significant performance boost See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250517) for more details. ### Enhancements - Improve compatibility of `VIRTUAL_ENV_PROMPT` value ([#13501](https://github.com/astral-sh/uv/pull/13501)) - Bump MSRV to 1.85 and Edition 2024 ([#13516](https://github.com/astral-sh/uv/pull/13516)) ### Bug fixes - Respect default extras in uv remove ([#13380](https://github.com/astral-sh/uv/pull/13380)) ### Documentation - Fix PowerShell code blocks ([#13511](https://github.com/astral-sh/uv/pull/13511)) ## 0.7.7 ### Python - Work around third-party packages that (incorrectly) assume the interpreter is dynamically linking libpython - Allow the experimental JIT to be enabled at runtime on Python 3.13 and 3.14 on macOS on aarch64 aka Apple Silicon See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250521) for more details. ### Bug fixes - Make `uv version` lock and sync ([#13317](https://github.com/astral-sh/uv/pull/13317)) - Fix references to `ldd` in diagnostics to correctly refer to `ld.so` ([#13552](https://github.com/astral-sh/uv/pull/13552)) ### Documentation - Clarify adding SSH Git dependencies ([#13534](https://github.com/astral-sh/uv/pull/13534)) ## 0.7.8 ### Python We are reverting most of our Python changes from `uv 0.7.6` and `uv 0.7.7` due to a miscompilation that makes the Python interpreter behave incorrectly, resulting in spurious type-errors involving str. This issue seems to be isolated to x86_64 Linux, and affected at least Python 3.12, 3.13, and 3.14. The following changes that were introduced in those versions of uv are temporarily being reverted while we test and deploy a proper fix for the miscompilation: - Add Python 3.14 on musl - free-threaded Python on musl - Add Python 3.14.0a7 - Statically link `libpython` into the interpreter on Linux for a significant performance boost See [the issue for details](https://github.com/astral-sh/uv/issues/13610). ### Documentation - Remove misleading line in pin documentation ([#13611](https://github.com/astral-sh/uv/pull/13611)) ## 0.7.9 ### Python The changes reverted in [0.7.8](#078) have been restored. See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250529) for more details. ### Enhancements - Improve obfuscation of credentials in URLs ([#13560](https://github.com/astral-sh/uv/pull/13560)) - Allow running non-default Python implementations via `uvx` ([#13583](https://github.com/astral-sh/uv/pull/13583)) - Add `uvw` as alias for `uv` without console window on Windows ([#11786](https://github.com/astral-sh/uv/pull/11786)) - Allow discovery of x86-64 managed Python builds on macOS ([#13722](https://github.com/astral-sh/uv/pull/13722)) - Differentiate between implicit vs explicit architecture requests ([#13723](https://github.com/astral-sh/uv/pull/13723)) - Implement ordering for Python architectures to prefer native installations ([#13709](https://github.com/astral-sh/uv/pull/13709)) - Only show the first match per platform (and architecture) by default in `uv python list` ([#13721](https://github.com/astral-sh/uv/pull/13721)) - Write the path of the parent environment to an `extends-environment` key in the `pyvenv.cfg` file of an ephemeral environment ([#13598](https://github.com/astral-sh/uv/pull/13598)) - Improve the error message when libc cannot be found, e.g., when using the distroless containers ([#13549](https://github.com/astral-sh/uv/pull/13549)) ### Performance - Avoid rendering info log level ([#13642](https://github.com/astral-sh/uv/pull/13642)) - Improve performance of `uv-python` crate's manylinux submodule ([#11131](https://github.com/astral-sh/uv/pull/11131)) - Optimize `Version` display ([#13643](https://github.com/astral-sh/uv/pull/13643)) - Reduce number of reference-checks for `uv cache clean` ([#13669](https://github.com/astral-sh/uv/pull/13669)) ### Bug fixes - Avoid reinstalling dependency group members with `--all-packages` ([#13678](https://github.com/astral-sh/uv/pull/13678)) - Don't fail direct URL hash checking with dependency metadata ([#13736](https://github.com/astral-sh/uv/pull/13736)) - Exit early on `self update` if global `--offline` is set ([#13663](https://github.com/astral-sh/uv/pull/13663)) - Fix cases where the uv lock is incorrectly marked as out of date ([#13635](https://github.com/astral-sh/uv/pull/13635)) - Include pre-release versions in `uv python install --reinstall` ([#13645](https://github.com/astral-sh/uv/pull/13645)) - Set `LC_ALL=C` for git when checking git worktree ([#13637](https://github.com/astral-sh/uv/pull/13637)) - Avoid rejecting Windows paths for remote Python download JSON targets ([#13625](https://github.com/astral-sh/uv/pull/13625)) ### Preview - Add `uv add --bounds` to configure version constraints ([#12946](https://github.com/astral-sh/uv/pull/12946)) ### Documentation - Add documentation about Python versions to Tools concept page ([#7673](https://github.com/astral-sh/uv/pull/7673)) - Add example of enabling Dependabot ([#13692](https://github.com/astral-sh/uv/pull/13692)) - Fix `exclude-newer` date format for persistent configuration files ([#13706](https://github.com/astral-sh/uv/pull/13706)) - Quote versions variables in GitLab documentation ([#13679](https://github.com/astral-sh/uv/pull/13679)) - Update Dependabot support status ([#13690](https://github.com/astral-sh/uv/pull/13690)) - Explicitly specify to add a new repo entry to the repos list item in the `.pre-commit-config.yaml` ([#10243](https://github.com/astral-sh/uv/pull/10243)) - Add integration with marimo guide ([#13691](https://github.com/astral-sh/uv/pull/13691)) - Add pronunciation to README ([#5336](https://github.com/astral-sh/uv/pull/5336)) ## 0.7.10 ### Enhancements - Add `--show-extras` to `uv tool list` ([#13783](https://github.com/astral-sh/uv/pull/13783)) - Add dynamically generated sysconfig replacement mappings ([#13441](https://github.com/astral-sh/uv/pull/13441)) - Add data locations to install wheel logs ([#13797](https://github.com/astral-sh/uv/pull/13797)) ### Bug fixes - Avoid redaction of placeholder `git` username when using SSH authentication ([#13799](https://github.com/astral-sh/uv/pull/13799)) - Propagate credentials to files on devpi indexes ending in `/+simple` ([#13743](https://github.com/astral-sh/uv/pull/13743)) - Restore retention of credentials for direct URLs in `uv export` ([#13809](https://github.com/astral-sh/uv/pull/13809)) ## 0.7.11 ### Python - Add Python 3.14.0b1 - Add Python 3.13.4 - Add Python 3.12.11 - Add Python 3.11.13 - Add Python 3.10.18 - Add Python 3.9.23 ### Enhancements - Add Pyodide support ([#12731](https://github.com/astral-sh/uv/pull/12731)) - Better error message for version specifier with missing operator ([#13803](https://github.com/astral-sh/uv/pull/13803)) ### Bug fixes - Downgrade `reqwest` and `hyper-util` to resolve connection reset errors over IPv6 ([#13835](https://github.com/astral-sh/uv/pull/13835)) - Prefer `uv`'s binary's version when checking if it's up to date ([#13840](https://github.com/astral-sh/uv/pull/13840)) ### Documentation - Use "terminal driver" instead of "shell" in `SIGINT` docs ([#13787](https://github.com/astral-sh/uv/pull/13787)) ## 0.7.12 ### Enhancements - Add `uv python pin --rm` to remove `.python-version` pins ([#13860](https://github.com/astral-sh/uv/pull/13860)) - Don't hint at versions removed by `excluded-newer` ([#13884](https://github.com/astral-sh/uv/pull/13884)) - Add hint to use `tool.uv.environments` on resolution error ([#13455](https://github.com/astral-sh/uv/pull/13455)) - Add hint to use `tool.uv.required-environments` on resolution error ([#13575](https://github.com/astral-sh/uv/pull/13575)) - Improve `python pin` error messages ([#13862](https://github.com/astral-sh/uv/pull/13862)) ### Bug fixes - Lock environments during `uv sync`, `uv add` and `uv remove` to prevent race conditions ([#13869](https://github.com/astral-sh/uv/pull/13869)) - Add `--no-editable` to `uv export` for `pylock.toml` ([#13852](https://github.com/astral-sh/uv/pull/13852)) ### Documentation - List `.gitignore` in project init files ([#13855](https://github.com/astral-sh/uv/pull/13855)) - Move the pip interface documentation into the concepts section ([#13841](https://github.com/astral-sh/uv/pull/13841)) - Remove the configuration section in favor of concepts / reference ([#13842](https://github.com/astral-sh/uv/pull/13842)) - Update Git and GitHub Actions docs to mention `gh auth login` ([#13850](https://github.com/astral-sh/uv/pull/13850)) ### Preview - Fix directory glob traversal fallback preventing exclusion of all files ([#13882](https://github.com/astral-sh/uv/pull/13882)) ## 0.7.13 ### Python - Add Python 3.14.0b2 - Add Python 3.13.5 - Fix stability of `uuid.getnode` on 3.13 See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250612) for more details. ### Enhancements - Download versions in `uv python pin` if not found ([#13946](https://github.com/astral-sh/uv/pull/13946)) - Use TTY detection to determine if SIGINT forwarding is enabled ([#13925](https://github.com/astral-sh/uv/pull/13925)) - Avoid fetching an exact, cached Git commit, even if it isn't locked ([#13748](https://github.com/astral-sh/uv/pull/13748)) - Add `zstd` and `deflate` to `Accept-Encoding` ([#13982](https://github.com/astral-sh/uv/pull/13982)) - Build binaries for riscv64 ([#12688](https://github.com/astral-sh/uv/pull/12688)) ### Bug fixes - Check if relative URL is valid directory before treating as index ([#13917](https://github.com/astral-sh/uv/pull/13917)) - Ignore Python discovery errors during `uv python pin` ([#13944](https://github.com/astral-sh/uv/pull/13944)) - Do not allow `uv add --group ... --script` ([#13997](https://github.com/astral-sh/uv/pull/13997)) ### Preview changes - Build backend: Support namespace packages ([#13833](https://github.com/astral-sh/uv/pull/13833)) ### Documentation - Add 3.14 to the supported platform reference ([#13990](https://github.com/astral-sh/uv/pull/13990)) - Add an `llms.txt` to uv ([#13929](https://github.com/astral-sh/uv/pull/13929)) - Add supported macOS version to the platform reference ([#13993](https://github.com/astral-sh/uv/pull/13993)) - Update platform support reference to include Python implementation list ([#13991](https://github.com/astral-sh/uv/pull/13991)) - Update pytorch.md ([#13899](https://github.com/astral-sh/uv/pull/13899)) - Update the CLI help and reference to include references to the Python bin directory ([#13978](https://github.com/astral-sh/uv/pull/13978)) ## 0.7.14 ### Enhancements - Add XPU to `--torch-backend` ([#14172](https://github.com/astral-sh/uv/pull/14172)) - Add ROCm backends to `--torch-backend` ([#14120](https://github.com/astral-sh/uv/pull/14120)) - Remove preview label from `--torch-backend` ([#14119](https://github.com/astral-sh/uv/pull/14119)) - Add `[tool.uv.dependency-groups].mygroup.requires-python` ([#13735](https://github.com/astral-sh/uv/pull/13735)) - Add auto-detection for AMD GPUs ([#14176](https://github.com/astral-sh/uv/pull/14176)) - Show retries for HTTP status code errors ([#13897](https://github.com/astral-sh/uv/pull/13897)) - Support transparent Python patch version upgrades ([#13954](https://github.com/astral-sh/uv/pull/13954)) - Warn on empty index directory ([#13940](https://github.com/astral-sh/uv/pull/13940)) - Publish to DockerHub ([#14088](https://github.com/astral-sh/uv/pull/14088)) ### Performance - Make cold resolves about 10% faster ([#14035](https://github.com/astral-sh/uv/pull/14035)) ### Bug fixes - Don't use walrus operator in interpreter query script ([#14108](https://github.com/astral-sh/uv/pull/14108)) - Fix handling of changes to `requires-python` ([#14076](https://github.com/astral-sh/uv/pull/14076)) - Fix implied `platform_machine` marker for `win_amd64` platform tag ([#14041](https://github.com/astral-sh/uv/pull/14041)) - Only update existing symlink directories on preview uninstall ([#14179](https://github.com/astral-sh/uv/pull/14179)) - Serialize Python requests for tools as canonicalized strings ([#14109](https://github.com/astral-sh/uv/pull/14109)) - Support netrc and same-origin credential propagation on index redirects ([#14126](https://github.com/astral-sh/uv/pull/14126)) - Support reading `dependency-groups` from pyproject.tomls with no `[project]` ([#13742](https://github.com/astral-sh/uv/pull/13742)) - Handle an existing shebang in `uv init --script` ([#14141](https://github.com/astral-sh/uv/pull/14141)) - Prevent concurrent updates of the environment in `uv run` ([#14153](https://github.com/astral-sh/uv/pull/14153)) - Filter managed Python distributions by platform before querying when included in request ([#13936](https://github.com/astral-sh/uv/pull/13936)) ### Documentation - Replace cuda124 with cuda128 ([#14168](https://github.com/astral-sh/uv/pull/14168)) - Document the way member sources shadow workspace sources ([#14136](https://github.com/astral-sh/uv/pull/14136)) - Sync documented PyTorch integration index for CUDA and ROCm versions from PyTorch website ([#14100](https://github.com/astral-sh/uv/pull/14100)) ## 0.7.15 ### Enhancements - Consistently use `Ordering::Relaxed` for standalone atomic use cases ([#14190](https://github.com/astral-sh/uv/pull/14190)) - Warn on ambiguous relative paths for `--index` ([#14152](https://github.com/astral-sh/uv/pull/14152)) - Skip GitHub fast path when rate-limited ([#13033](https://github.com/astral-sh/uv/pull/13033)) - Preserve newlines in `schema.json` descriptions ([#13693](https://github.com/astral-sh/uv/pull/13693)) ### Bug fixes - Add check for using minor version link when creating a venv on Windows ([#14252](https://github.com/astral-sh/uv/pull/14252)) - Strip query parameters when parsing source URL ([#14224](https://github.com/astral-sh/uv/pull/14224)) ### Documentation - Add a link to PyPI FAQ to clarify what per-project token is ([#14242](https://github.com/astral-sh/uv/pull/14242)) ### Preview features - Allow symlinks in the build backend ([#14212](https://github.com/astral-sh/uv/pull/14212)) ## 0.7.16 ### Python - Add Python 3.14.0b3 See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250626) for more details. ### Enhancements - Include path or URL when failing to convert in lockfile ([#14292](https://github.com/astral-sh/uv/pull/14292)) - Warn when `~=` is used as a Python version specifier without a patch version ([#14008](https://github.com/astral-sh/uv/pull/14008)) ### Preview features - Ensure preview default Python installs are upgradeable ([#14261](https://github.com/astral-sh/uv/pull/14261)) ### Performance - Share workspace cache between lock and sync operations ([#14321](https://github.com/astral-sh/uv/pull/14321)) ### Bug fixes - Allow local indexes to reference remote files ([#14294](https://github.com/astral-sh/uv/pull/14294)) - Avoid rendering desugared prefix matches in error messages ([#14195](https://github.com/astral-sh/uv/pull/14195)) - Avoid using path URL for workspace Git dependencies in `requirements.txt` ([#14288](https://github.com/astral-sh/uv/pull/14288)) - Normalize index URLs to remove trailing slash ([#14245](https://github.com/astral-sh/uv/pull/14245)) - Respect URL-encoded credentials in redirect location ([#14315](https://github.com/astral-sh/uv/pull/14315)) - Lock the source tree when running setuptools, to protect concurrent builds ([#14174](https://github.com/astral-sh/uv/pull/14174)) ### Documentation - Note that GCP Artifact Registry download URLs must have `/simple` component ([#14251](https://github.com/astral-sh/uv/pull/14251)) ## 0.7.17 ### Bug fixes - Apply build constraints when resolving `--with` dependencies ([#14340](https://github.com/astral-sh/uv/pull/14340)) - Drop trailing slashes when converting index URL from URL ([#14346](https://github.com/astral-sh/uv/pull/14346)) - Ignore `UV_PYTHON_CACHE_DIR` when empty ([#14336](https://github.com/astral-sh/uv/pull/14336)) - Fix error message ordering for `pyvenv.cfg` version conflict ([#14329](https://github.com/astral-sh/uv/pull/14329)) ## 0.7.18 ### Python - Added arm64 Windows Python 3.11, 3.12, 3.13, and 3.14 These are not downloaded by default, since x86-64 Python has broader ecosystem support on Windows. However, they can be requested with `cpython--windows-aarch64`. See the [python-build-standalone release](https://github.com/astral-sh/python-build-standalone/releases/tag/20250630) for more details. ### Enhancements - Keep track of retries in `ManagedPythonDownload::fetch_with_retry` ([#14378](https://github.com/astral-sh/uv/pull/14378)) - Reuse build (virtual) environments across resolution and installation ([#14338](https://github.com/astral-sh/uv/pull/14338)) - Improve trace message for cached Python interpreter query ([#14328](https://github.com/astral-sh/uv/pull/14328)) - Use parsed URLs for conflicting URL error message ([#14380](https://github.com/astral-sh/uv/pull/14380)) ### Preview features - Ignore invalid build backend settings when not building ([#14372](https://github.com/astral-sh/uv/pull/14372)) ### Bug fixes - Fix equals-star and tilde-equals with `python_version` and `python_full_version` ([#14271](https://github.com/astral-sh/uv/pull/14271)) - Include the canonical path in the interpreter query cache key ([#14331](https://github.com/astral-sh/uv/pull/14331)) - Only drop build directories on program exit ([#14304](https://github.com/astral-sh/uv/pull/14304)) - Error instead of panic on conflict between global and subcommand flags ([#14368](https://github.com/astral-sh/uv/pull/14368)) - Consistently normalize trailing slashes on URLs with no path segments ([#14349](https://github.com/astral-sh/uv/pull/14349)) ### Documentation - Add instructions for publishing to JFrog's Artifactory ([#14253](https://github.com/astral-sh/uv/pull/14253)) - Edits to the build backend documentation ([#14376](https://github.com/astral-sh/uv/pull/14376)) ## 0.7.19 The **[uv build backend](https://docs.astral.sh/uv/concepts/build-backend/) is now stable**, and considered ready for production use. The uv build backend is a great choice for pure Python projects. It has reasonable defaults, with the goal of requiring zero configuration for most users, but provides flexible configuration to accommodate most Python project structures. It integrates tightly with uv, to improve messaging and user experience. It validates project metadata and structures, preventing common mistakes. And, finally, it's very fast — `uv sync` on a new project (from `uv init`) is 10-30x faster than with other build backends. To use uv as a build backend in an existing project, add `uv_build` to the `[build-system]` section in your `pyproject.toml`: ```toml [build-system] requires = ["uv_build>=0.7.19,<0.8.0"] build-backend = "uv_build" ``` In a future release, it will replace `hatchling` as the default in `uv init`. As before, uv will remain compatible with all standards-compliant build backends. ### Python - Add PGO distributions of Python for aarch64 Linux, which are more optimized for better performance See the [python-build-standalone release](https://github.com/astral-sh/python-build-standalone/releases/tag/20250702) for more details. ### Enhancements - Ignore Python patch version for `--universal` pip compile ([#14405](https://github.com/astral-sh/uv/pull/14405)) - Update the tilde version specifier warning to include more context ([#14335](https://github.com/astral-sh/uv/pull/14335)) - Clarify behavior and hint on tool install when no executables are available ([#14423](https://github.com/astral-sh/uv/pull/14423)) ### Bug fixes - Make project and interpreter lock acquisition non-fatal ([#14404](https://github.com/astral-sh/uv/pull/14404)) - Includes `sys.prefix` in cached environment keys to avoid `--with` collisions across projects ([#14403](https://github.com/astral-sh/uv/pull/14403)) ### Documentation - Add a migration guide from pip to uv projects ([#12382](https://github.com/astral-sh/uv/pull/12382)) ## 0.7.20 ### Python - Add Python 3.14.0b4 - Add zstd support to Python 3.14 on Unix (it already was available on Windows) - Add PyPy 7.3.20 (for Python 3.11.13) See the [PyPy](https://pypy.org/posts/2025/07/pypy-v7320-release.html) and [`python-build-standalone`](https://github.com/astral-sh/python-build-standalone/releases/tag/20250708) release notes for more details. ### Enhancements - Add `--workspace` flag to `uv add` ([#14496](https://github.com/astral-sh/uv/pull/14496)) - Add auto-detection for Intel GPUs ([#14386](https://github.com/astral-sh/uv/pull/14386)) - Drop trailing arguments when writing shebangs ([#14519](https://github.com/astral-sh/uv/pull/14519)) - Add debug message when skipping Python downloads ([#14509](https://github.com/astral-sh/uv/pull/14509)) - Add support for declaring multiple modules in namespace packages ([#14460](https://github.com/astral-sh/uv/pull/14460)) ### Bug fixes - Revert normalization of trailing slashes on index URLs ([#14511](https://github.com/astral-sh/uv/pull/14511)) - Fix forced resolution with all extras in `uv version` ([#14434](https://github.com/astral-sh/uv/pull/14434)) - Fix handling of pre-releases in preferences ([#14498](https://github.com/astral-sh/uv/pull/14498)) - Remove transparent variants in `uv-extract` to enable retries ([#14450](https://github.com/astral-sh/uv/pull/14450)) ### Rust API - Add method to get packages involved in a `NoSolutionError` ([#14457](https://github.com/astral-sh/uv/pull/14457)) - Make `ErrorTree` for `NoSolutionError` public ([#14444](https://github.com/astral-sh/uv/pull/14444)) ### Documentation - Finish incomplete sentence in pip migration guide ([#14432](https://github.com/astral-sh/uv/pull/14432)) - Remove `cache-dependency-glob` examples for `setup-uv` ([#14493](https://github.com/astral-sh/uv/pull/14493)) - Remove `uv pip sync` suggestion with `pyproject.toml` ([#14510](https://github.com/astral-sh/uv/pull/14510)) - Update documentation for GitHub to use `setup-uv@v6` ([#14490](https://github.com/astral-sh/uv/pull/14490)) ## 0.7.21 ### Python - Restore the SQLite `fts4`, `fts5`, `rtree`, and `geopoly` extensions on macOS and Linux See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250712) for more details. ### Enhancements - Add `--python-platform` to `uv sync` ([#14320](https://github.com/astral-sh/uv/pull/14320)) - Support pre-releases in `uv version --bump` ([#13578](https://github.com/astral-sh/uv/pull/13578)) - Add `-w` shorthand for `--with` ([#14530](https://github.com/astral-sh/uv/pull/14530)) - Add an exception handler on Windows to display information on crash ([#14582](https://github.com/astral-sh/uv/pull/14582)) - Add hint when Python downloads are disabled ([#14522](https://github.com/astral-sh/uv/pull/14522)) - Add `UV_HTTP_RETRIES` to customize retry counts ([#14544](https://github.com/astral-sh/uv/pull/14544)) - Follow leaf symlinks matched by globs in `cache-key` ([#13438](https://github.com/astral-sh/uv/pull/13438)) - Support parent path components (`..`) in globs in `cache-key` ([#13469](https://github.com/astral-sh/uv/pull/13469)) - Improve `cache-key` performance ([#13469](https://github.com/astral-sh/uv/pull/13469)) ### Preview features - Add `uv sync --output-format json` ([#13689](https://github.com/astral-sh/uv/pull/13689)) ### Bug fixes - Do not re-resolve with a new Python version in `uv tool` if it is incompatible with `--python` ([#14606](https://github.com/astral-sh/uv/pull/14606)) ### Documentation - Document how to nest dependency groups with `include-group` ([#14539](https://github.com/astral-sh/uv/pull/14539)) - Fix repeated word in Pyodide doc ([#14554](https://github.com/astral-sh/uv/pull/14554)) - Update CONTRIBUTING.md with instructions to format Markdown files via Docker ([#14246](https://github.com/astral-sh/uv/pull/14246)) - Fix version number for `setup-python` ([#14533](https://github.com/astral-sh/uv/pull/14533)) ## 0.7.22 ### Python - Upgrade GraalPy to 24.2.2 See the [GraalPy release notes](https://github.com/oracle/graalpython/releases/tag/graal-24.2.2) for more details. ### Configuration - Add `UV_COMPILE_BYTECODE_TIMEOUT` environment variable ([#14369](https://github.com/astral-sh/uv/pull/14369)) - Allow users to override index `cache-control` headers ([#14620](https://github.com/astral-sh/uv/pull/14620)) - Add `UV_LIBC` to override libc selection in multi-libc environment ([#14646](https://github.com/astral-sh/uv/pull/14646)) ### Bug fixes - Fix `--all-arches` when paired with `--only-downloads` ([#14629](https://github.com/astral-sh/uv/pull/14629)) - Skip Windows Python interpreters that return a broken MSIX package code ([#14636](https://github.com/astral-sh/uv/pull/14636)) - Warn on invalid `uv.toml` when provided via direct path ([#14653](https://github.com/astral-sh/uv/pull/14653)) - Improve async signal safety in Windows exception handler ([#14619](https://github.com/astral-sh/uv/pull/14619)) ### Documentation - Mention the `revision` in the lockfile versioning doc ([#14634](https://github.com/astral-sh/uv/pull/14634)) - Move "Conflicting dependencies" to the "Resolution" page ([#14633](https://github.com/astral-sh/uv/pull/14633)) - Rename "Dependency specifiers" section to exclude PEP 508 reference ([#14631](https://github.com/astral-sh/uv/pull/14631)) - Suggest `uv cache clean` prior to `--reinstall` ([#14659](https://github.com/astral-sh/uv/pull/14659)) ### Preview features - Make preview Python registration on Windows non-fatal ([#14614](https://github.com/astral-sh/uv/pull/14614)) - Update preview installation of Python executables to be non-fatal ([#14612](https://github.com/astral-sh/uv/pull/14612)) - Add `uv python update-shell` ([#14627](https://github.com/astral-sh/uv/pull/14627)) uv-0.9.17+ds1/changelogs/0.8.x.md000066400000000000000000001377461520155276700161700ustar00rootroot00000000000000## 0.8.0 Since we released uv [0.7.0](https://github.com/astral-sh/uv/releases/tag/0.7.0) in April, we've accumulated various changes that improve correctness and user experience, but could break some workflows. This release contains those changes; many have been marked as breaking out of an abundance of caution. We expect most users to be able to upgrade without making changes. This release also includes the stabilization of a couple `uv python install` features, which have been available under preview since late last year. ### Breaking changes - **Install Python executables into a directory on the `PATH` ([#14626](https://github.com/astral-sh/uv/pull/14626))** `uv python install` now installs a versioned Python executable (e.g., `python3.13`) into a directory on the `PATH` (e.g., `~/.local/bin`) by default. This behavior has been available under the `--preview` flag since [Oct 2024](https://github.com/astral-sh/uv/pull/8458). This change should not be breaking unless it shadows a Python executable elsewhere on the `PATH`. To install unversioned executables, i.e., `python3` and `python`, use the `--default` flag. The `--default` flag has also been in preview, but is not stabilized in this release. Note that these executables point to the base Python installation and only include the standard library. That means they will not include dependencies from your current project (use `uv run python` instead) and you cannot install packages into their environment (use `uvx --with python` instead). As with tool installation, the target directory respects common variables like `XDG_BIN_HOME` and can be overridden with a `UV_PYTHON_BIN_DIR` variable. You can opt out of this behavior with `uv python install --no-bin` or `UV_PYTHON_INSTALL_BIN=0`. See the [documentation on installing Python executables](https://docs.astral.sh/uv/concepts/python-versions/#installing-python-executables) for more details. - **Register Python versions with the Windows Registry ([#14625](https://github.com/astral-sh/uv/pull/14625))** `uv python install` now registers the installed Python version with the Windows Registry as specified by [PEP 514](https://peps.python.org/pep-0514/). This allows using uv installed Python versions via the `py` launcher. This behavior has been available under the `--preview` flag since [Jan 2025](https://github.com/astral-sh/uv/pull/10634). This change should not be breaking, as using the uv Python versions with `py` requires explicit opt in. You can opt out of this behavior with `uv python install --no-registry` or `UV_PYTHON_INSTALL_REGISTRY=0`. - **Prompt before removing an existing directory in `uv venv` ([#14309](https://github.com/astral-sh/uv/pull/14309))** Previously, `uv venv` would remove an existing virtual environment without confirmation. While this is consistent with the behavior of project commands (e.g., `uv sync`), it's surprising to users that are using imperative workflows (i.e., `uv pip`). Now, `uv venv` will prompt for confirmation before removing an existing virtual environment. **If not in an interactive context, uv will still remove the virtual environment for backwards compatibility. However, this behavior is likely to change in a future release.** The behavior for other commands (e.g., `uv sync`) is unchanged. You can opt out of this behavior by setting `UV_VENV_CLEAR=1` or passing the `--clear` flag. - **Validate that discovered interpreters meet the Python preference ([#7934](https://github.com/astral-sh/uv/pull/7934))** uv allows opting out of its managed Python versions with the `--no-managed-python` and `python-preference` options. Previously, uv would not enforce this option for Python interpreters discovered on the `PATH`. For example, if a symlink to a managed Python interpreter was created, uv would allow it to be used even if `--no-managed-python` was provided. Now, uv ignores Python interpreters that do not match the Python preference _unless_ they are in an active virtual environment or are explicitly requested, e.g., with `--python /path/to/python3.13`. Similarly, uv would previously not invalidate existing project environments if they did not match the Python preference. Now, uv will invalidate and recreate project environments when the Python preference changes. You can opt out of this behavior by providing the explicit path to the Python interpreter providing `--managed-python` / `--no-managed-python` matching the interpreter you want. - **Install dependencies without build systems when they are `path` sources ([#14413](https://github.com/astral-sh/uv/pull/14413))** When working on a project, uv uses the [presence of a build system](https://docs.astral.sh/uv/concepts/projects/config/#build-systems) to determine if it should be built and installed into the environment. However, when a project is a dependency of another project, it can be surprising for the dependency to be missing from the environment. Previously, uv would not build and install dependencies with [`path` sources](https://docs.astral.sh/uv/concepts/projects/dependencies/#path) unless they declared a build system or set `tool.uv.package = true`. Now, dependencies with `path` sources are built and installed regardless of the presence of a build system. If a build system is not present, the `setuptools.build_meta:__legacy__ ` backend will be used (per [PEP 517](https://peps.python.org/pep-0517/#source-trees)). You can opt out of this behavior by setting `package = false` in the source declaration, e.g.: ```toml [tool.uv.sources] foo = { path = "./foo", package = false } ``` Or, by setting `tool.uv.package = false` in the dependent `pyproject.toml`. See the documentation on [virtual dependencies](https://docs.astral.sh/uv/concepts/projects/dependencies/#virtual-dependencies) for details. - **Install dependencies without build systems when they are workspace members ([#14663](https://github.com/astral-sh/uv/pull/14663))** As described above for dependencies with `path` sources, uv previously would not build and install workspace members that did not declare a build system. Now, uv will build and install workspace members that are a dependency of _another_ workspace member regardless of the presence of a build system. The behavior is unchanged for workspace members that are not included in the `project.dependencies`, `project.optional-dependencies`, or `dependency-groups` tables of another workspace member. You can opt out of this behavior by setting `tool.uv.package = false` in the workspace member's `pyproject.toml`. See the documentation on [virtual dependencies](https://docs.astral.sh/uv/concepts/projects/dependencies/#virtual-dependencies) for details. - **Bump `--python-platform linux` to `manylinux_2_28` ([#14300](https://github.com/astral-sh/uv/pull/14300))** uv allows performing [platform-specific resolution](https://docs.astral.sh/uv/concepts/resolution/#platform-specific-resolution) for explicit targets and provides short aliases, e.g., `linux`, for common targets. Previously, the default target for `--python-platform linux` was `manylinux_2_17`, which is compatible with most Linux distributions from 2014 or newer. We now default to `manylinux_2_28`, which is compatible with most Linux distributions from 2019 or newer. This change follows the lead of other tools, such as `cibuildwheel`, which changed their default to `manylinux_2_28` in [Mar 2025](https://github.com/pypa/cibuildwheel/pull/2330). This change only affects users requesting a specific target platform. Otherwise, uv detects the `manylinux` target from your local glibc version. You can opt out of this behavior by using `--python-platform x86_64-manylinux_2_17` instead. - **Remove `uv version` fallback ([#14161](https://github.com/astral-sh/uv/pull/14161))** In [Apr 2025](https://github.com/astral-sh/uv/pull/12349), uv changed the `uv version` command to an interface for viewing and updating the version of the current project. However, when outside a project, `uv version` would continue to display uv's version for backwards compatibility. Now, when used outside of a project, `uv version` will fail. You cannot opt out of this behavior. Use `uv self version` instead. - **Require `--global` for removal of the global Python pin ([#14169](https://github.com/astral-sh/uv/pull/14169))** Previously, `uv python pin --rm` would allow you to remove the global Python pin without opt in. Now, uv requires the `--global` flag to remove the global Python pin. You cannot opt out of this behavior. Use the `--global` flag instead. - **Support conflicting editable settings across groups ([#14197](https://github.com/astral-sh/uv/pull/14197))** Previously, uv would always treat a package as editable if any requirement requested it as editable. However, this prevented users from declaring `path` sources that toggled the `editable` setting across dependency groups. Now, uv allows declaring different `editable` values for conflicting groups. However, if a project includes a path dependency twice, once with `editable = true` and once without any editable annotation, those are now considered conflicting, and uv will exit with an error. You cannot opt out of this behavior. Use consistent `editable` settings or [mark groups as conflicting](https://docs.astral.sh/uv/concepts/projects/config/#conflicting-dependencies). - **Make `uv_build` the default build backend in `uv init` ([#14661](https://github.com/astral-sh/uv/pull/14661))** The uv build backend (`uv_build`) was [stabilized in uv 0.7.19](https://github.com/astral-sh/uv/releases/tag/0.7.19). Now, it is the default build backend for `uv init --package` and `uv init --lib`. Previously, `hatchling` was the default build backend. A build backend is still not used without opt-in in `uv init`, but we expect to change this in a future release. You can opt out of this behavior with `uv init --build-backend hatchling`. - **Set default `UV_TOOL_BIN_DIR` on Docker images ([#13391](https://github.com/astral-sh/uv/pull/13391))** Previously, `UV_TOOL_BIN_DIR` was not set in Docker images which meant that `uv tool install` did not install tools into a directory on the `PATH` without additional configuration. Now, `UV_TOOL_BIN_DIR` is set to `/usr/local/bin` in all Docker derived images. When the default image user is overridden (e.g. `USER `) with a less privileged user, this may cause `uv tool install` to fail. You can opt out of this behavior by setting an alternative `UV_TOOL_BIN_DIR`. - **Update `--check` to return an exit code of 1 ([#14167](https://github.com/astral-sh/uv/pull/14167))** uv uses an exit code of 1 to indicate a "successful failure" and an exit code of 2 to indicate an "error". Previously, `uv lock --check` and `uv sync --check` would exit with a code of 2 when the lockfile or environment were outdated. Now, uv will exit with a code of 1. You cannot opt out of this behavior. - **Use an ephemeral environment for `uv run --with` invocations ([#14447](https://github.com/astral-sh/uv/pull/14447))** When using `uv run --with`, uv layers the requirements requested using `--with` into another virtual environment and caches it. Previously, uv would invoke the Python interpreter in this layered environment. However, this allows poisoning the cached environment and introduces race conditions for concurrent invocations. Now, uv will layer _another_ empty virtual environment on top of the cached environment and invoke the Python interpreter there. This should only cause breakage in cases where the environment is being inspected at runtime. You cannot opt out of this behavior. - **Restructure the `uv venv` command output and exit codes ([#14546](https://github.com/astral-sh/uv/pull/14546))** Previously, uv used `miette` to format the `uv venv` output. However, this was inconsistent with most of the uv CLI. Now, the output is a little different and the exit code has switched from 1 to 2 for some error cases. You cannot opt out of this behavior. - **Default to `--workspace` when adding subdirectories ([#14529](https://github.com/astral-sh/uv/pull/14529))** When using `uv add` to add a subdirectory in a workspace, uv now defaults to adding the target as a workspace member. You can opt out of this behavior by providing `--no-workspace`. - **Add missing validations for disallowed `uv.toml` fields ([#14322](https://github.com/astral-sh/uv/pull/14322))** uv does not allow some settings in the `uv.toml`. Previously, some settings were silently ignored when present in the `uv.toml`. Now, uv will error. You cannot opt out of this behavior. Use `--no-config` or remove the invalid settings. ### Configuration - Add support for toggling Python bin and registry install options via env vars ([#14662](https://github.com/astral-sh/uv/pull/14662)) ## 0.8.1 ### Enhancements - Add support for `HF_TOKEN` ([#14797](https://github.com/astral-sh/uv/pull/14797)) - Allow `--config-settings-package` to apply configuration settings at the package level ([#14573](https://github.com/astral-sh/uv/pull/14573)) - Create (e.g.) `python3.13t` executables in `uv venv` ([#14764](https://github.com/astral-sh/uv/pull/14764)) - Disallow writing symlinks outside the source distribution target directory ([#12259](https://github.com/astral-sh/uv/pull/12259)) - Elide traceback when `python -m uv` in interrupted with Ctrl-C on Windows ([#14715](https://github.com/astral-sh/uv/pull/14715)) - Match `--bounds` formatting for `uv_build` bounds in `uv init` ([#14731](https://github.com/astral-sh/uv/pull/14731)) - Support `extras` and `dependency_groups` markers in PEP 508 grammar ([#14753](https://github.com/astral-sh/uv/pull/14753)) - Support `extras` and `dependency_groups` markers on `uv pip install` and `uv pip sync` ([#14755](https://github.com/astral-sh/uv/pull/14755)) - Add hint to use `uv self version` when `uv version` cannot find a project ([#14738](https://github.com/astral-sh/uv/pull/14738)) - Improve error reporting when removing Python versions from the Windows registry ([#14722](https://github.com/astral-sh/uv/pull/14722)) - Make warnings about masked `[tool.uv]` fields more precise ([#14325](https://github.com/astral-sh/uv/pull/14325)) ### Preview features - Emit JSON output in `uv sync` with `--quiet` ([#14810](https://github.com/astral-sh/uv/pull/14810)) ### Bug fixes - Allow removal of virtual environments with missing interpreters ([#14812](https://github.com/astral-sh/uv/pull/14812)) - Apply `Cache-Control` overrides to response, not request headers ([#14736](https://github.com/astral-sh/uv/pull/14736)) - Copy entry points into ephemeral environments to ensure layers are respected ([#14790](https://github.com/astral-sh/uv/pull/14790)) - Workaround Jupyter Lab application directory discovery in ephemeral environments ([#14790](https://github.com/astral-sh/uv/pull/14790)) - Enforce `requires-python` in `pylock.toml` ([#14787](https://github.com/astral-sh/uv/pull/14787)) - Fix kebab casing of `README` variants in build backend ([#14762](https://github.com/astral-sh/uv/pull/14762)) - Improve concurrency resilience of removing Python versions from the Windows registry ([#14717](https://github.com/astral-sh/uv/pull/14717)) - Retry HTTP requests on invalid data errors ([#14703](https://github.com/astral-sh/uv/pull/14703)) - Update virtual environment removal to delete `pyvenv.cfg` last ([#14808](https://github.com/astral-sh/uv/pull/14808)) - Error on unknown fields in `dependency-metadata` ([#14801](https://github.com/astral-sh/uv/pull/14801)) ### Documentation - Recommend installing `setup-uv` after `setup-python` in Github Actions integration guide ([#14741](https://github.com/astral-sh/uv/pull/14741)) - Clarify which portions of `requires-python` behavior are consistent with pip ([#14752](https://github.com/astral-sh/uv/pull/14752)) ## 0.8.2 ### Enhancements - Add derivation chains for dependency errors ([#14824](https://github.com/astral-sh/uv/pull/14824)) ### Configuration - Add `UV_INIT_BUILD_BACKEND` ([#14821](https://github.com/astral-sh/uv/pull/14821)) ### Bug fixes - Avoid reading files in the environment bin that are not entrypoints ([#14830](https://github.com/astral-sh/uv/pull/14830)) - Avoid removing empty directories when constructing virtual environments ([#14822](https://github.com/astral-sh/uv/pull/14822)) - Preserve index URL priority order when writing to pyproject.toml ([#14831](https://github.com/astral-sh/uv/pull/14831)) ### Rust API - Expose `tls_built_in_root_certs` for client ([#14816](https://github.com/astral-sh/uv/pull/14816)) ### Documentation - Archive the 0.7.x changelog ([#14819](https://github.com/astral-sh/uv/pull/14819)) ## 0.8.3 ### Python - Add CPython 3.14.0rc1 See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250723) for more details. ### Enhancements - Allow non-standard entrypoint names in `uv_build` ([#14867](https://github.com/astral-sh/uv/pull/14867)) - Publish riscv64 wheels to PyPI ([#14852](https://github.com/astral-sh/uv/pull/14852)) ### Bug fixes - Avoid writing redacted credentials to tool receipt ([#14855](https://github.com/astral-sh/uv/pull/14855)) - Respect `--with` versions over base environment versions ([#14863](https://github.com/astral-sh/uv/pull/14863)) - Respect credentials from all defined indexes ([#14858](https://github.com/astral-sh/uv/pull/14858)) - Fix missed stabilization of removal of registry entry during Python uninstall ([#14859](https://github.com/astral-sh/uv/pull/14859)) - Improve concurrency safety of Python downloads into cache ([#14846](https://github.com/astral-sh/uv/pull/14846)) ### Documentation - Fix typos in `uv_build` reference documentation ([#14853](https://github.com/astral-sh/uv/pull/14853)) - Move the "Cargo" install method further down in docs ([#14842](https://github.com/astral-sh/uv/pull/14842)) ## 0.8.4 ### Enhancements - Improve styling of warning cause chains ([#14934](https://github.com/astral-sh/uv/pull/14934)) - Extend wheel filtering to Android tags ([#14977](https://github.com/astral-sh/uv/pull/14977)) - Perform wheel lockfile filtering based on platform and OS intersection ([#14976](https://github.com/astral-sh/uv/pull/14976)) - Clarify messaging when a new resolution needs to be performed ([#14938](https://github.com/astral-sh/uv/pull/14938)) ### Preview features - Add support for extending package's build dependencies with `extra-build-dependencies` ([#14735](https://github.com/astral-sh/uv/pull/14735)) - Split preview mode into separate feature flags ([#14823](https://github.com/astral-sh/uv/pull/14823)) ### Configuration - Add support for package specific `exclude-newer` dates via `exclude-newer-package` ([#14489](https://github.com/astral-sh/uv/pull/14489)) ### Bug fixes - Avoid invalidating lockfile when path or workspace dependencies define explicit indexes ([#14876](https://github.com/astral-sh/uv/pull/14876)) - Copy entrypoints that have a shebang that differs in `python` vs `python3` ([#14970](https://github.com/astral-sh/uv/pull/14970)) - Fix incorrect file permissions in wheel packages ([#14930](https://github.com/astral-sh/uv/pull/14930)) - Update validation for `environments` and `required-environments` in `uv.toml` ([#14905](https://github.com/astral-sh/uv/pull/14905)) ### Documentation - Show `uv_build` in projects documentation ([#14968](https://github.com/astral-sh/uv/pull/14968)) - Add `UV_` prefix to installer environment variables ([#14964](https://github.com/astral-sh/uv/pull/14964)) - Un-hide `uv` from `--build-backend` options ([#14939](https://github.com/astral-sh/uv/pull/14939)) - Update documentation for preview flags ([#14902](https://github.com/astral-sh/uv/pull/14902)) ## 0.8.5 ### Enhancements - Enable `uv run` with a GitHub Gist ([#15058](https://github.com/astral-sh/uv/pull/15058)) - Improve HTTP response caching log messages ([#15067](https://github.com/astral-sh/uv/pull/15067)) - Show wheel tag hints in install plan ([#15066](https://github.com/astral-sh/uv/pull/15066)) - Support installing additional executables in `uv tool install` ([#14014](https://github.com/astral-sh/uv/pull/14014)) ### Preview features - Enable extra build dependencies to 'match runtime' versions ([#15036](https://github.com/astral-sh/uv/pull/15036)) - Remove duplicate `extra-build-dependencies` warnings for `uv pip` ([#15088](https://github.com/astral-sh/uv/pull/15088)) - Use "option" instead of "setting" in `pylock` warning ([#15089](https://github.com/astral-sh/uv/pull/15089)) - Respect extra build requires when reading from wheel cache ([#15030](https://github.com/astral-sh/uv/pull/15030)) - Preserve lowered extra build dependencies ([#15038](https://github.com/astral-sh/uv/pull/15038)) ### Bug fixes - Add Python versions to markers implied from wheels ([#14913](https://github.com/astral-sh/uv/pull/14913)) - Ensure consistent indentation when adding dependencies ([#14991](https://github.com/astral-sh/uv/pull/14991)) - Fix handling of `python-preference = system` when managed interpreters are on the PATH ([#15059](https://github.com/astral-sh/uv/pull/15059)) - Fix symlink preservation in virtual environment creation ([#14933](https://github.com/astral-sh/uv/pull/14933)) - Gracefully handle entrypoint permission errors ([#15026](https://github.com/astral-sh/uv/pull/15026)) - Include wheel hashes from local Simple indexes ([#14993](https://github.com/astral-sh/uv/pull/14993)) - Prefer system Python installations over managed ones when `--system` is used ([#15061](https://github.com/astral-sh/uv/pull/15061)) - Remove retry wrapper when matching on error kind ([#14996](https://github.com/astral-sh/uv/pull/14996)) - Revert `h2` upgrade ([#15079](https://github.com/astral-sh/uv/pull/15079)) ### Documentation - Improve visibility of copy and line separator in dark mode ([#14987](https://github.com/astral-sh/uv/pull/14987)) ## 0.8.6 This release contains hardening measures to address differentials in behavior between uv and Python's built-in ZIP parser ([CVE-2025-54368](https://github.com/astral-sh/uv/security/advisories/GHSA-8qf3-x8v5-2pj8)). Prior to this release, attackers could construct ZIP files that would be extracted differently by pip, uv, and other tools. As a result, ZIPs could be constructed that would be considered harmless by (e.g.) scanners, but contain a malicious payload when extracted by uv. As of v0.8.6, uv now applies additional checks to reject such ZIPs. Thanks to a triage effort with the [Python Security Response Team](https://devguide.python.org/developer-workflow/psrt/) and PyPI maintainers, we were able to determine that these differentials **were not exploited** via PyPI during the time they were present. The PyPI team has also implemented similar checks and now guards against these parsing differentials on upload. Although the practical risk of exploitation is low, we take the _hypothetical_ risk of parser differentials very seriously. Out of an abundance of caution, we have assigned this advisory a CVE identifier and have given it a "moderate" severity suggestion. These changes have been validated against the top 15,000 PyPI packages; however, it's plausible that a non-malicious ZIP could be falsely rejected with this additional hardening. As an escape hatch, users who do encounter breaking changes can enable `UV_INSECURE_NO_ZIP_VALIDATION` to restore the previous behavior. If you encounter such a rejection, please file an issue in uv and to the upstream package. For additional information, please refer to the following blog posts: - [Astral: uv security advisory: ZIP payload obfuscation](https://astral.sh/blog/uv-security-advisory-cve-2025-54368) - [PyPI: Preventing ZIP parser confusion attacks on Python package installers](https://blog.pypi.org/posts/2025-08-07-wheel-archive-confusion-attacks/) ### Security - Harden ZIP streaming to reject repeated entries and other malformed ZIP files ([#15136](https://github.com/astral-sh/uv/pull/15136)) ### Python - Add CPython 3.13.6 ### Configuration - Add support for per-project build-time environment variables ([#15095](https://github.com/astral-sh/uv/pull/15095)) ### Bug fixes - Avoid invalid simplification with conflict markers ([#15041](https://github.com/astral-sh/uv/pull/15041)) - Respect `UV_HTTP_RETRIES` in `uv publish` ([#15106](https://github.com/astral-sh/uv/pull/15106)) - Support `UV_NO_EDITABLE` where `--no-editable` is supported ([#15107](https://github.com/astral-sh/uv/pull/15107)) - Upgrade `cargo-dist` to add `UV_INSTALLER_URL` to PowerShell installer ([#15114](https://github.com/astral-sh/uv/pull/15114)) - Upgrade `h2` again to avoid `too_many_internal_resets` errors ([#15111](https://github.com/astral-sh/uv/pull/15111)) - Consider `pythonw` when copying entry points in uv run ([#15134](https://github.com/astral-sh/uv/pull/15134)) ### Documentation - Ensure symlink warning is shown ([#15126](https://github.com/astral-sh/uv/pull/15126)) ## 0.8.7 ### Python - On Mac/Linux, libtcl, libtk, and \_tkinter are built as separate shared objects, which fixes matplotlib's `tkagg` backend (the default on Linux), Pillow's `PIL.ImageTk` library, and other extension modules that need to use libtcl/libtk directly. - Tix is no longer provided on Linux. This is a deprecated Tk extension that appears to have been previously broken. See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250808) for details. ### Enhancements - Do not update `uv.lock` when using `--isolated` ([#15154](https://github.com/astral-sh/uv/pull/15154)) - Add support for `--prefix` and `--with` installations in `find_uv_bin` ([#14184](https://github.com/astral-sh/uv/pull/14184)) - Add support for discovering base prefix installations in `find_uv_bin` ([#14181](https://github.com/astral-sh/uv/pull/14181)) - Improve error messages in `find_uv_bin` ([#14182](https://github.com/astral-sh/uv/pull/14182)) - Warn when two packages write to the same module ([#13437](https://github.com/astral-sh/uv/pull/13437)) ### Preview features - Add support for `package`-level conflicts in workspaces ([#14906](https://github.com/astral-sh/uv/pull/14906)) ### Configuration - Add `UV_DEV` and `UV_NO_DEV` environment variables (for `--dev` and `--no-dev`) ([#15010](https://github.com/astral-sh/uv/pull/15010)) ### Bug fixes - Fix regression where `--require-hashes` applied to build dependencies in `uv pip install` ([#15153](https://github.com/astral-sh/uv/pull/15153)) - Ignore GraalPy devtags ([#15013](https://github.com/astral-sh/uv/pull/15013)) - Include all site packages directories in ephemeral environment overlays ([#15121](https://github.com/astral-sh/uv/pull/15121)) - Search in the user scheme scripts directory last in `find_uv_bin` ([#14191](https://github.com/astral-sh/uv/pull/14191)) ### Documentation - Add missing periods (`.`) to list elements in `Features` docs page ([#15138](https://github.com/astral-sh/uv/pull/15138)) ## 0.8.8 ### Bug fixes - Fix `find_uv_bin` compatibility with Python <3.10 ([#15177](https://github.com/astral-sh/uv/pull/15177)) ## 0.8.9 ### Enhancements - Add `--reinstall` flag to `uv python upgrade` ([#15194](https://github.com/astral-sh/uv/pull/15194)) ### Bug fixes - Include build settings in cache key for registry source distribution lookups ([#15225](https://github.com/astral-sh/uv/pull/15225)) - Avoid creating bin links on `uv python upgrade` if they don't already exist ([#15192](https://github.com/astral-sh/uv/pull/15192)) - Respect system proxies on macOS and Windows ([#15221](https://github.com/astral-sh/uv/pull/15221)) ### Documentation - Add the 3.14 classifier ([#15187](https://github.com/astral-sh/uv/pull/15187)) ## 0.8.10 ### Python - Add support for installing Pyodide versions ([#14518](https://github.com/astral-sh/uv/pull/14518)) ### Enhancements - Allow Python requests with missing segments, e.g., just `aarch64` ([#14399](https://github.com/astral-sh/uv/pull/14399)) ### Preview - Move warnings for conflicting modules into preview ([#15253](https://github.com/astral-sh/uv/pull/15253)) ## 0.8.11 ### Python - Add Python 3.14.0rc2 - Update Pyodide to 0.28.1 ### Enhancements - Add Debian 13 trixie to published Docker images ([#15269](https://github.com/astral-sh/uv/pull/15269)) - Add `extra-build-dependencies` hint for any missing module on build failure ([#15252](https://github.com/astral-sh/uv/pull/15252)) - Make 'v' prefix cyan in overlap warnings ([#15259](https://github.com/astral-sh/uv/pull/15259)) ### Bug fixes - Fix missing uv version in extended Docker image tags ([#15263](https://github.com/astral-sh/uv/pull/15263)) - Persist cache info when re-installing cached wheels ([#15274](https://github.com/astral-sh/uv/pull/15274)) ### Rust API - Allow passing custom `reqwest` clients to `RegistryClient` ([#15281](https://github.com/astral-sh/uv/pull/15281)) ## 0.8.12 ### Python - Add 3.13.7 - Improve performance of zstd in Python 3.14 See the [python-build-standalone release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250818) for details. ### Enhancements - Add an `aarch64-pc-windows-msvc` target for `python-platform` ([#15347](https://github.com/astral-sh/uv/pull/15347)) - Add fallback parent process detection to `uv tool update-shell` ([#15356](https://github.com/astral-sh/uv/pull/15356)) - Install non-build-isolation packages in a second phase ([#15306](https://github.com/astral-sh/uv/pull/15306)) - Add hint when virtual environments are included in source distributions ([#15202](https://github.com/astral-sh/uv/pull/15202)) - Add Docker images derived from `buildpack-deps:trixie`, `debian:trixie-slim`, `alpine:3.22` ([#15351](https://github.com/astral-sh/uv/pull/15351)) ### Bug fixes - Reject already-installed wheels built with outdated settings ([#15289](https://github.com/astral-sh/uv/pull/15289)) - Skip interpreters that are not found on query ([#15315](https://github.com/astral-sh/uv/pull/15315)) - Handle dotted package names in script path resolution ([#15300](https://github.com/astral-sh/uv/pull/15300)) - Reject `match-runtime = true` for dynamic packages ([#15292](https://github.com/astral-sh/uv/pull/15292)) ### Documentation - Document improvements to build-isolation setups ([#15326](https://github.com/astral-sh/uv/pull/15326)) - Fix reference documentation recommendation to use `uv cache clean` instead of `clear` ([#15313](https://github.com/astral-sh/uv/pull/15313)) ## 0.8.13 ### Enhancements - Add `--no-install-*` arguments to `uv add` ([#15375](https://github.com/astral-sh/uv/pull/15375)) - Initialize Git prior to reading author in `uv init` ([#15377](https://github.com/astral-sh/uv/pull/15377)) - Add CUDA 129 to available torch backends ([#15416](https://github.com/astral-sh/uv/pull/15416)) - Update Pyodide to 0.28.2 ([#15385](https://github.com/astral-sh/uv/pull/15385)) ### Preview features - Add an experimental `uv format` command ([#15017](https://github.com/astral-sh/uv/pull/15017)) - Allow version specifiers in `extra-build-dependencies` if match-runtime is explicitly `false` ([#15420](https://github.com/astral-sh/uv/pull/15420)) ### Bug fixes - Add `triton` to `torch-backend` manifest ([#15405](https://github.com/astral-sh/uv/pull/15405)) - Avoid panicking when resolver returns stale distributions ([#15389](https://github.com/astral-sh/uv/pull/15389)) - Fix `uv_build` wheel hashes ([#15400](https://github.com/astral-sh/uv/pull/15400)) - Treat `--upgrade-package` on the command-line as overriding `upgrade = false` in configuration ([#15395](https://github.com/astral-sh/uv/pull/15395)) - Restore DockerHub publishing ([#15381](https://github.com/astral-sh/uv/pull/15381)) ## 0.8.14 ### Python - Add managed CPython distributions for aarch64 musl ### Enhancements - Add `--python-platform` to `uv pip check` ([#15486](https://github.com/astral-sh/uv/pull/15486)) - Add an environment variable for `UV_ISOLATED` ([#15428](https://github.com/astral-sh/uv/pull/15428)) - Add logging to the uv build backend ([#15533](https://github.com/astral-sh/uv/pull/15533)) - Allow more trailing null bytes in zip files ([#15452](https://github.com/astral-sh/uv/pull/15452)) - Allow pinning managed Python versions to specific build versions ([#15314](https://github.com/astral-sh/uv/pull/15314)) - Cache PyTorch wheels by default ([#15481](https://github.com/astral-sh/uv/pull/15481)) - Reject already-installed wheels that don't match the target platform ([#15484](https://github.com/astral-sh/uv/pull/15484)) - Add `--no-install-local` option to `uv sync`, `uv add` and `uv export` ([#15328](https://github.com/astral-sh/uv/pull/15328)) - Include cycle error message in `uv pip` CLI ([#15453](https://github.com/astral-sh/uv/pull/15453)) ### Preview features - Fix format of `{version}` on `uv format` failure ([#15527](https://github.com/astral-sh/uv/pull/15527)) - Lock during installs in `uv format` to prevent races ([#15551](https://github.com/astral-sh/uv/pull/15551)) - Respect `--project` in `uv format` ([#15438](https://github.com/astral-sh/uv/pull/15438)) - Run `uv format` in the project root ([#15440](https://github.com/astral-sh/uv/pull/15440)) ### Configuration - Add file-to-CLI overrides for build isolation configuration ([#15437](https://github.com/astral-sh/uv/pull/15437)) - Add file-to-CLI overrides for reinstall configuration ([#15426](https://github.com/astral-sh/uv/pull/15426)) ### Performance - Cache `WHEEL` and `METADATA` reads in installed distributions ([#15489](https://github.com/astral-sh/uv/pull/15489)) ### Bug fixes - Avoid erroring when creating `venv` in current working directory ([#15537](https://github.com/astral-sh/uv/pull/15537)) - Avoid introducing unnecessary system dependency on CUDA ([#15449](https://github.com/astral-sh/uv/pull/15449)) - Clear discovered site packages when creating virtual environment ([#15522](https://github.com/astral-sh/uv/pull/15522)) - Read index credentials from the environment during `uv publish` checks ([#15545](https://github.com/astral-sh/uv/pull/15545)) - Refuse to remove non-virtual environments in `uv venv` ([#15538](https://github.com/astral-sh/uv/pull/15538)) - Stop setting `CLICOLOR_FORCE=1` when calling build backends ([#15472](https://github.com/astral-sh/uv/pull/15472)) - Support file or directory removal for Windows symlinks ([#15543](https://github.com/astral-sh/uv/pull/15543)) ### Documentation - Fix GitHub guide highlight lines ([#15443](https://github.com/astral-sh/uv/pull/15443)) - Move Resolver to new Internals section in the Reference ([#15465](https://github.com/astral-sh/uv/pull/15465)) - Split the "Authentication" page into sections ([#15575](https://github.com/astral-sh/uv/pull/15575)) - Update uninstall docs to mention `uvw.exe` needs to be removed ([#15536](https://github.com/astral-sh/uv/pull/15536)) ## 0.8.15 ### Python - Upgrade SQLite 3.50.4 in CPython builds for [CVE-2025-6965](https://github.com/advisories/GHSA-2m69-gcr7-jv3q) (see also [python/cpython#137134](https://github.com/python/cpython/issues/137134)) ### Enhancements - Add `uv auth` commands for credential management ([#15570](https://github.com/astral-sh/uv/pull/15570)) - Add pyx support to `uv auth` commands ([#15636](https://github.com/astral-sh/uv/pull/15636)) - Add `uv tree --show-sizes` to show package sizes ([#15531](https://github.com/astral-sh/uv/pull/15531)) - Add `--python-platform riscv64-unknown-linux` ([#15630](https://github.com/astral-sh/uv/pull/15630)) - Add `--python-platform` to `uv run` and `uv tool` ([#15515](https://github.com/astral-sh/uv/pull/15515)) - Add `uv publish --dry-run` ([#15638](https://github.com/astral-sh/uv/pull/15638)) - Add zstandard support for wheels ([#15645](https://github.com/astral-sh/uv/pull/15645)) - Allow registries to pre-provide core metadata ([#15644](https://github.com/astral-sh/uv/pull/15644)) - Retry streaming Python and binary download errors ([#15567](https://github.com/astral-sh/uv/pull/15567)) ### Bug fixes - Fix settings rendering for `extra-build-dependencies` ([#15622](https://github.com/astral-sh/uv/pull/15622)) - Skip non-existent directories in bytecode compilation ([#15608](https://github.com/astral-sh/uv/pull/15608)) ### Error messages - Add error trace to invalid package format ([#15626](https://github.com/astral-sh/uv/pull/15626)) ## 0.8.16 ### Enhancements - Allow `--editable` to override `editable = false` annotations ([#15712](https://github.com/astral-sh/uv/pull/15712)) - Allow `editable = false` for workspace sources ([#15708](https://github.com/astral-sh/uv/pull/15708)) - Show a dedicated error for virtual environments in source trees on build ([#15748](https://github.com/astral-sh/uv/pull/15748)) - Support Android platform tags ([#15646](https://github.com/astral-sh/uv/pull/15646)) - Support iOS platform tags ([#15640](https://github.com/astral-sh/uv/pull/15640)) - Support scripts with inline metadata in `--with-requirements` and `--requirements` ([#12763](https://github.com/astral-sh/uv/pull/12763)) ### Preview features - Support `--no-project` in `uv format` ([#15572](https://github.com/astral-sh/uv/pull/15572)) - Allow `uv format` in unmanaged projects ([#15553](https://github.com/astral-sh/uv/pull/15553)) ### Bug fixes - Avoid erroring when `match-runtime` target is optional ([#15671](https://github.com/astral-sh/uv/pull/15671)) - Ban empty usernames and passwords in `uv auth` ([#15743](https://github.com/astral-sh/uv/pull/15743)) - Error early for parent path in build backend ([#15733](https://github.com/astral-sh/uv/pull/15733)) - Retry on IO errors during HTTP/2 streaming ([#15675](https://github.com/astral-sh/uv/pull/15675)) - Support recursive requirements and constraints inclusion ([#15657](https://github.com/astral-sh/uv/pull/15657)) - Use token store credentials for `uv publish` ([#15759](https://github.com/astral-sh/uv/pull/15759)) - Fix virtual environment activation script compatibility with latest nushell ([#15272](https://github.com/astral-sh/uv/pull/15272)) - Skip Python interpreters that cannot be queried with permission errors ([#15685](https://github.com/astral-sh/uv/pull/15685)) ### Documentation - Clarify that `uv auth` commands take a URL ([#15664](https://github.com/astral-sh/uv/pull/15664)) - Improve the CLI help for options that accept requirements files ([#15706](https://github.com/astral-sh/uv/pull/15706)) - Adds example for caching for managed Python downloads in Docker builds ([#15689](https://github.com/astral-sh/uv/pull/15689)) ## 0.8.17 Released on 2025-09-10. ### Enhancements - Improve error message for HTTP validation in auth services ([#15768](https://github.com/astral-sh/uv/pull/15768)) - Respect `PYX_API_URL` when suggesting `uv auth login` on 401 ([#15774](https://github.com/astral-sh/uv/pull/15774)) - Add pyx as a supported PyTorch index URL ([#15769](https://github.com/astral-sh/uv/pull/15769)) ### Bug fixes - Avoid initiating login flow for invalid API keys ([#15773](https://github.com/astral-sh/uv/pull/15773)) - Do not search for a password for requests with a token attached already ([#15772](https://github.com/astral-sh/uv/pull/15772)) - Filter pre-release Python versions in `uv init --script` ([#15747](https://github.com/astral-sh/uv/pull/15747)) ## 0.8.18 Released on 2025-09-17. ### Enhancements - Add PyG packages to torch backend ([#15911](https://github.com/astral-sh/uv/pull/15911)) - Add handling for unnamed conda environments in base environment detection ([#15681](https://github.com/astral-sh/uv/pull/15681)) - Allow selection of debug build interpreters ([#11520](https://github.com/astral-sh/uv/pull/11520)) - Improve `uv init` defaults for native build backend cache keys ([#15705](https://github.com/astral-sh/uv/pull/15705)) - Error when `pyproject.toml` target does not exist for dependency groups ([#15831](https://github.com/astral-sh/uv/pull/15831)) - Infer check URL from publish URL when known ([#15886](https://github.com/astral-sh/uv/pull/15886)) - Support Gitlab CI/CD as a trusted publisher ([#15583](https://github.com/astral-sh/uv/pull/15583)) - Add GraalPy 25.0.0 with support for Python 3.12 ([#15900](https://github.com/astral-sh/uv/pull/15900)) - Add `--no-clear` to `uv venv` to disable removal prompts ([#15795](https://github.com/astral-sh/uv/pull/15795)) - Add conflict detection between `--only-group` and `--extra` flags ([#15788](https://github.com/astral-sh/uv/pull/15788)) - Allow `[project]` to be missing from a `pyproject.toml` ([#14113](https://github.com/astral-sh/uv/pull/14113)) - Always treat conda environments named `base` and `root` as base environments ([#15682](https://github.com/astral-sh/uv/pull/15682)) - Improve log message when direct build for `uv_build` is skipped ([#15898](https://github.com/astral-sh/uv/pull/15898)) - Log when the cache is disabled ([#15828](https://github.com/astral-sh/uv/pull/15828)) - Show pyx organization name after authenticating ([#15823](https://github.com/astral-sh/uv/pull/15823)) - Use `_CONDA_ROOT` to detect Conda base environments ([#15680](https://github.com/astral-sh/uv/pull/15680)) - Include blake2b hash in `uv publish` upload form ([#15794](https://github.com/astral-sh/uv/pull/15794)) - Fix misleading debug message when removing environments in `uv sync` ([#15881](https://github.com/astral-sh/uv/pull/15881)) ### Deprecations - Deprecate `tool.uv.dev-dependencies` ([#15469](https://github.com/astral-sh/uv/pull/15469)) - Revert "feat(ci): build loongarch64 binaries in CI (#15387)" ([#15820](https://github.com/astral-sh/uv/pull/15820)) ### Preview features - Propagate preview flag to client for `native-auth` feature ([#15872](https://github.com/astral-sh/uv/pull/15872)) - Store native credentials for realms with the https scheme stripped ([#15879](https://github.com/astral-sh/uv/pull/15879)) - Use the root index URL when retrieving credentials from the native store ([#15873](https://github.com/astral-sh/uv/pull/15873)) ### Bug fixes - Fix `uv sync --no-sources` not switching from editable to registry installations ([#15234](https://github.com/astral-sh/uv/pull/15234)) - Avoid display of an empty string when a path is the working directory ([#15897](https://github.com/astral-sh/uv/pull/15897)) - Allow cached environment reuse with `@latest` ([#15827](https://github.com/astral-sh/uv/pull/15827)) - Allow escaping spaces in --env-file handling ([#15815](https://github.com/astral-sh/uv/pull/15815)) - Avoid ANSI codes in debug! messages ([#15843](https://github.com/astral-sh/uv/pull/15843)) - Improve BSD tag construction ([#15829](https://github.com/astral-sh/uv/pull/15829)) - Include SHA when listing lockfile changes ([#15817](https://github.com/astral-sh/uv/pull/15817)) - Invert the logic for determining if a path is a base conda environment ([#15679](https://github.com/astral-sh/uv/pull/15679)) - Load credentials for explicit members when lowering ([#15844](https://github.com/astral-sh/uv/pull/15844)) - Re-add `triton` as a torch backend package ([#15910](https://github.com/astral-sh/uv/pull/15910)) - Respect `UV_INSECURE_NO_ZIP_VALIDATION=1` in duplicate header errors ([#15912](https://github.com/astral-sh/uv/pull/15912)) ### Documentation - Add GitHub Actions to PyPI trusted publishing example ([#15753](https://github.com/astral-sh/uv/pull/15753)) - Add Coiled integration documentation ([#14430](https://github.com/astral-sh/uv/pull/14430)) - Add verbose output to the getting help section ([#15915](https://github.com/astral-sh/uv/pull/15915)) - Document `NO_PROXY` support ([#15816](https://github.com/astral-sh/uv/pull/15816)) - Document cache-keys for native build backends ([#15811](https://github.com/astral-sh/uv/pull/15811)) - Add documentation for dependency group `requires-python` ([#14282](https://github.com/astral-sh/uv/pull/14282)) ## 0.8.19 Released on 2025-09-19. ### Python - Add CPython 3.14.0rc3 - Upgrade OpenSSL to 3.5.3 See the [python-build-standalone release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250918) for more details. ### Bug fixes - Make `uv cache clean` parallel process safe ([#15888](https://github.com/astral-sh/uv/pull/15888)) - Fix implied `platform_machine` marker for `win_arm64` platform tag ([#15921](https://github.com/astral-sh/uv/pull/15921)) ## 0.8.20 Released on 2025-09-22. ### Enhancements - Add `--force` flag for `uv cache clean` ([#15992](https://github.com/astral-sh/uv/pull/15992)) - Improve resolution errors with proxied packages ([#15200](https://github.com/astral-sh/uv/pull/15200)) ### Preview features - Allow upgrading pre-release versions of the same minor Python version ([#15959](https://github.com/astral-sh/uv/pull/15959)) ### Bug fixes - Hide `freethreaded+debug` Python downloads in `uv python list` ([#15985](https://github.com/astral-sh/uv/pull/15985)) - Retain the cache lock and temporary caches during `uv run` and `uvx` ([#15990](https://github.com/astral-sh/uv/pull/15990)) ### Documentation - Add `package` level conflicts to the conflicting dependencies docs ([#15963](https://github.com/astral-sh/uv/pull/15963)) - Document pyodide support ([#15962](https://github.com/astral-sh/uv/pull/15962)) - Document support for free-threaded and debug Python versions ([#15961](https://github.com/astral-sh/uv/pull/15961)) - Expand the contribution docs on issue selection ([#15966](https://github.com/astral-sh/uv/pull/15966)) - Tweak title for viewing version in project guide ([#15964](https://github.com/astral-sh/uv/pull/15964)) ## 0.8.21 Released on 2025-09-23. ### Enhancements - Refresh lockfile when `--refresh` is provided ([#15994](https://github.com/astral-sh/uv/pull/15994)) ### Preview features - Add support for S3 request signing ([#15925](https://github.com/astral-sh/uv/pull/15925)) ## 0.8.22 Released on 2025-09-23. ### Python - Upgrade Pyodide to 0.28.3 ([#15999](https://github.com/astral-sh/uv/pull/15999)) ### Security - Upgrade `astral-tokio-tar` to 0.5.5 which [hardens tar archive extraction](https://github.com/astral-sh/tokio-tar/security/advisories/GHSA-3wgq-wrwc-vqmv) ([#16004](https://github.com/astral-sh/uv/pull/16004)) ## 0.8.23 Released on 2025-10-03. ### Enhancements - Build `s390x` on stable Rust compiler version ([#16082](https://github.com/astral-sh/uv/pull/16082)) - Add `UV_SKIP_WHEEL_FILENAME_CHECK` to allow installing invalid wheels ([#16046](https://github.com/astral-sh/uv/pull/16046)) ### Bug fixes - Avoid rejecting already-installed URL distributions with `--no-sources` ([#16094](https://github.com/astral-sh/uv/pull/16094)) - Confirm that the directory name is a valid Python install key during managed check ([#16080](https://github.com/astral-sh/uv/pull/16080)) - Ignore origin when comparing installed tools ([#16055](https://github.com/astral-sh/uv/pull/16055)) - Make cache control lookups robust to username ([#16088](https://github.com/astral-sh/uv/pull/16088)) - Re-order lock validation checks by severity ([#16045](https://github.com/astral-sh/uv/pull/16045)) - Remove tracking of inferred dependency conflicts ([#15909](https://github.com/astral-sh/uv/pull/15909)) - Respect `--no-color` on the CLI ([#16044](https://github.com/astral-sh/uv/pull/16044)) - Deduplicate marker-specific dependencies in `uv pip tree` output ([#16078](https://github.com/astral-sh/uv/pull/16078)) ### Documentation - Document transparent x86_64 emulation on aarch64 ([#16041](https://github.com/astral-sh/uv/pull/16041)) - Document why we ban URLs from index dependencies ([#15929](https://github.com/astral-sh/uv/pull/15929)) - Fix rendering of `_CONDA_ROOT` in reference ([#16114](https://github.com/astral-sh/uv/pull/16114)) - Windows arm64 and Linux RISC-V64 are Tier 2 supported ([#16027](https://github.com/astral-sh/uv/pull/16027)) ## 0.8.24 Released on 2025-10-06. ### Enhancements - Emit a message on `cache clean` and `prune` when lock is held ([#16138](https://github.com/astral-sh/uv/pull/16138)) - Add `--force` flag for `uv cache prune` ([#16137](https://github.com/astral-sh/uv/pull/16137)) ### Documentation - Fix example of bumping beta version without patch bump ([#16132](https://github.com/astral-sh/uv/pull/16132)) uv-0.9.17+ds1/clippy.toml000066400000000000000000000033361520155276700151410ustar00rootroot00000000000000doc-valid-idents = [ "PyPI", "PubGrub", "PyPy", "CPython", "GraalPy", "ReFS", "PyTorch", "ROCm", "XPU", "PowerShell", ".." # Include the defaults ] disallowed-types = [ "std::fs::DirEntry", "std::fs::File", "std::fs::OpenOptions", "std::fs::ReadDir", "tokio::fs::DirBuilder", "tokio::fs::DirEntry", "tokio::fs::File", "tokio::fs::OpenOptions", "tokio::fs::ReadDir", ] disallowed-methods = [ "std::fs::canonicalize", "std::fs::copy", "std::fs::create_dir", "std::fs::create_dir_all", "std::fs::hard_link", "std::fs::metadata", "std::fs::read", "std::fs::read_dir", "std::fs::read_link", "std::fs::read_to_string", "std::fs::remove_dir", "std::fs::remove_dir_all", "std::fs::remove_file", "std::fs::rename", "std::fs::set_permissions", "std::fs::soft_link", "std::fs::symlink_metadata", "std::fs::write", "tokio::fs::canonicalize", "tokio::fs::copy", "tokio::fs::create_dir", "tokio::fs::create_dir_all", "tokio::fs::hard_link", "tokio::fs::metadata", "tokio::fs::read", "tokio::fs::read_dir", "tokio::fs::read_link", "tokio::fs::read_to_string", "tokio::fs::remove_dir", "tokio::fs::remove_dir_all", "tokio::fs::remove_file", "tokio::fs::rename", "tokio::fs::set_permissions", "tokio::fs::symlink_metadata", "tokio::fs::try_exists", "tokio::fs::write", { path = "std::os::unix::fs::symlink", allow-invalid = true }, { path = "std::os::windows::fs::symlink_dir", allow-invalid = true }, { path = "std::os::windows::fs::symlink_file", allow-invalid = true }, { path = "tokio::fs::symlink", allow-invalid = true }, { path = "tokio::fs::symlink_dir", allow-invalid = true }, { path = "tokio::fs::symlink_file", allow-invalid = true }, ] uv-0.9.17+ds1/crates/000077500000000000000000000000001520155276700142205ustar00rootroot00000000000000uv-0.9.17+ds1/crates/README.md000066400000000000000000000067011520155276700155030ustar00rootroot00000000000000# Crates ## [uv-bench](./uv-bench) Functionality for benchmarking uv. ## [uv-cache-key](./uv-cache-key) Generic functionality for caching paths, URLs, and other resources across platforms. ## [uv-distribution-filename](./uv-distribution-filename) Parse built distribution (wheel) and source distribution (sdist) filenames to extract structured metadata. ## [uv-distribution-types](./uv-distribution-types) Abstractions for representing built distributions (wheels) and source distributions (sdists), and the sources from which they can be downloaded. ## [uv-install-wheel-rs](./uv-install-wheel) Install built distributions (wheels) into a virtual environment. ## [uv-once-map](./uv-once-map) A [`waitmap`](https://github.com/withoutboats/waitmap)-like concurrent hash map for executing tasks exactly once. ## [uv-pep440-rs](./uv-pep440) Utilities for interacting with Python version numbers and specifiers. ## [uv-pep508-rs](./uv-pep508) Utilities for parsing and evaluating [dependency specifiers](https://packaging.python.org/en/latest/specifications/dependency-specifiers/), previously known as [PEP 508](https://peps.python.org/pep-0508/). ## [uv-platform-tags](./uv-platform-tags) Functionality for parsing and inferring Python platform tags as per [PEP 425](https://peps.python.org/pep-0425/). ## [uv-cli](./uv-cli) Command-line interface for the uv package manager. ## [uv-build-frontend](./uv-build-frontend) A [PEP 517](https://www.python.org/dev/peps/pep-0517/)-compatible build frontend for uv. ## [uv-cache](./uv-cache) Functionality for caching Python packages and associated metadata. ## [uv-client](./uv-client) Client for interacting with PyPI-compatible HTTP APIs. ## [uv-dev](./uv-dev) Development utilities for uv. ## [uv-dispatch](./uv-dispatch) A centralized `struct` for resolving and building source distributions in isolated environments. Implements the traits defined in `uv-types`. ## [uv-distribution](./uv-distribution) Client for interacting with built distributions (wheels) and source distributions (sdists). Capable of fetching metadata, distribution contents, etc. ## [uv-extract](./uv-extract) Utilities for extracting files from archives. ## [uv-fs](./uv-fs) Utilities for interacting with the filesystem. ## [uv-git](./uv-git) Functionality for interacting with Git repositories. ## [uv-installer](./uv-installer) Functionality for installing Python packages into a virtual environment. ## [uv-python](./uv-python) Functionality for detecting and leveraging the current Python interpreter. ## [uv-normalize](./uv-normalize) Normalize package and extra names as per Python specifications. ## [uv-requirements](./uv-requirements) Utilities for reading package requirements from `pyproject.toml` and `requirements.txt` files. ## [uv-resolver](./uv-resolver) Functionality for resolving Python packages and their dependencies. ## [uv-shell](./uv-shell) Utilities for detecting and manipulating shell environments. ## [uv-types](./uv-types) Shared traits for uv, to avoid circular dependencies. ## [uv-pypi-types](./uv-pypi-types) General-purpose type definitions for types used in PyPI-compatible APIs. ## [uv-virtualenv](./uv-virtualenv) A `venv` replacement to create virtual environments in Rust. ## [uv-warnings](./uv-warnings) User-facing warnings for uv. ## [uv-workspace](./uv-workspace) Workspace abstractions for uv. ## [uv-requirements-txt](./uv-requirements-txt) Functionality for parsing `requirements.txt` files. uv-0.9.17+ds1/crates/uv-auth/000077500000000000000000000000001520155276700156115ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-auth/Cargo.toml000066400000000000000000000033051520155276700175420ustar00rootroot00000000000000[package] name = "uv-auth" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-cache-key = { workspace = true } uv-fs = { workspace = true } uv-keyring = { workspace = true, features = ["apple-native", "secret-service", "windows-native"] } uv-once-map = { workspace = true } uv-preview = { workspace = true } uv-redacted = { workspace = true } uv-small-str = { workspace = true } uv-state = { workspace = true } uv-static = { workspace = true } uv-warnings = { workspace = true } anyhow = { workspace = true } arcstr = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } etcetera = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } http = { workspace = true } jiff = { workspace = true } percent-encoding = { workspace = true } reqsign = { workspace = true } reqwest = { workspace = true } reqwest-middleware = { workspace = true } rust-netrc = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } toml = { workspace = true } tracing = { workspace = true } url = { workspace = true } [dev-dependencies] insta = { workspace = true } tempfile = { workspace = true } test-log = { workspace = true } tokio = { workspace = true } wiremock = { workspace = true } uv-0.9.17+ds1/crates/uv-auth/README.md000066400000000000000000000010211520155276700170620ustar00rootroot00000000000000 # uv-auth This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-auth). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-auth/src/000077500000000000000000000000001520155276700164005ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-auth/src/access_token.rs000066400000000000000000000014001520155276700214020ustar00rootroot00000000000000/// An encoded JWT access token. #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] #[serde(transparent)] pub struct AccessToken(String); impl AccessToken { /// Return the [`AccessToken`] as a vector of bytes. pub fn into_bytes(self) -> Vec { self.0.into_bytes() } /// Return the [`AccessToken`] as a string slice. pub fn as_str(&self) -> &str { &self.0 } } impl From for AccessToken { fn from(value: String) -> Self { Self(value) } } impl AsRef<[u8]> for AccessToken { fn as_ref(&self) -> &[u8] { self.0.as_bytes() } } impl std::fmt::Display for AccessToken { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "****") } } uv-0.9.17+ds1/crates/uv-auth/src/cache.rs000066400000000000000000000307141520155276700200160ustar00rootroot00000000000000use std::fmt::Display; use std::fmt::Formatter; use std::hash::BuildHasherDefault; use std::sync::Arc; use std::sync::RwLock; use rustc_hash::{FxHashMap, FxHasher}; use tracing::trace; use url::Url; use uv_once_map::OnceMap; use uv_redacted::DisplaySafeUrl; use crate::credentials::{Authentication, Username}; use crate::{Credentials, Realm}; type FxOnceMap = OnceMap>; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) enum FetchUrl { /// A full index URL Index(DisplaySafeUrl), /// A realm URL Realm(Realm), } impl Display for FetchUrl { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { match self { Self::Index(index) => Display::fmt(index, f), Self::Realm(realm) => Display::fmt(realm, f), } } } #[derive(Debug)] // All internal types are redacted. pub struct CredentialsCache { /// A cache per realm and username realms: RwLock>>, /// A cache tracking the result of realm or index URL fetches from external services pub(crate) fetches: FxOnceMap<(FetchUrl, Username), Option>>, /// A cache per URL, uses a trie for efficient prefix queries. urls: RwLock>>, } impl Default for CredentialsCache { fn default() -> Self { Self::new() } } impl CredentialsCache { /// Create a new cache. pub fn new() -> Self { Self { fetches: FxOnceMap::default(), realms: RwLock::new(FxHashMap::default()), urls: RwLock::new(UrlTrie::new()), } } /// Populate the global authentication store with credentials on a URL, if there are any. /// /// Returns `true` if the store was updated. pub fn store_credentials_from_url(&self, url: &DisplaySafeUrl) -> bool { if let Some(credentials) = Credentials::from_url(url) { trace!("Caching credentials for {url}"); self.insert(url, Arc::new(Authentication::from(credentials))); true } else { false } } /// Populate the global authentication store with credentials on a URL, if there are any. /// /// Returns `true` if the store was updated. pub fn store_credentials(&self, url: &DisplaySafeUrl, credentials: Credentials) { trace!("Caching credentials for {url}"); self.insert(url, Arc::new(Authentication::from(credentials))); } /// Return the credentials that should be used for a realm and username, if any. pub(crate) fn get_realm( &self, realm: Realm, username: Username, ) -> Option> { let realms = self.realms.read().unwrap(); let given_username = username.is_some(); let key = (realm, username); let Some(credentials) = realms.get(&key).cloned() else { trace!( "No credentials in cache for realm {}", RealmUsername::from(key) ); return None; }; if given_username && credentials.password().is_none() { // If given a username, don't return password-less credentials trace!( "No password in cache for realm {}", RealmUsername::from(key) ); return None; } trace!( "Found cached credentials for realm {}", RealmUsername::from(key) ); Some(credentials) } /// Return the cached credentials for a URL and username, if any. /// /// Note we do not cache per username, but if a username is passed we will confirm that the /// cached credentials have a username equal to the provided one — otherwise `None` is returned. /// If multiple usernames are used per URL, the realm cache should be queried instead. pub(crate) fn get_url(&self, url: &Url, username: &Username) -> Option> { let urls = self.urls.read().unwrap(); let credentials = urls.get(url); if let Some(credentials) = credentials { if username.is_none() || username.as_deref() == credentials.username() { if username.is_some() && credentials.password().is_none() { // If given a username, don't return password-less credentials trace!("No password in cache for URL {url}"); return None; } trace!("Found cached credentials for URL {url}"); return Some(credentials.clone()); } } trace!("No credentials in cache for URL {url}"); None } /// Update the cache with the given credentials. pub(crate) fn insert(&self, url: &Url, credentials: Arc) { // Do not cache empty credentials if credentials.is_empty() { return; } // Insert an entry for requests including the username let username = credentials.to_username(); if username.is_some() { let realm = (Realm::from(url), username); self.insert_realm(realm, &credentials); } // Insert an entry for requests with no username self.insert_realm((Realm::from(url), Username::none()), &credentials); // Insert an entry for the URL let mut urls = self.urls.write().unwrap(); urls.insert(url, credentials); } /// Private interface to update a realm cache entry. /// /// Returns replaced credentials, if any. fn insert_realm( &self, key: (Realm, Username), credentials: &Arc, ) -> Option> { // Do not cache empty credentials if credentials.is_empty() { return None; } let mut realms = self.realms.write().unwrap(); // Always replace existing entries if we have a password or token if credentials.is_authenticated() { return realms.insert(key, credentials.clone()); } // If we only have a username, add a new entry or replace an existing entry if it doesn't have a password let existing = realms.get(&key); if existing.is_none() || existing.is_some_and(|credentials| credentials.password().is_none()) { return realms.insert(key, credentials.clone()); } None } } #[derive(Debug)] struct UrlTrie { states: Vec>, } #[derive(Debug)] struct TrieState { children: Vec<(String, usize)>, value: Option, } impl Default for TrieState { fn default() -> Self { Self { children: vec![], value: None, } } } impl UrlTrie { fn new() -> Self { let mut trie = Self { states: vec![] }; trie.alloc(); trie } fn get(&self, url: &Url) -> Option<&T> { let mut state = 0; let realm = Realm::from(url).to_string(); for component in [realm.as_str()] .into_iter() .chain(url.path_segments().unwrap().filter(|item| !item.is_empty())) { state = self.states[state].get(component)?; if let Some(ref value) = self.states[state].value { return Some(value); } } self.states[state].value.as_ref() } fn insert(&mut self, url: &Url, value: T) { let mut state = 0; let realm = Realm::from(url).to_string(); for component in [realm.as_str()] .into_iter() .chain(url.path_segments().unwrap().filter(|item| !item.is_empty())) { match self.states[state].index(component) { Ok(i) => state = self.states[state].children[i].1, Err(i) => { let new_state = self.alloc(); self.states[state] .children .insert(i, (component.to_string(), new_state)); state = new_state; } } } self.states[state].value = Some(value); } fn alloc(&mut self) -> usize { let id = self.states.len(); self.states.push(TrieState::default()); id } } impl TrieState { fn get(&self, component: &str) -> Option { let i = self.index(component).ok()?; Some(self.children[i].1) } fn index(&self, component: &str) -> Result { self.children .binary_search_by(|(label, _)| label.as_str().cmp(component)) } } #[derive(Debug)] struct RealmUsername(Realm, Username); impl std::fmt::Display for RealmUsername { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let Self(realm, username) = self; if let Some(username) = username.as_deref() { write!(f, "{username}@{realm}") } else { write!(f, "{realm}") } } } impl From<(Realm, Username)> for RealmUsername { fn from((realm, username): (Realm, Username)) -> Self { Self(realm, username) } } #[cfg(test)] mod tests { use crate::Credentials; use crate::credentials::Password; use super::*; #[test] fn test_trie() { let credentials1 = Credentials::basic(Some("username1".to_string()), Some("password1".to_string())); let credentials2 = Credentials::basic(Some("username2".to_string()), Some("password2".to_string())); let credentials3 = Credentials::basic(Some("username3".to_string()), Some("password3".to_string())); let credentials4 = Credentials::basic(Some("username4".to_string()), Some("password4".to_string())); let mut trie = UrlTrie::new(); trie.insert( &Url::parse("https://burntsushi.net").unwrap(), credentials1.clone(), ); trie.insert( &Url::parse("https://astral.sh").unwrap(), credentials2.clone(), ); trie.insert( &Url::parse("https://example.com/foo").unwrap(), credentials3.clone(), ); trie.insert( &Url::parse("https://example.com/bar").unwrap(), credentials4.clone(), ); let url = Url::parse("https://burntsushi.net/regex-internals").unwrap(); assert_eq!(trie.get(&url), Some(&credentials1)); let url = Url::parse("https://burntsushi.net/").unwrap(); assert_eq!(trie.get(&url), Some(&credentials1)); let url = Url::parse("https://astral.sh/about").unwrap(); assert_eq!(trie.get(&url), Some(&credentials2)); let url = Url::parse("https://example.com/foo").unwrap(); assert_eq!(trie.get(&url), Some(&credentials3)); let url = Url::parse("https://example.com/foo/").unwrap(); assert_eq!(trie.get(&url), Some(&credentials3)); let url = Url::parse("https://example.com/foo/bar").unwrap(); assert_eq!(trie.get(&url), Some(&credentials3)); let url = Url::parse("https://example.com/bar").unwrap(); assert_eq!(trie.get(&url), Some(&credentials4)); let url = Url::parse("https://example.com/bar/").unwrap(); assert_eq!(trie.get(&url), Some(&credentials4)); let url = Url::parse("https://example.com/bar/foo").unwrap(); assert_eq!(trie.get(&url), Some(&credentials4)); let url = Url::parse("https://example.com/about").unwrap(); assert_eq!(trie.get(&url), None); let url = Url::parse("https://example.com/foobar").unwrap(); assert_eq!(trie.get(&url), None); } #[test] fn test_url_with_credentials() { let username = Username::new(Some(String::from("username"))); let password = Password::new(String::from("password")); let credentials = Arc::new(Authentication::from(Credentials::Basic { username: username.clone(), password: Some(password), })); let cache = CredentialsCache::default(); // Insert with URL with credentials and get with redacted URL. let url = Url::parse("https://username:password@example.com/foobar").unwrap(); cache.insert(&url, credentials.clone()); assert_eq!(cache.get_url(&url, &username), Some(credentials.clone())); // Insert with redacted URL and get with URL with credentials. let url = Url::parse("https://username:password@second-example.com/foobar").unwrap(); cache.insert(&url, credentials.clone()); assert_eq!(cache.get_url(&url, &username), Some(credentials.clone())); } } uv-0.9.17+ds1/crates/uv-auth/src/credentials.rs000066400000000000000000000513201520155276700212440ustar00rootroot00000000000000use std::borrow::Cow; use std::fmt; use std::io::Read; use std::io::Write; use std::str::FromStr; use base64::prelude::BASE64_STANDARD; use base64::read::DecoderReader; use base64::write::EncoderWriter; use http::Uri; use netrc::Netrc; use reqsign::aws::DefaultSigner; use reqwest::Request; use reqwest::header::HeaderValue; use serde::{Deserialize, Serialize}; use url::Url; use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; #[derive(Clone, Debug, PartialEq, Eq)] pub enum Credentials { /// RFC 7617 HTTP Basic Authentication Basic { /// The username to use for authentication. username: Username, /// The password to use for authentication. password: Option, }, /// RFC 6750 Bearer Token Authentication Bearer { /// The token to use for authentication. token: Token, }, } #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)] #[serde(transparent)] pub struct Username(Option); impl Username { /// Create a new username. /// /// Unlike `reqwest`, empty usernames are be encoded as `None` instead of an empty string. pub(crate) fn new(value: Option) -> Self { // Ensure empty strings are `None` Self(value.filter(|s| !s.is_empty())) } pub(crate) fn none() -> Self { Self::new(None) } pub(crate) fn is_none(&self) -> bool { self.0.is_none() } pub(crate) fn is_some(&self) -> bool { self.0.is_some() } pub(crate) fn as_deref(&self) -> Option<&str> { self.0.as_deref() } } impl From for Username { fn from(value: String) -> Self { Self::new(Some(value)) } } impl From> for Username { fn from(value: Option) -> Self { Self::new(value) } } #[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)] #[serde(transparent)] pub struct Password(String); impl Password { pub fn new(password: String) -> Self { Self(password) } /// Return the [`Password`] as a string slice. pub fn as_str(&self) -> &str { self.0.as_str() } /// Convert the [`Password`] into its underlying [`String`]. pub fn into_string(self) -> String { self.0 } } impl fmt::Debug for Password { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "****") } } #[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Deserialize)] #[serde(transparent)] pub struct Token(Vec); impl Token { pub fn new(token: Vec) -> Self { Self(token) } /// Return the [`Token`] as a byte slice. pub fn as_slice(&self) -> &[u8] { self.0.as_slice() } /// Convert the [`Token`] into its underlying [`Vec`]. pub fn into_bytes(self) -> Vec { self.0 } /// Return whether the [`Token`] is empty. pub fn is_empty(&self) -> bool { self.0.is_empty() } } impl fmt::Debug for Token { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "****") } } impl Credentials { /// Create a set of HTTP Basic Authentication credentials. #[allow(dead_code)] pub fn basic(username: Option, password: Option) -> Self { Self::Basic { username: Username::new(username), password: password.map(Password), } } /// Create a set of Bearer Authentication credentials. #[allow(dead_code)] pub fn bearer(token: Vec) -> Self { Self::Bearer { token: Token::new(token), } } pub fn username(&self) -> Option<&str> { match self { Self::Basic { username, .. } => username.as_deref(), Self::Bearer { .. } => None, } } pub(crate) fn to_username(&self) -> Username { match self { Self::Basic { username, .. } => username.clone(), Self::Bearer { .. } => Username::none(), } } pub(crate) fn as_username(&self) -> Cow<'_, Username> { match self { Self::Basic { username, .. } => Cow::Borrowed(username), Self::Bearer { .. } => Cow::Owned(Username::none()), } } pub fn password(&self) -> Option<&str> { match self { Self::Basic { password, .. } => password.as_ref().map(Password::as_str), Self::Bearer { .. } => None, } } pub fn is_authenticated(&self) -> bool { match self { Self::Basic { username: _, password, } => password.is_some(), Self::Bearer { token } => !token.is_empty(), } } pub(crate) fn is_empty(&self) -> bool { match self { Self::Basic { username, password } => username.is_none() && password.is_none(), Self::Bearer { token } => token.is_empty(), } } /// Return [`Credentials`] for a [`Url`] from a [`Netrc`] file, if any. /// /// If a username is provided, it must match the login in the netrc file or [`None`] is returned. pub(crate) fn from_netrc( netrc: &Netrc, url: &DisplaySafeUrl, username: Option<&str>, ) -> Option { let host = url.host_str()?; let entry = netrc .hosts .get(host) .or_else(|| netrc.hosts.get("default"))?; // Ensure the username matches if provided if username.is_some_and(|username| username != entry.login) { return None; } Some(Self::Basic { username: Username::new(Some(entry.login.clone())), password: Some(Password(entry.password.clone())), }) } /// Parse [`Credentials`] from a URL, if any. /// /// Returns [`None`] if both [`Url::username`] and [`Url::password`] are not populated. pub fn from_url(url: &Url) -> Option { if url.username().is_empty() && url.password().is_none() { return None; } Some(Self::Basic { // Remove percent-encoding from URL credentials // See username: if url.username().is_empty() { None } else { Some( percent_encoding::percent_decode_str(url.username()) .decode_utf8() .expect("An encoded username should always decode") .into_owned(), ) } .into(), password: url.password().map(|password| { Password( percent_encoding::percent_decode_str(password) .decode_utf8() .expect("An encoded password should always decode") .into_owned(), ) }), }) } /// Extract the [`Credentials`] from the environment, given a named source. /// /// For example, given a name of `"pytorch"`, search for `UV_INDEX_PYTORCH_USERNAME` and /// `UV_INDEX_PYTORCH_PASSWORD`. pub fn from_env(name: impl AsRef) -> Option { let username = std::env::var(EnvVars::index_username(name.as_ref())).ok(); let password = std::env::var(EnvVars::index_password(name.as_ref())).ok(); if username.is_none() && password.is_none() { None } else { Some(Self::basic(username, password)) } } /// Parse [`Credentials`] from an HTTP request, if any. /// /// Only HTTP Basic Authentication is supported. pub(crate) fn from_request(request: &Request) -> Option { // First, attempt to retrieve the credentials from the URL Self::from_url(request.url()).or( // Then, attempt to pull the credentials from the headers request .headers() .get(reqwest::header::AUTHORIZATION) .map(Self::from_header_value)?, ) } /// Parse [`Credentials`] from an authorization header, if any. /// /// HTTP Basic and Bearer Authentication are both supported. /// [`None`] will be returned if another authorization scheme is detected. /// /// Panics if the authentication is not conformant to the HTTP Basic Authentication scheme: /// - The contents must be base64 encoded /// - There must be a `:` separator pub(crate) fn from_header_value(header: &HeaderValue) -> Option { // Parse a `Basic` authentication header. if let Some(mut value) = header.as_bytes().strip_prefix(b"Basic ") { let mut decoder = DecoderReader::new(&mut value, &BASE64_STANDARD); let mut buf = String::new(); decoder .read_to_string(&mut buf) .expect("HTTP Basic Authentication should be base64 encoded"); let (username, password) = buf .split_once(':') .expect("HTTP Basic Authentication should include a `:` separator"); let username = if username.is_empty() { None } else { Some(username.to_string()) }; let password = if password.is_empty() { None } else { Some(password.to_string()) }; return Some(Self::Basic { username: Username::new(username), password: password.map(Password), }); } // Parse a `Bearer` authentication header. if let Some(token) = header.as_bytes().strip_prefix(b"Bearer ") { return Some(Self::Bearer { token: Token::new(token.to_vec()), }); } None } /// Create an HTTP Basic Authentication header for the credentials. /// /// Panics if the username or password cannot be base64 encoded. pub fn to_header_value(&self) -> HeaderValue { match self { Self::Basic { .. } => { // See: let mut buf = b"Basic ".to_vec(); { let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD); write!(encoder, "{}:", self.username().unwrap_or_default()) .expect("Write to base64 encoder should succeed"); if let Some(password) = self.password() { write!(encoder, "{password}") .expect("Write to base64 encoder should succeed"); } } let mut header = HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"); header.set_sensitive(true); header } Self::Bearer { token } => { let mut header = HeaderValue::from_bytes(&[b"Bearer ", token.as_slice()].concat()) .expect("Bearer token is always valid HeaderValue"); header.set_sensitive(true); header } } } /// Apply the credentials to the given URL. /// /// Any existing credentials will be overridden. #[must_use] pub fn apply(&self, mut url: DisplaySafeUrl) -> DisplaySafeUrl { if let Some(username) = self.username() { let _ = url.set_username(username); } if let Some(password) = self.password() { let _ = url.set_password(Some(password)); } url } /// Attach the credentials to the given request. /// /// Any existing credentials will be overridden. #[must_use] pub fn authenticate(&self, mut request: Request) -> Request { request .headers_mut() .insert(reqwest::header::AUTHORIZATION, Self::to_header_value(self)); request } } #[derive(Clone, Debug)] pub(crate) enum Authentication { /// HTTP Basic or Bearer Authentication credentials. Credentials(Credentials), /// AWS Signature Version 4 signing. Signer(DefaultSigner), } impl PartialEq for Authentication { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Credentials(a), Self::Credentials(b)) => a == b, (Self::Signer(..), Self::Signer(..)) => true, _ => false, } } } impl Eq for Authentication {} impl From for Authentication { fn from(credentials: Credentials) -> Self { Self::Credentials(credentials) } } impl From for Authentication { fn from(signer: DefaultSigner) -> Self { Self::Signer(signer) } } impl Authentication { /// Return the password used for authentication, if any. pub(crate) fn password(&self) -> Option<&str> { match self { Self::Credentials(credentials) => credentials.password(), Self::Signer(..) => None, } } /// Return the username used for authentication, if any. pub(crate) fn username(&self) -> Option<&str> { match self { Self::Credentials(credentials) => credentials.username(), Self::Signer(..) => None, } } /// Return the username used for authentication, if any. pub(crate) fn as_username(&self) -> Cow<'_, Username> { match self { Self::Credentials(credentials) => credentials.as_username(), Self::Signer(..) => Cow::Owned(Username::none()), } } /// Return the username used for authentication, if any. pub(crate) fn to_username(&self) -> Username { match self { Self::Credentials(credentials) => credentials.to_username(), Self::Signer(..) => Username::none(), } } /// Return `true` if the object contains a means of authenticating. pub(crate) fn is_authenticated(&self) -> bool { match self { Self::Credentials(credentials) => credentials.is_authenticated(), Self::Signer(..) => true, } } /// Return `true` if the object contains no credentials. pub(crate) fn is_empty(&self) -> bool { match self { Self::Credentials(credentials) => credentials.is_empty(), Self::Signer(..) => false, } } /// Apply the authentication to the given request. /// /// Any existing credentials will be overridden. #[must_use] pub(crate) async fn authenticate(&self, mut request: Request) -> Request { match self { Self::Credentials(credentials) => credentials.authenticate(request), Self::Signer(signer) => { // Build an `http::Request` from the `reqwest::Request`. // SAFETY: If we have a valid `reqwest::Request`, we expect (e.g.) the URL to be valid. let uri = Uri::from_str(request.url().as_str()).unwrap(); let mut http_req = http::Request::builder() .method(request.method().clone()) .uri(uri) .body(()) .unwrap(); *http_req.headers_mut() = request.headers().clone(); // Sign the parts. let (mut parts, ()) = http_req.into_parts(); signer .sign(&mut parts, None) .await .expect("AWS signing should succeed"); // Copy over the signed headers. request.headers_mut().extend(parts.headers); // Copy over the signed path and query, if any. if let Some(path_and_query) = parts.uri.path_and_query() { request.url_mut().set_path(path_and_query.path()); request.url_mut().set_query(path_and_query.query()); } request } } } } #[cfg(test)] mod tests { use insta::assert_debug_snapshot; use super::*; #[test] fn from_url_no_credentials() { let url = &Url::parse("https://example.com/simple/first/").unwrap(); assert_eq!(Credentials::from_url(url), None); } #[test] fn from_url_username_and_password() { let url = &Url::parse("https://example.com/simple/first/").unwrap(); let mut auth_url = url.clone(); auth_url.set_username("user").unwrap(); auth_url.set_password(Some("password")).unwrap(); let credentials = Credentials::from_url(&auth_url).unwrap(); assert_eq!(credentials.username(), Some("user")); assert_eq!(credentials.password(), Some("password")); } #[test] fn from_url_no_username() { let url = &Url::parse("https://example.com/simple/first/").unwrap(); let mut auth_url = url.clone(); auth_url.set_password(Some("password")).unwrap(); let credentials = Credentials::from_url(&auth_url).unwrap(); assert_eq!(credentials.username(), None); assert_eq!(credentials.password(), Some("password")); } #[test] fn from_url_no_password() { let url = &Url::parse("https://example.com/simple/first/").unwrap(); let mut auth_url = url.clone(); auth_url.set_username("user").unwrap(); let credentials = Credentials::from_url(&auth_url).unwrap(); assert_eq!(credentials.username(), Some("user")); assert_eq!(credentials.password(), None); } #[test] fn authenticated_request_from_url() { let url = Url::parse("https://example.com/simple/first/").unwrap(); let mut auth_url = url.clone(); auth_url.set_username("user").unwrap(); auth_url.set_password(Some("password")).unwrap(); let credentials = Credentials::from_url(&auth_url).unwrap(); let mut request = Request::new(reqwest::Method::GET, url); request = credentials.authenticate(request); let mut header = request .headers() .get(reqwest::header::AUTHORIZATION) .expect("Authorization header should be set") .clone(); header.set_sensitive(false); assert_debug_snapshot!(header, @r###""Basic dXNlcjpwYXNzd29yZA==""###); assert_eq!(Credentials::from_header_value(&header), Some(credentials)); } #[test] fn authenticated_request_from_url_with_percent_encoded_user() { let url = Url::parse("https://example.com/simple/first/").unwrap(); let mut auth_url = url.clone(); auth_url.set_username("user@domain").unwrap(); auth_url.set_password(Some("password")).unwrap(); let credentials = Credentials::from_url(&auth_url).unwrap(); let mut request = Request::new(reqwest::Method::GET, url); request = credentials.authenticate(request); let mut header = request .headers() .get(reqwest::header::AUTHORIZATION) .expect("Authorization header should be set") .clone(); header.set_sensitive(false); assert_debug_snapshot!(header, @r###""Basic dXNlckBkb21haW46cGFzc3dvcmQ=""###); assert_eq!(Credentials::from_header_value(&header), Some(credentials)); } #[test] fn authenticated_request_from_url_with_percent_encoded_password() { let url = Url::parse("https://example.com/simple/first/").unwrap(); let mut auth_url = url.clone(); auth_url.set_username("user").unwrap(); auth_url.set_password(Some("password==")).unwrap(); let credentials = Credentials::from_url(&auth_url).unwrap(); let mut request = Request::new(reqwest::Method::GET, url); request = credentials.authenticate(request); let mut header = request .headers() .get(reqwest::header::AUTHORIZATION) .expect("Authorization header should be set") .clone(); header.set_sensitive(false); assert_debug_snapshot!(header, @r###""Basic dXNlcjpwYXNzd29yZD09""###); assert_eq!(Credentials::from_header_value(&header), Some(credentials)); } // Test that we don't include the password in debug messages. #[test] fn test_password_obfuscation() { let credentials = Credentials::basic(Some(String::from("user")), Some(String::from("password"))); let debugged = format!("{credentials:?}"); assert_eq!( debugged, "Basic { username: Username(Some(\"user\")), password: Some(****) }" ); } #[test] fn test_bearer_token_obfuscation() { let token = "super_secret_token"; let credentials = Credentials::bearer(token.into()); let debugged = format!("{credentials:?}"); assert!( !debugged.contains(token), "Token should be obfuscated in Debug impl: {debugged}" ); } } uv-0.9.17+ds1/crates/uv-auth/src/index.rs000066400000000000000000000065761520155276700200730ustar00rootroot00000000000000use std::fmt::{self, Display, Formatter}; use rustc_hash::FxHashSet; use url::Url; use uv_redacted::DisplaySafeUrl; /// When to use authentication. #[derive( Copy, Clone, Debug, Default, Hash, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize, )] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum AuthPolicy { /// Authenticate when necessary. /// /// If credentials are provided, they will be used. Otherwise, an unauthenticated request will /// be attempted first. If the request fails, uv will search for credentials. If credentials are /// found, an authenticated request will be attempted. #[default] Auto, /// Always authenticate. /// /// If credentials are not provided, uv will eagerly search for credentials. If credentials /// cannot be found, uv will error instead of attempting an unauthenticated request. Always, /// Never authenticate. /// /// If credentials are provided, uv will error. uv will not search for credentials. Never, } impl Display for AuthPolicy { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::Auto => write!(f, "auto"), Self::Always => write!(f, "always"), Self::Never => write!(f, "never"), } } } // TODO(john): We are not using `uv_distribution_types::Index` directly // here because it would cause circular crate dependencies. However, this // could potentially make sense for a future refactor. #[derive(Debug, Clone, Hash, Eq, PartialEq)] pub struct Index { pub url: DisplaySafeUrl, /// The root endpoint where authentication is applied. /// For PEP 503 endpoints, this excludes `/simple`. pub root_url: DisplaySafeUrl, pub auth_policy: AuthPolicy, } impl Index { pub fn is_prefix_for(&self, url: &Url) -> bool { if self.root_url.scheme() != url.scheme() || self.root_url.host_str() != url.host_str() || self.root_url.port_or_known_default() != url.port_or_known_default() { return false; } url.path().starts_with(self.root_url.path()) } } // TODO(john): Multiple methods in this struct need to iterate over // all the indexes in the set. There are probably not many URLs to // iterate through, but we could use a trie instead of a HashSet here // for more efficient search. #[derive(Debug, Default, Clone, Eq, PartialEq)] pub struct Indexes(FxHashSet); impl Indexes { pub fn new() -> Self { Self(FxHashSet::default()) } /// Create a new [`Indexes`] instance from an iterator of [`Index`]s. pub fn from_indexes(urls: impl IntoIterator) -> Self { let mut index_urls = Self::new(); for url in urls { index_urls.0.insert(url); } index_urls } /// Get the index for a URL if one exists. pub fn index_for(&self, url: &Url) -> Option<&Index> { self.find_prefix_index(url) } /// Get the [`AuthPolicy`] for a URL. pub fn auth_policy_for(&self, url: &Url) -> AuthPolicy { self.find_prefix_index(url) .map(|index| index.auth_policy) .unwrap_or(AuthPolicy::Auto) } fn find_prefix_index(&self, url: &Url) -> Option<&Index> { self.0.iter().find(|&index| index.is_prefix_for(url)) } } uv-0.9.17+ds1/crates/uv-auth/src/keyring.rs000066400000000000000000000515201520155276700204210ustar00rootroot00000000000000use std::{io::Write, process::Stdio}; use tokio::process::Command; use tracing::{debug, instrument, trace, warn}; use uv_redacted::DisplaySafeUrl; use uv_warnings::warn_user_once; use crate::credentials::Credentials; /// Service name prefix for storing credentials in a keyring. static UV_SERVICE_PREFIX: &str = "uv:"; /// A backend for retrieving credentials from a keyring. /// /// See pip's implementation for reference /// #[derive(Debug)] pub struct KeyringProvider { backend: KeyringProviderBackend, } #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] Keyring(#[from] uv_keyring::Error), #[error("The '{0}' keyring provider does not support storing credentials")] StoreUnsupported(KeyringProviderBackend), #[error("The '{0}' keyring provider does not support removing credentials")] RemoveUnsupported(KeyringProviderBackend), } #[derive(Debug, Clone)] pub enum KeyringProviderBackend { /// Use a native system keyring integration for credentials. Native, /// Use the external `keyring` command for credentials. Subprocess, #[cfg(test)] Dummy(Vec<(String, &'static str, &'static str)>), } impl std::fmt::Display for KeyringProviderBackend { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Native => write!(f, "native"), Self::Subprocess => write!(f, "subprocess"), #[cfg(test)] Self::Dummy(_) => write!(f, "dummy"), } } } impl KeyringProvider { /// Create a new [`KeyringProvider::Native`]. pub fn native() -> Self { Self { backend: KeyringProviderBackend::Native, } } /// Create a new [`KeyringProvider::Subprocess`]. pub fn subprocess() -> Self { Self { backend: KeyringProviderBackend::Subprocess, } } /// Store credentials for the given [`DisplaySafeUrl`] to the keyring. /// /// Only [`KeyringProviderBackend::Native`] is supported at this time. #[instrument(skip_all, fields(url = % url.to_string(), username))] pub async fn store( &self, url: &DisplaySafeUrl, credentials: &Credentials, ) -> Result { let Some(username) = credentials.username() else { trace!("Unable to store credentials in keyring for {url} due to missing username"); return Ok(false); }; let Some(password) = credentials.password() else { trace!("Unable to store credentials in keyring for {url} due to missing password"); return Ok(false); }; // Ensure we strip credentials from the URL before storing let url = url.without_credentials(); // If there's no path, we'll perform a host-level login let target = if let Some(host) = url.host_str().filter(|_| !url.path().is_empty()) { let mut target = String::new(); if url.scheme() != "https" { target.push_str(url.scheme()); target.push_str("://"); } target.push_str(host); if let Some(port) = url.port() { target.push(':'); target.push_str(&port.to_string()); } target } else { url.to_string() }; match &self.backend { KeyringProviderBackend::Native => { self.store_native(&target, username, password).await?; Ok(true) } KeyringProviderBackend::Subprocess => { Err(Error::StoreUnsupported(self.backend.clone())) } #[cfg(test)] KeyringProviderBackend::Dummy(_) => Err(Error::StoreUnsupported(self.backend.clone())), } } /// Store credentials to the system keyring. #[instrument(skip(self))] async fn store_native( &self, service: &str, username: &str, password: &str, ) -> Result<(), Error> { let prefixed_service = format!("{UV_SERVICE_PREFIX}{service}"); let entry = uv_keyring::Entry::new(&prefixed_service, username)?; entry.set_password(password).await?; Ok(()) } /// Remove credentials for the given [`DisplaySafeUrl`] and username from the keyring. /// /// Only [`KeyringProviderBackend::Native`] is supported at this time. #[instrument(skip_all, fields(url = % url.to_string(), username))] pub async fn remove(&self, url: &DisplaySafeUrl, username: &str) -> Result<(), Error> { // Ensure we strip credentials from the URL before storing let url = url.without_credentials(); // If there's no path, we'll perform a host-level login let target = if let Some(host) = url.host_str().filter(|_| !url.path().is_empty()) { let mut target = String::new(); if url.scheme() != "https" { target.push_str(url.scheme()); target.push_str("://"); } target.push_str(host); if let Some(port) = url.port() { target.push(':'); target.push_str(&port.to_string()); } target } else { url.to_string() }; match &self.backend { KeyringProviderBackend::Native => { self.remove_native(&target, username).await?; Ok(()) } KeyringProviderBackend::Subprocess => { Err(Error::RemoveUnsupported(self.backend.clone())) } #[cfg(test)] KeyringProviderBackend::Dummy(_) => Err(Error::RemoveUnsupported(self.backend.clone())), } } /// Remove credentials from the system keyring for the given `service_name`/`username` /// pair. #[instrument(skip(self))] async fn remove_native( &self, service_name: &str, username: &str, ) -> Result<(), uv_keyring::Error> { let prefixed_service = format!("{UV_SERVICE_PREFIX}{service_name}"); let entry = uv_keyring::Entry::new(&prefixed_service, username)?; entry.delete_credential().await?; trace!("Removed credentials for {username}@{service_name} from system keyring"); Ok(()) } /// Fetch credentials for the given [`Url`] from the keyring. /// /// Returns [`None`] if no password was found for the username or if any errors /// are encountered in the keyring backend. #[instrument(skip_all, fields(url = % url.to_string(), username))] pub async fn fetch(&self, url: &DisplaySafeUrl, username: Option<&str>) -> Option { // Validate the request debug_assert!( url.host_str().is_some(), "Should only use keyring for URLs with host" ); debug_assert!( url.password().is_none(), "Should only use keyring for URLs without a password" ); debug_assert!( !username.map(str::is_empty).unwrap_or(false), "Should only use keyring with a non-empty username" ); // Check the full URL first // trace!("Checking keyring for URL {url}"); let mut credentials = match self.backend { KeyringProviderBackend::Native => self.fetch_native(url.as_str(), username).await, KeyringProviderBackend::Subprocess => { self.fetch_subprocess(url.as_str(), username).await } #[cfg(test)] KeyringProviderBackend::Dummy(ref store) => { Self::fetch_dummy(store, url.as_str(), username) } }; // And fallback to a check for the host if credentials.is_none() { let host = if let Some(port) = url.port() { format!("{}:{}", url.host_str()?, port) } else { url.host_str()?.to_string() }; trace!("Checking keyring for host {host}"); credentials = match self.backend { KeyringProviderBackend::Native => self.fetch_native(&host, username).await, KeyringProviderBackend::Subprocess => self.fetch_subprocess(&host, username).await, #[cfg(test)] KeyringProviderBackend::Dummy(ref store) => { Self::fetch_dummy(store, &host, username) } }; } credentials.map(|(username, password)| Credentials::basic(Some(username), Some(password))) } #[instrument(skip(self))] async fn fetch_subprocess( &self, service_name: &str, username: Option<&str>, ) -> Option<(String, String)> { // https://github.com/pypa/pip/blob/24.0/src/pip/_internal/network/auth.py#L136-L141 let mut command = Command::new("keyring"); command.arg("get").arg(service_name); if let Some(username) = username { command.arg(username); } else { command.arg("--mode").arg("creds"); } let child = command .stdin(Stdio::null()) .stdout(Stdio::piped()) // If we're using `--mode creds`, we need to capture the output in order to avoid // showing users an "unrecognized arguments: --mode" error; otherwise, we stream stderr // so the user has visibility into keyring's behavior if it's doing something slow .stderr(if username.is_some() { Stdio::inherit() } else { Stdio::piped() }) .spawn() .inspect_err(|err| warn!("Failure running `keyring` command: {err}")) .ok()?; let output = child .wait_with_output() .await .inspect_err(|err| warn!("Failed to wait for `keyring` output: {err}")) .ok()?; if output.status.success() { // If we captured stderr, display it in case it's helpful to the user // TODO(zanieb): This was done when we added `--mode creds` support for parity with the // existing behavior, but it might be a better UX to hide this on success? It also // might be problematic that we're not streaming it. We could change this given some // user feedback. std::io::stderr().write_all(&output.stderr).ok(); // On success, parse the newline terminated credentials let output = String::from_utf8(output.stdout) .inspect_err(|err| warn!("Failed to parse response from `keyring` command: {err}")) .ok()?; let (username, password) = if let Some(username) = username { // We're only expecting a password let password = output.trim_end(); (username, password) } else { // We're expecting a username and password let mut lines = output.lines(); let username = lines.next()?; let Some(password) = lines.next() else { warn!( "Got username without password for `{service_name}` from `keyring` command" ); return None; }; (username, password) }; if password.is_empty() { // We allow this for backwards compatibility, but it might be better to return // `None` instead if there's confusion from users — we haven't seen this in practice // yet. warn!("Got empty password for `{username}@{service_name}` from `keyring` command"); } Some((username.to_string(), password.to_string())) } else { // On failure, no password was available let stderr = std::str::from_utf8(&output.stderr).ok()?; if stderr.contains("unrecognized arguments: --mode") { // N.B. We do not show the `service_name` here because we'll show the warning twice // otherwise, once for the URL and once for the realm. warn_user_once!( "Attempted to fetch credentials using the `keyring` command, but it does not support `--mode creds`; upgrade to `keyring>=v25.2.1` or provide a username" ); } else if username.is_none() { // If we captured stderr, display it in case it's helpful to the user std::io::stderr().write_all(&output.stderr).ok(); } None } } #[instrument(skip(self))] async fn fetch_native( &self, service: &str, username: Option<&str>, ) -> Option<(String, String)> { let prefixed_service = format!("{UV_SERVICE_PREFIX}{service}"); let username = username?; let Ok(entry) = uv_keyring::Entry::new(&prefixed_service, username) else { return None; }; match entry.get_password().await { Ok(password) => return Some((username.to_string(), password)), Err(uv_keyring::Error::NoEntry) => { debug!("No entry found in system keyring for {service}"); } Err(err) => { warn_user_once!( "Unable to fetch credentials for {service} from system keyring: {err}" ); } } None } #[cfg(test)] fn fetch_dummy( store: &Vec<(String, &'static str, &'static str)>, service_name: &str, username: Option<&str>, ) -> Option<(String, String)> { store.iter().find_map(|(service, user, password)| { if service == service_name && username.is_none_or(|username| username == *user) { Some(((*user).to_string(), (*password).to_string())) } else { None } }) } /// Create a new provider with [`KeyringProviderBackend::Dummy`]. #[cfg(test)] pub fn dummy, T: IntoIterator>( iter: T, ) -> Self { Self { backend: KeyringProviderBackend::Dummy( iter.into_iter() .map(|(service, username, password)| (service.into(), username, password)) .collect(), ), } } /// Create a new provider with no credentials available. #[cfg(test)] pub fn empty() -> Self { Self { backend: KeyringProviderBackend::Dummy(Vec::new()), } } } #[cfg(test)] mod tests { use super::*; use futures::FutureExt; use url::Url; #[tokio::test] async fn fetch_url_no_host() { let url = Url::parse("file:/etc/bin/").unwrap(); let keyring = KeyringProvider::empty(); // Panics due to debug assertion; returns `None` in production let fetch = keyring.fetch(DisplaySafeUrl::ref_cast(&url), Some("user")); if cfg!(debug_assertions) { let result = std::panic::AssertUnwindSafe(fetch).catch_unwind().await; assert!(result.is_err()); } else { assert_eq!(fetch.await, None); } } #[tokio::test] async fn fetch_url_with_password() { let url = Url::parse("https://user:password@example.com").unwrap(); let keyring = KeyringProvider::empty(); // Panics due to debug assertion; returns `None` in production let fetch = keyring.fetch(DisplaySafeUrl::ref_cast(&url), Some(url.username())); if cfg!(debug_assertions) { let result = std::panic::AssertUnwindSafe(fetch).catch_unwind().await; assert!(result.is_err()); } else { assert_eq!(fetch.await, None); } } #[tokio::test] async fn fetch_url_with_empty_username() { let url = Url::parse("https://example.com").unwrap(); let keyring = KeyringProvider::empty(); // Panics due to debug assertion; returns `None` in production let fetch = keyring.fetch(DisplaySafeUrl::ref_cast(&url), Some(url.username())); if cfg!(debug_assertions) { let result = std::panic::AssertUnwindSafe(fetch).catch_unwind().await; assert!(result.is_err()); } else { assert_eq!(fetch.await, None); } } #[tokio::test] async fn fetch_url_no_auth() { let url = Url::parse("https://example.com").unwrap(); let url = DisplaySafeUrl::ref_cast(&url); let keyring = KeyringProvider::empty(); let credentials = keyring.fetch(url, Some("user")); assert!(credentials.await.is_none()); } #[tokio::test] async fn fetch_url() { let url = Url::parse("https://example.com").unwrap(); let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]); assert_eq!( keyring .fetch(DisplaySafeUrl::ref_cast(&url), Some("user")) .await, Some(Credentials::basic( Some("user".to_string()), Some("password".to_string()) )) ); assert_eq!( keyring .fetch( DisplaySafeUrl::ref_cast(&url.join("test").unwrap()), Some("user") ) .await, Some(Credentials::basic( Some("user".to_string()), Some("password".to_string()) )) ); } #[tokio::test] async fn fetch_url_no_match() { let url = Url::parse("https://example.com").unwrap(); let keyring = KeyringProvider::dummy([("other.com", "user", "password")]); let credentials = keyring .fetch(DisplaySafeUrl::ref_cast(&url), Some("user")) .await; assert_eq!(credentials, None); } #[tokio::test] async fn fetch_url_prefers_url_to_host() { let url = Url::parse("https://example.com/").unwrap(); let keyring = KeyringProvider::dummy([ (url.join("foo").unwrap().as_str(), "user", "password"), (url.host_str().unwrap(), "user", "other-password"), ]); assert_eq!( keyring .fetch( DisplaySafeUrl::ref_cast(&url.join("foo").unwrap()), Some("user") ) .await, Some(Credentials::basic( Some("user".to_string()), Some("password".to_string()) )) ); assert_eq!( keyring .fetch(DisplaySafeUrl::ref_cast(&url), Some("user")) .await, Some(Credentials::basic( Some("user".to_string()), Some("other-password".to_string()) )) ); assert_eq!( keyring .fetch( DisplaySafeUrl::ref_cast(&url.join("bar").unwrap()), Some("user") ) .await, Some(Credentials::basic( Some("user".to_string()), Some("other-password".to_string()) )) ); } #[tokio::test] async fn fetch_url_username() { let url = Url::parse("https://example.com").unwrap(); let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]); let credentials = keyring .fetch(DisplaySafeUrl::ref_cast(&url), Some("user")) .await; assert_eq!( credentials, Some(Credentials::basic( Some("user".to_string()), Some("password".to_string()) )) ); } #[tokio::test] async fn fetch_url_no_username() { let url = Url::parse("https://example.com").unwrap(); let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]); let credentials = keyring.fetch(DisplaySafeUrl::ref_cast(&url), None).await; assert_eq!( credentials, Some(Credentials::basic( Some("user".to_string()), Some("password".to_string()) )) ); } #[tokio::test] async fn fetch_url_username_no_match() { let url = Url::parse("https://example.com").unwrap(); let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "foo", "password")]); let credentials = keyring .fetch(DisplaySafeUrl::ref_cast(&url), Some("bar")) .await; assert_eq!(credentials, None); // Still fails if we have `foo` in the URL itself let url = Url::parse("https://foo@example.com").unwrap(); let credentials = keyring .fetch(DisplaySafeUrl::ref_cast(&url), Some("bar")) .await; assert_eq!(credentials, None); } } uv-0.9.17+ds1/crates/uv-auth/src/lib.rs000066400000000000000000000012151520155276700175130ustar00rootroot00000000000000pub use access_token::AccessToken; pub use cache::CredentialsCache; pub use credentials::{Credentials, Username}; pub use index::{AuthPolicy, Index, Indexes}; pub use keyring::KeyringProvider; pub use middleware::AuthMiddleware; pub use pyx::{ DEFAULT_TOLERANCE_SECS, PyxJwt, PyxOAuthTokens, PyxTokenStore, PyxTokens, TokenStoreError, }; pub use realm::{Realm, RealmRef}; pub use service::{Service, ServiceParseError}; pub use store::{AuthBackend, AuthScheme, TextCredentialStore, TomlCredentialError}; mod access_token; mod cache; mod credentials; mod index; mod keyring; mod middleware; mod providers; mod pyx; mod realm; mod service; mod store; uv-0.9.17+ds1/crates/uv-auth/src/middleware.rs000066400000000000000000002553541520155276700211010ustar00rootroot00000000000000use std::sync::{Arc, LazyLock}; use anyhow::{anyhow, format_err}; use http::{Extensions, StatusCode}; use netrc::Netrc; use reqwest::{Request, Response}; use reqwest_middleware::{ClientWithMiddleware, Error, Middleware, Next}; use tokio::sync::Mutex; use tracing::{debug, trace, warn}; use uv_preview::{Preview, PreviewFeatures}; use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; use uv_warnings::owo_colors::OwoColorize; use crate::credentials::Authentication; use crate::providers::{HuggingFaceProvider, S3EndpointProvider}; use crate::pyx::{DEFAULT_TOLERANCE_SECS, PyxTokenStore}; use crate::{ AccessToken, CredentialsCache, KeyringProvider, cache::FetchUrl, credentials::{Credentials, Username}, index::{AuthPolicy, Indexes}, realm::Realm, }; use crate::{Index, TextCredentialStore}; /// Cached check for whether we're running in Dependabot. static IS_DEPENDABOT: LazyLock = LazyLock::new(|| std::env::var(EnvVars::DEPENDABOT).is_ok_and(|value| value == "true")); /// Strategy for loading netrc files. enum NetrcMode { Automatic(LazyLock>), Enabled(Netrc), Disabled, } impl Default for NetrcMode { fn default() -> Self { Self::Automatic(LazyLock::new(|| match Netrc::new() { Ok(netrc) => Some(netrc), Err(netrc::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => { debug!("No netrc file found"); None } Err(err) => { warn!("Error reading netrc file: {err}"); None } })) } } impl NetrcMode { /// Get the parsed netrc file if enabled. fn get(&self) -> Option<&Netrc> { match self { Self::Automatic(lock) => lock.as_ref(), Self::Enabled(netrc) => Some(netrc), Self::Disabled => None, } } } /// Strategy for loading text-based credential files. enum TextStoreMode { Automatic(tokio::sync::OnceCell>), Enabled(TextCredentialStore), Disabled, } impl Default for TextStoreMode { fn default() -> Self { Self::Automatic(tokio::sync::OnceCell::new()) } } impl TextStoreMode { async fn load_default_store() -> Option { let path = TextCredentialStore::default_file() .inspect_err(|err| { warn!("Failed to determine credentials file path: {}", err); }) .ok()?; match TextCredentialStore::read(&path).await { Ok((store, _lock)) => { debug!("Loaded credential file {}", path.display()); Some(store) } Err(err) if err .as_io_error() .is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound) => { debug!("No credentials file found at {}", path.display()); None } Err(err) => { warn!( "Failed to load credentials from {}: {}", path.display(), err ); None } } } /// Get the parsed credential store, if enabled. async fn get(&self) -> Option<&TextCredentialStore> { match self { // TODO(zanieb): Reconsider this pattern. We're just mirroring the [`NetrcMode`] // implementation for now. Self::Automatic(lock) => lock.get_or_init(Self::load_default_store).await.as_ref(), Self::Enabled(store) => Some(store), Self::Disabled => None, } } } #[derive(Debug, Clone)] enum TokenState { /// The token state has not yet been initialized from the store. Uninitialized, /// The token state has been initialized, and the store either returned tokens or `None` if /// the user has not yet authenticated. Initialized(Option), } /// A middleware that adds basic authentication to requests. /// /// Uses a cache to propagate credentials from previously seen requests and /// fetches credentials from a netrc file, TOML file, and the keyring. pub struct AuthMiddleware { netrc: NetrcMode, text_store: TextStoreMode, keyring: Option, /// Global authentication cache for a uv invocation to share credentials across uv clients. cache: Arc, /// Auth policies for specific URLs. indexes: Indexes, /// Set all endpoints as needing authentication. We never try to send an /// unauthenticated request, avoiding cloning an uncloneable request. only_authenticated: bool, /// The base client to use for requests within the middleware. base_client: Option, /// The pyx token store to use for persistent credentials. pyx_token_store: Option, /// Tokens to use for persistent credentials. pyx_token_state: Mutex, preview: Preview, } impl Default for AuthMiddleware { fn default() -> Self { Self::new() } } impl AuthMiddleware { pub fn new() -> Self { Self { netrc: NetrcMode::default(), text_store: TextStoreMode::default(), keyring: None, // TODO(konsti): There shouldn't be a credential cache without that in the initializer. cache: Arc::new(CredentialsCache::default()), indexes: Indexes::new(), only_authenticated: false, base_client: None, pyx_token_store: None, pyx_token_state: Mutex::new(TokenState::Uninitialized), preview: Preview::default(), } } /// Configure the [`Netrc`] credential file to use. /// /// `None` disables authentication via netrc. #[must_use] pub fn with_netrc(mut self, netrc: Option) -> Self { self.netrc = if let Some(netrc) = netrc { NetrcMode::Enabled(netrc) } else { NetrcMode::Disabled }; self } /// Configure the text credential store to use. /// /// `None` disables authentication via text store. #[must_use] pub fn with_text_store(mut self, store: Option) -> Self { self.text_store = if let Some(store) = store { TextStoreMode::Enabled(store) } else { TextStoreMode::Disabled }; self } /// Configure the [`KeyringProvider`] to use. #[must_use] pub fn with_keyring(mut self, keyring: Option) -> Self { self.keyring = keyring; self } /// Configure the [`Preview`] features to use. #[must_use] pub fn with_preview(mut self, preview: Preview) -> Self { self.preview = preview; self } /// Configure the [`CredentialsCache`] to use. #[must_use] pub fn with_cache(mut self, cache: CredentialsCache) -> Self { self.cache = Arc::new(cache); self } /// Configure the [`CredentialsCache`] to use from an existing [`Arc`]. #[must_use] pub fn with_cache_arc(mut self, cache: Arc) -> Self { self.cache = cache; self } /// Configure the [`AuthPolicy`]s to use for URLs. #[must_use] pub fn with_indexes(mut self, indexes: Indexes) -> Self { self.indexes = indexes; self } /// Set all endpoints as needing authentication. We never try to send an /// unauthenticated request, avoiding cloning an uncloneable request. #[must_use] pub fn with_only_authenticated(mut self, only_authenticated: bool) -> Self { self.only_authenticated = only_authenticated; self } /// Configure the [`ClientWithMiddleware`] to use for requests within the middleware. #[must_use] pub fn with_base_client(mut self, client: ClientWithMiddleware) -> Self { self.base_client = Some(client); self } /// Configure the [`PyxTokenStore`] to use for persistent credentials. #[must_use] pub fn with_pyx_token_store(mut self, token_store: PyxTokenStore) -> Self { self.pyx_token_store = Some(token_store); self } /// Global authentication cache for a uv invocation to share credentials across uv clients. fn cache(&self) -> &CredentialsCache { &self.cache } } #[async_trait::async_trait] impl Middleware for AuthMiddleware { /// Handle authentication for a request. /// /// ## If the request has a username and password /// /// We already have a fully authenticated request and we don't need to perform a look-up. /// /// - Perform the request /// - Add the username and password to the cache if successful /// /// ## If the request only has a username /// /// We probably need additional authentication, because a username is provided. /// We'll avoid making a request we expect to fail and look for a password. /// The discovered credentials must have the requested username to be used. /// /// - Check the cache (index URL or realm key) for a password /// - Check the netrc for a password /// - Check the keyring for a password /// - Perform the request /// - Add the username and password to the cache if successful /// /// ## If the request has no authentication /// /// We may or may not need authentication. We'll check for cached credentials for the URL, /// which is relatively specific and can save us an expensive failed request. Otherwise, /// we'll make the request and look for less-specific credentials on failure i.e. if the /// server tells us authorization is needed. This pattern avoids attaching credentials to /// requests that do not need them, which can cause some servers to deny the request. /// /// - Check the cache (URL key) /// - Perform the request /// - On 401, 403, or 404 check for authentication if there was a cache miss /// - Check the cache (index URL or realm key) for the username and password /// - Check the netrc for a username and password /// - Perform the request again if found /// - Add the username and password to the cache if successful async fn handle( &self, mut request: Request, extensions: &mut Extensions, next: Next<'_>, ) -> reqwest_middleware::Result { // Check for credentials attached to the request already let request_credentials = Credentials::from_request(&request).map(Authentication::from); // In the middleware, existing credentials are already moved from the URL // to the headers so for display purposes we restore some information let url = tracing_url(&request, request_credentials.as_ref()); let index = self.indexes.index_for(request.url()); let auth_policy = self.indexes.auth_policy_for(request.url()); trace!("Handling request for {url} with authentication policy {auth_policy}"); let credentials: Option> = if matches!(auth_policy, AuthPolicy::Never) { None } else { if let Some(request_credentials) = request_credentials { return self .complete_request_with_request_credentials( request_credentials, request, extensions, next, &url, index, auth_policy, ) .await; } // We have no credentials trace!("Request for {url} is unauthenticated, checking cache"); // Check the cache for a URL match first. This can save us from // making a failing request let credentials = self.cache().get_url(request.url(), &Username::none()); if let Some(credentials) = credentials.as_ref() { request = credentials.authenticate(request).await; // If it's fully authenticated, finish the request if credentials.is_authenticated() { trace!("Request for {url} is fully authenticated"); return self .complete_request(None, request, extensions, next, auth_policy) .await; } // If we just found a username, we'll make the request then look for password elsewhere // if it fails trace!("Found username for {url} in cache, attempting request"); } credentials }; let attempt_has_username = credentials .as_ref() .is_some_and(|credentials| credentials.username().is_some()); // Determine whether this is a "known" URL. let is_known_url = self .pyx_token_store .as_ref() .is_some_and(|token_store| token_store.is_known_url(request.url())); let must_authenticate = self.only_authenticated || (match auth_policy { AuthPolicy::Auto => is_known_url, AuthPolicy::Always => true, AuthPolicy::Never => false, } // Dependabot intercepts HTTP requests and injects credentials, which means that we // cannot eagerly enforce an `AuthPolicy` as we don't know whether credentials will be // added outside of uv. && !*IS_DEPENDABOT); let (mut retry_request, response) = if !must_authenticate { let url = tracing_url(&request, credentials.as_deref()); if credentials.is_none() { trace!("Attempting unauthenticated request for {url}"); } else { trace!("Attempting partially authenticated request for {url}"); } // // Clone the request so we can retry it on authentication failure let retry_request = request.try_clone().ok_or_else(|| { Error::Middleware(anyhow!( "Request object is not cloneable. Are you passing a streaming body?" .to_string() )) })?; let response = next.clone().run(request, extensions).await?; // If we don't fail with authorization related codes or // authentication policy is Never, return the response. if !matches!( response.status(), StatusCode::FORBIDDEN | StatusCode::NOT_FOUND | StatusCode::UNAUTHORIZED ) || matches!(auth_policy, AuthPolicy::Never) { return Ok(response); } // Otherwise, search for credentials trace!( "Request for {url} failed with {}, checking for credentials", response.status() ); (retry_request, Some(response)) } else { // For endpoints where we require the user to provide credentials, we don't try the // unauthenticated request first. trace!("Checking for credentials for {url}"); (request, None) }; let retry_request_url = DisplaySafeUrl::ref_cast(retry_request.url()); let username = credentials .as_ref() .map(|credentials| credentials.to_username()) .unwrap_or(Username::none()); let credentials = if let Some(index) = index { self.cache().get_url(&index.url, &username).or_else(|| { self.cache() .get_realm(Realm::from(&**retry_request_url), username) }) } else { // Since there is no known index for this URL, check if there are credentials in // the realm-level cache. self.cache() .get_realm(Realm::from(&**retry_request_url), username) } .or(credentials); if let Some(credentials) = credentials.as_ref() { if credentials.is_authenticated() { trace!("Retrying request for {url} with credentials from cache {credentials:?}"); retry_request = credentials.authenticate(retry_request).await; return self .complete_request(None, retry_request, extensions, next, auth_policy) .await; } } // Then, fetch from external services. // Here, we use the username from the cache if present. if let Some(credentials) = self .fetch_credentials( credentials.as_deref(), retry_request_url, index, auth_policy, ) .await { retry_request = credentials.authenticate(retry_request).await; trace!("Retrying request for {url} with {credentials:?}"); return self .complete_request( Some(credentials), retry_request, extensions, next, auth_policy, ) .await; } if let Some(credentials) = credentials.as_ref() { if !attempt_has_username { trace!("Retrying request for {url} with username from cache {credentials:?}"); retry_request = credentials.authenticate(retry_request).await; return self .complete_request(None, retry_request, extensions, next, auth_policy) .await; } } if let Some(response) = response { Ok(response) } else if let Some(store) = is_known_url .then_some(self.pyx_token_store.as_ref()) .flatten() { let domain = store .api() .domain() .unwrap_or("pyx.dev") .trim_start_matches("api."); Err(Error::Middleware(format_err!( "Run `{}` to authenticate uv with pyx", format!("uv auth login {domain}").green() ))) } else { Err(Error::Middleware(format_err!( "Missing credentials for {url}" ))) } } } impl AuthMiddleware { /// Run a request to completion. /// /// If credentials are present, insert them into the cache on success. async fn complete_request( &self, credentials: Option>, request: Request, extensions: &mut Extensions, next: Next<'_>, auth_policy: AuthPolicy, ) -> reqwest_middleware::Result { let Some(credentials) = credentials else { // Nothing to insert into the cache if we don't have credentials return next.run(request, extensions).await; }; let url = DisplaySafeUrl::from_url(request.url().clone()); if matches!(auth_policy, AuthPolicy::Always) && credentials.password().is_none() { return Err(Error::Middleware(format_err!("Missing password for {url}"))); } let result = next.run(request, extensions).await; // Update the cache with new credentials on a successful request if result .as_ref() .is_ok_and(|response| response.error_for_status_ref().is_ok()) { // TODO(zanieb): Consider also updating the system keyring after successful use trace!("Updating cached credentials for {url} to {credentials:?}"); self.cache().insert(&url, credentials); } result } /// Use known request credentials to complete the request. async fn complete_request_with_request_credentials( &self, credentials: Authentication, mut request: Request, extensions: &mut Extensions, next: Next<'_>, url: &DisplaySafeUrl, index: Option<&Index>, auth_policy: AuthPolicy, ) -> reqwest_middleware::Result { let credentials = Arc::new(credentials); // If there's a password, send the request and cache if credentials.is_authenticated() { trace!("Request for {url} already contains username and password"); return self .complete_request(Some(credentials), request, extensions, next, auth_policy) .await; } trace!("Request for {url} is missing a password, looking for credentials"); // There's just a username, try to find a password. // If we have an index, check the cache for that URL. Otherwise, // check for the realm. let maybe_cached_credentials = if let Some(index) = index { self.cache() .get_url(&index.url, credentials.as_username().as_ref()) .or_else(|| { self.cache() .get_url(&index.root_url, credentials.as_username().as_ref()) }) } else { self.cache() .get_realm(Realm::from(request.url()), credentials.to_username()) }; if let Some(credentials) = maybe_cached_credentials { request = credentials.authenticate(request).await; // Do not insert already-cached credentials let credentials = None; return self .complete_request(credentials, request, extensions, next, auth_policy) .await; } let credentials = if let Some(credentials) = self .cache() .get_url(request.url(), credentials.as_username().as_ref()) { request = credentials.authenticate(request).await; // Do not insert already-cached credentials None } else if let Some(credentials) = self .fetch_credentials( Some(&credentials), DisplaySafeUrl::ref_cast(request.url()), index, auth_policy, ) .await { request = credentials.authenticate(request).await; Some(credentials) } else if index.is_some() { // If this is a known index, we fall back to checking for the realm. if let Some(credentials) = self .cache() .get_realm(Realm::from(request.url()), credentials.to_username()) { request = credentials.authenticate(request).await; Some(credentials) } else { Some(credentials) } } else { // If we don't find a password, we'll still attempt the request with the existing credentials Some(credentials) }; self.complete_request(credentials, request, extensions, next, auth_policy) .await } /// Fetch credentials for a URL. /// /// Supports netrc file and keyring lookups. async fn fetch_credentials( &self, credentials: Option<&Authentication>, url: &DisplaySafeUrl, index: Option<&Index>, auth_policy: AuthPolicy, ) -> Option> { let username = Username::from( credentials.map(|credentials| credentials.username().unwrap_or_default().to_string()), ); // Fetches can be expensive, so we will only run them _once_ per realm or index URL and username combination // All other requests for the same realm or index URL will wait until the first one completes let key = if let Some(index) = index { (FetchUrl::Index(index.url.clone()), username) } else { (FetchUrl::Realm(Realm::from(&**url)), username) }; if !self.cache().fetches.register(key.clone()) { let credentials = self .cache() .fetches .wait(&key) .await .expect("The key must exist after register is called"); if credentials.is_some() { trace!("Using credentials from previous fetch for {}", key.0); } else { trace!( "Skipping fetch of credentials for {}, previous attempt failed", key.0 ); } return credentials; } // Support for known providers, like Hugging Face and S3. if let Some(credentials) = HuggingFaceProvider::credentials_for(url) .map(Authentication::from) .map(Arc::new) { debug!("Found Hugging Face credentials for {url}"); self.cache().fetches.done(key, Some(credentials.clone())); return Some(credentials); } if let Some(credentials) = S3EndpointProvider::credentials_for(url, self.preview) .map(Authentication::from) .map(Arc::new) { debug!("Found S3 credentials for {url}"); self.cache().fetches.done(key, Some(credentials.clone())); return Some(credentials); } // If this is a known URL, authenticate it via the token store. if let Some(base_client) = self.base_client.as_ref() { if let Some(token_store) = self.pyx_token_store.as_ref() { if token_store.is_known_url(url) { let mut token_state = self.pyx_token_state.lock().await; // If the token store is uninitialized, initialize it. let token = match *token_state { TokenState::Uninitialized => { trace!("Initializing token store for {url}"); let generated = match token_store .access_token(base_client, DEFAULT_TOLERANCE_SECS) .await { Ok(Some(token)) => Some(token), Ok(None) => None, Err(err) => { warn!("Failed to generate access tokens: {err}"); None } }; *token_state = TokenState::Initialized(generated.clone()); generated } TokenState::Initialized(ref tokens) => tokens.clone(), }; let credentials = token.map(|token| { trace!("Using credentials from token store for {url}"); Arc::new(Authentication::from(Credentials::from(token))) }); // Register the fetch for this key self.cache().fetches.done(key.clone(), credentials.clone()); return credentials; } } } // Netrc support based on: . let credentials = if let Some(credentials) = self.netrc.get().and_then(|netrc| { debug!("Checking netrc for credentials for {url}"); Credentials::from_netrc( netrc, url, credentials .as_ref() .and_then(|credentials| credentials.username()), ) }) { debug!("Found credentials in netrc file for {url}"); Some(credentials) // Text credential store support. } else if let Some(credentials) = self.text_store.get().await.and_then(|text_store| { debug!("Checking text store for credentials for {url}"); text_store .get_credentials( url, credentials .as_ref() .and_then(|credentials| credentials.username()), ) .cloned() }) { debug!("Found credentials in plaintext store for {url}"); Some(credentials) } else if let Some(credentials) = { if self.preview.is_enabled(PreviewFeatures::NATIVE_AUTH) { let native_store = KeyringProvider::native(); let username = credentials.and_then(|credentials| credentials.username()); let display_username = if let Some(username) = username { format!("{username}@") } else { String::new() }; if let Some(index) = index { // N.B. The native store performs an exact look up right now, so we use the root // URL of the index instead of relying on prefix-matching. debug!( "Checking native store for credentials for index URL {}{}", display_username, index.root_url ); native_store.fetch(&index.root_url, username).await } else { debug!( "Checking native store for credentials for URL {}{}", display_username, url ); native_store.fetch(url, username).await } // TODO(zanieb): We should have a realm fallback here too } else { None } } { debug!("Found credentials in native store for {url}"); Some(credentials) // N.B. The keyring provider performs lookups for the exact URL then falls back to the host. // But, in the absence of an index URL, we cache the result per realm. So in that case, // if a keyring implementation returns different credentials for different URLs in the // same realm we will use the wrong credentials. } else if let Some(credentials) = match self.keyring { Some(ref keyring) => { // The subprocess keyring provider is _slow_ so we do not perform fetches for all // URLs; instead, we fetch if there's a username or if the user has requested to // always authenticate. if let Some(username) = credentials.and_then(|credentials| credentials.username()) { if let Some(index) = index { debug!( "Checking keyring for credentials for index URL {}@{}", username, index.url ); keyring .fetch(DisplaySafeUrl::ref_cast(&index.url), Some(username)) .await } else { debug!( "Checking keyring for credentials for full URL {}@{}", username, url ); keyring.fetch(url, Some(username)).await } } else if matches!(auth_policy, AuthPolicy::Always) { if let Some(index) = index { debug!( "Checking keyring for credentials for index URL {} without username due to `authenticate = always`", index.url ); keyring .fetch(DisplaySafeUrl::ref_cast(&index.url), None) .await } else { None } } else { debug!( "Skipping keyring fetch for {url} without username; use `authenticate = always` to force" ); None } } None => None, } { debug!("Found credentials in keyring for {url}"); Some(credentials) } else { None }; let credentials = credentials.map(Authentication::from).map(Arc::new); // Register the fetch for this key self.cache().fetches.done(key, credentials.clone()); credentials } } fn tracing_url(request: &Request, credentials: Option<&Authentication>) -> DisplaySafeUrl { let mut url = DisplaySafeUrl::from_url(request.url().clone()); if let Some(Authentication::Credentials(creds)) = credentials { if let Some(username) = creds.username() { let _ = url.set_username(username); } if let Some(password) = creds.password() { let _ = url.set_password(Some(password)); } } url } #[cfg(test)] mod tests { use std::io::Write; use http::Method; use reqwest::Client; use tempfile::NamedTempFile; use test_log::test; use url::Url; use wiremock::matchers::{basic_auth, method, path_regex}; use wiremock::{Mock, MockServer, ResponseTemplate}; use crate::Index; use crate::credentials::Password; use super::*; type Error = Box; async fn start_test_server(username: &'static str, password: &'static str) -> MockServer { let server = MockServer::start().await; Mock::given(method("GET")) .and(basic_auth(username, password)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; server } fn test_client_builder() -> reqwest_middleware::ClientBuilder { reqwest_middleware::ClientBuilder::new( Client::builder() .build() .expect("Reqwest client should build"), ) } #[test(tokio::test)] async fn test_no_credentials() -> Result<(), Error> { let server = start_test_server("user", "password").await; let client = test_client_builder() .with(AuthMiddleware::new().with_cache(CredentialsCache::new())) .build(); assert_eq!( client .get(format!("{}/foo", server.uri())) .send() .await? .status(), 401 ); assert_eq!( client .get(format!("{}/bar", server.uri())) .send() .await? .status(), 401 ); Ok(()) } /// Without seeding the cache, authenticated requests are not cached #[test(tokio::test)] async fn test_credentials_in_url_no_seed() -> Result<(), Error> { let username = "user"; let password = "password"; let server = start_test_server(username, password).await; let client = test_client_builder() .with(AuthMiddleware::new().with_cache(CredentialsCache::new())) .build(); let base_url = Url::parse(&server.uri())?; let mut url = base_url.clone(); url.set_username(username).unwrap(); url.set_password(Some(password)).unwrap(); assert_eq!(client.get(url).send().await?.status(), 200); // Works for a URL without credentials now assert_eq!( client.get(server.uri()).send().await?.status(), 200, "Subsequent requests should not require credentials" ); assert_eq!( client .get(format!("{}/foo", server.uri())) .send() .await? .status(), 200, "Requests can be to different paths in the same realm" ); let mut url = base_url.clone(); url.set_username(username).unwrap(); url.set_password(Some("invalid")).unwrap(); assert_eq!( client.get(url).send().await?.status(), 401, "Credentials in the URL should take precedence and fail" ); Ok(()) } #[test(tokio::test)] async fn test_credentials_in_url_seed() -> Result<(), Error> { let username = "user"; let password = "password"; let server = start_test_server(username, password).await; let base_url = Url::parse(&server.uri())?; let cache = CredentialsCache::new(); cache.insert( &base_url, Arc::new(Authentication::from(Credentials::basic( Some(username.to_string()), Some(password.to_string()), ))), ); let client = test_client_builder() .with(AuthMiddleware::new().with_cache(cache)) .build(); let mut url = base_url.clone(); url.set_username(username).unwrap(); url.set_password(Some(password)).unwrap(); assert_eq!(client.get(url).send().await?.status(), 200); // Works for a URL without credentials too assert_eq!( client.get(server.uri()).send().await?.status(), 200, "Requests should not require credentials" ); assert_eq!( client .get(format!("{}/foo", server.uri())) .send() .await? .status(), 200, "Requests can be to different paths in the same realm" ); let mut url = base_url.clone(); url.set_username(username).unwrap(); url.set_password(Some("invalid")).unwrap(); assert_eq!( client.get(url).send().await?.status(), 401, "Credentials in the URL should take precedence and fail" ); Ok(()) } #[test(tokio::test)] async fn test_credentials_in_url_username_only() -> Result<(), Error> { let username = "user"; let password = ""; let server = start_test_server(username, password).await; let base_url = Url::parse(&server.uri())?; let cache = CredentialsCache::new(); cache.insert( &base_url, Arc::new(Authentication::from(Credentials::basic( Some(username.to_string()), None, ))), ); let client = test_client_builder() .with(AuthMiddleware::new().with_cache(cache)) .build(); let mut url = base_url.clone(); url.set_username(username).unwrap(); url.set_password(None).unwrap(); assert_eq!(client.get(url).send().await?.status(), 200); // Works for a URL without credentials too assert_eq!( client.get(server.uri()).send().await?.status(), 200, "Requests should not require credentials" ); assert_eq!( client .get(format!("{}/foo", server.uri())) .send() .await? .status(), 200, "Requests can be to different paths in the same realm" ); let mut url = base_url.clone(); url.set_username(username).unwrap(); url.set_password(Some("invalid")).unwrap(); assert_eq!( client.get(url).send().await?.status(), 401, "Credentials in the URL should take precedence and fail" ); assert_eq!( client.get(server.uri()).send().await?.status(), 200, "Subsequent requests should not use the invalid credentials" ); Ok(()) } #[test(tokio::test)] async fn test_netrc_file_default_host() -> Result<(), Error> { let username = "user"; let password = "password"; let mut netrc_file = NamedTempFile::new()?; writeln!(netrc_file, "default login {username} password {password}")?; let server = start_test_server(username, password).await; let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_netrc(Netrc::from_file(netrc_file.path()).ok()), ) .build(); assert_eq!( client.get(server.uri()).send().await?.status(), 200, "Credentials should be pulled from the netrc file" ); let mut url = Url::parse(&server.uri())?; url.set_username(username).unwrap(); url.set_password(Some("invalid")).unwrap(); assert_eq!( client.get(url).send().await?.status(), 401, "Credentials in the URL should take precedence and fail" ); assert_eq!( client.get(server.uri()).send().await?.status(), 200, "Subsequent requests should not use the invalid credentials" ); Ok(()) } #[test(tokio::test)] async fn test_netrc_file_matching_host() -> Result<(), Error> { let username = "user"; let password = "password"; let server = start_test_server(username, password).await; let base_url = Url::parse(&server.uri())?; let mut netrc_file = NamedTempFile::new()?; writeln!( netrc_file, r"machine {} login {username} password {password}", base_url.host_str().unwrap() )?; let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_netrc(Some( Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"), )), ) .build(); assert_eq!( client.get(server.uri()).send().await?.status(), 200, "Credentials should be pulled from the netrc file" ); let mut url = base_url.clone(); url.set_username(username).unwrap(); url.set_password(Some("invalid")).unwrap(); assert_eq!( client.get(url).send().await?.status(), 401, "Credentials in the URL should take precedence and fail" ); assert_eq!( client.get(server.uri()).send().await?.status(), 200, "Subsequent requests should not use the invalid credentials" ); Ok(()) } #[test(tokio::test)] async fn test_netrc_file_mismatched_host() -> Result<(), Error> { let username = "user"; let password = "password"; let server = start_test_server(username, password).await; let mut netrc_file = NamedTempFile::new()?; writeln!( netrc_file, r"machine example.com login {username} password {password}", )?; let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_netrc(Some( Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"), )), ) .build(); assert_eq!( client.get(server.uri()).send().await?.status(), 401, "Credentials should not be pulled from the netrc file due to host mismatch" ); let mut url = Url::parse(&server.uri())?; url.set_username(username).unwrap(); url.set_password(Some(password)).unwrap(); assert_eq!( client.get(url).send().await?.status(), 200, "Credentials in the URL should still work" ); Ok(()) } #[test(tokio::test)] async fn test_netrc_file_mismatched_username() -> Result<(), Error> { let username = "user"; let password = "password"; let server = start_test_server(username, password).await; let base_url = Url::parse(&server.uri())?; let mut netrc_file = NamedTempFile::new()?; writeln!( netrc_file, r"machine {} login {username} password {password}", base_url.host_str().unwrap() )?; let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_netrc(Some( Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"), )), ) .build(); let mut url = base_url.clone(); url.set_username("other-user").unwrap(); assert_eq!( client.get(url).send().await?.status(), 401, "The netrc password should not be used due to a username mismatch" ); let mut url = base_url.clone(); url.set_username("user").unwrap(); assert_eq!( client.get(url).send().await?.status(), 200, "The netrc password should be used for a matching user" ); Ok(()) } #[test(tokio::test)] async fn test_keyring() -> Result<(), Error> { let username = "user"; let password = "password"; let server = start_test_server(username, password).await; let base_url = Url::parse(&server.uri())?; let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_keyring(Some(KeyringProvider::dummy([( format!( "{}:{}", base_url.host_str().unwrap(), base_url.port().unwrap() ), username, password, )]))), ) .build(); assert_eq!( client.get(server.uri()).send().await?.status(), 401, "Credentials are not pulled from the keyring without a username" ); let mut url = base_url.clone(); url.set_username(username).unwrap(); assert_eq!( client.get(url).send().await?.status(), 200, "Credentials for the username should be pulled from the keyring" ); let mut url = base_url.clone(); url.set_username(username).unwrap(); url.set_password(Some("invalid")).unwrap(); assert_eq!( client.get(url).send().await?.status(), 401, "Password in the URL should take precedence and fail" ); let mut url = base_url.clone(); url.set_username(username).unwrap(); assert_eq!( client.get(url.clone()).send().await?.status(), 200, "Subsequent requests should not use the invalid password" ); let mut url = base_url.clone(); url.set_username("other_user").unwrap(); assert_eq!( client.get(url).send().await?.status(), 401, "Credentials are not pulled from the keyring when given another username" ); Ok(()) } #[test(tokio::test)] async fn test_keyring_always_authenticate() -> Result<(), Error> { let username = "user"; let password = "password"; let server = start_test_server(username, password).await; let base_url = Url::parse(&server.uri())?; let indexes = indexes_for(&base_url, AuthPolicy::Always); let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_keyring(Some(KeyringProvider::dummy([( format!( "{}:{}", base_url.host_str().unwrap(), base_url.port().unwrap() ), username, password, )]))) .with_indexes(indexes), ) .build(); assert_eq!( client.get(server.uri()).send().await?.status(), 200, "Credentials (including a username) should be pulled from the keyring" ); let mut url = base_url.clone(); url.set_username(username).unwrap(); assert_eq!( client.get(url).send().await?.status(), 200, "The password for the username should be pulled from the keyring" ); let mut url = base_url.clone(); url.set_username(username).unwrap(); url.set_password(Some("invalid")).unwrap(); assert_eq!( client.get(url).send().await?.status(), 401, "Password in the URL should take precedence and fail" ); let mut url = base_url.clone(); url.set_username("other_user").unwrap(); assert!( matches!( client.get(url).send().await, Err(reqwest_middleware::Error::Middleware(_)) ), "If the username does not match, a password should not be fetched, and the middleware should fail eagerly since `authenticate = always` is not satisfied" ); Ok(()) } /// We include ports in keyring requests, e.g., `localhost:8000` should be distinct from `localhost`, /// unless the server is running on a default port, e.g., `localhost:80` is equivalent to `localhost`. /// We don't unit test the latter case because it's possible to collide with a server a developer is /// actually running. #[test(tokio::test)] async fn test_keyring_includes_non_standard_port() -> Result<(), Error> { let username = "user"; let password = "password"; let server = start_test_server(username, password).await; let base_url = Url::parse(&server.uri())?; let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_keyring(Some(KeyringProvider::dummy([( // Omit the port from the keyring entry base_url.host_str().unwrap(), username, password, )]))), ) .build(); let mut url = base_url.clone(); url.set_username(username).unwrap(); assert_eq!( client.get(url).send().await?.status(), 401, "We should fail because the port is not present in the keyring entry" ); Ok(()) } #[test(tokio::test)] async fn test_credentials_in_keyring_seed() -> Result<(), Error> { let username = "user"; let password = "password"; let server = start_test_server(username, password).await; let base_url = Url::parse(&server.uri())?; let cache = CredentialsCache::new(); // Seed _just_ the username. We should pull the username from the cache if not present on the // URL. cache.insert( &base_url, Arc::new(Authentication::from(Credentials::basic( Some(username.to_string()), None, ))), ); let client = test_client_builder() .with(AuthMiddleware::new().with_cache(cache).with_keyring(Some( KeyringProvider::dummy([( format!( "{}:{}", base_url.host_str().unwrap(), base_url.port().unwrap() ), username, password, )]), ))) .build(); assert_eq!( client.get(server.uri()).send().await?.status(), 200, "The username is pulled from the cache, and the password from the keyring" ); let mut url = base_url.clone(); url.set_username(username).unwrap(); assert_eq!( client.get(url).send().await?.status(), 200, "Credentials for the username should be pulled from the keyring" ); Ok(()) } #[test(tokio::test)] async fn test_credentials_in_url_multiple_realms() -> Result<(), Error> { let username_1 = "user1"; let password_1 = "password1"; let server_1 = start_test_server(username_1, password_1).await; let base_url_1 = Url::parse(&server_1.uri())?; let username_2 = "user2"; let password_2 = "password2"; let server_2 = start_test_server(username_2, password_2).await; let base_url_2 = Url::parse(&server_2.uri())?; let cache = CredentialsCache::new(); // Seed the cache with our credentials cache.insert( &base_url_1, Arc::new(Authentication::from(Credentials::basic( Some(username_1.to_string()), Some(password_1.to_string()), ))), ); cache.insert( &base_url_2, Arc::new(Authentication::from(Credentials::basic( Some(username_2.to_string()), Some(password_2.to_string()), ))), ); let client = test_client_builder() .with(AuthMiddleware::new().with_cache(cache)) .build(); // Both servers should work assert_eq!( client.get(server_1.uri()).send().await?.status(), 200, "Requests should not require credentials" ); assert_eq!( client.get(server_2.uri()).send().await?.status(), 200, "Requests should not require credentials" ); assert_eq!( client .get(format!("{}/foo", server_1.uri())) .send() .await? .status(), 200, "Requests can be to different paths in the same realm" ); assert_eq!( client .get(format!("{}/foo", server_2.uri())) .send() .await? .status(), 200, "Requests can be to different paths in the same realm" ); Ok(()) } #[test(tokio::test)] async fn test_credentials_from_keyring_multiple_realms() -> Result<(), Error> { let username_1 = "user1"; let password_1 = "password1"; let server_1 = start_test_server(username_1, password_1).await; let base_url_1 = Url::parse(&server_1.uri())?; let username_2 = "user2"; let password_2 = "password2"; let server_2 = start_test_server(username_2, password_2).await; let base_url_2 = Url::parse(&server_2.uri())?; let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_keyring(Some(KeyringProvider::dummy([ ( format!( "{}:{}", base_url_1.host_str().unwrap(), base_url_1.port().unwrap() ), username_1, password_1, ), ( format!( "{}:{}", base_url_2.host_str().unwrap(), base_url_2.port().unwrap() ), username_2, password_2, ), ]))), ) .build(); // Both servers do not work without a username assert_eq!( client.get(server_1.uri()).send().await?.status(), 401, "Requests should require a username" ); assert_eq!( client.get(server_2.uri()).send().await?.status(), 401, "Requests should require a username" ); let mut url_1 = base_url_1.clone(); url_1.set_username(username_1).unwrap(); assert_eq!( client.get(url_1.clone()).send().await?.status(), 200, "Requests with a username should succeed" ); assert_eq!( client.get(server_2.uri()).send().await?.status(), 401, "Credentials should not be re-used for the second server" ); let mut url_2 = base_url_2.clone(); url_2.set_username(username_2).unwrap(); assert_eq!( client.get(url_2.clone()).send().await?.status(), 200, "Requests with a username should succeed" ); assert_eq!( client.get(format!("{url_1}/foo")).send().await?.status(), 200, "Requests can be to different paths in the same realm" ); assert_eq!( client.get(format!("{url_2}/foo")).send().await?.status(), 200, "Requests can be to different paths in the same realm" ); Ok(()) } #[test(tokio::test)] async fn test_credentials_in_url_mixed_authentication_in_realm() -> Result<(), Error> { let username_1 = "user1"; let password_1 = "password1"; let username_2 = "user2"; let password_2 = "password2"; let server = MockServer::start().await; Mock::given(method("GET")) .and(path_regex("/prefix_1.*")) .and(basic_auth(username_1, password_1)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .and(path_regex("/prefix_2.*")) .and(basic_auth(username_2, password_2)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; // Create a third, public prefix // It will throw a 401 if it receives credentials Mock::given(method("GET")) .and(path_regex("/prefix_3.*")) .and(basic_auth(username_1, password_1)) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; Mock::given(method("GET")) .and(path_regex("/prefix_3.*")) .and(basic_auth(username_2, password_2)) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; Mock::given(method("GET")) .and(path_regex("/prefix_3.*")) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; let base_url = Url::parse(&server.uri())?; let base_url_1 = base_url.join("prefix_1")?; let base_url_2 = base_url.join("prefix_2")?; let base_url_3 = base_url.join("prefix_3")?; let cache = CredentialsCache::new(); // Seed the cache with our credentials cache.insert( &base_url_1, Arc::new(Authentication::from(Credentials::basic( Some(username_1.to_string()), Some(password_1.to_string()), ))), ); cache.insert( &base_url_2, Arc::new(Authentication::from(Credentials::basic( Some(username_2.to_string()), Some(password_2.to_string()), ))), ); let client = test_client_builder() .with(AuthMiddleware::new().with_cache(cache)) .build(); // Both servers should work assert_eq!( client.get(base_url_1.clone()).send().await?.status(), 200, "Requests should not require credentials" ); assert_eq!( client.get(base_url_2.clone()).send().await?.status(), 200, "Requests should not require credentials" ); assert_eq!( client .get(base_url.join("prefix_1/foo")?) .send() .await? .status(), 200, "Requests can be to different paths in the same realm" ); assert_eq!( client .get(base_url.join("prefix_2/foo")?) .send() .await? .status(), 200, "Requests can be to different paths in the same realm" ); assert_eq!( client .get(base_url.join("prefix_1_foo")?) .send() .await? .status(), 401, "Requests to paths with a matching prefix but different resource segments should fail" ); assert_eq!( client.get(base_url_3.clone()).send().await?.status(), 200, "Requests to the 'public' prefix should not use credentials" ); Ok(()) } #[test(tokio::test)] async fn test_credentials_from_keyring_mixed_authentication_in_realm() -> Result<(), Error> { let username_1 = "user1"; let password_1 = "password1"; let username_2 = "user2"; let password_2 = "password2"; let server = MockServer::start().await; Mock::given(method("GET")) .and(path_regex("/prefix_1.*")) .and(basic_auth(username_1, password_1)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .and(path_regex("/prefix_2.*")) .and(basic_auth(username_2, password_2)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; // Create a third, public prefix // It will throw a 401 if it receives credentials Mock::given(method("GET")) .and(path_regex("/prefix_3.*")) .and(basic_auth(username_1, password_1)) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; Mock::given(method("GET")) .and(path_regex("/prefix_3.*")) .and(basic_auth(username_2, password_2)) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; Mock::given(method("GET")) .and(path_regex("/prefix_3.*")) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; let base_url = Url::parse(&server.uri())?; let base_url_1 = base_url.join("prefix_1")?; let base_url_2 = base_url.join("prefix_2")?; let base_url_3 = base_url.join("prefix_3")?; let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_keyring(Some(KeyringProvider::dummy([ ( format!( "{}:{}", base_url_1.host_str().unwrap(), base_url_1.port().unwrap() ), username_1, password_1, ), ( format!( "{}:{}", base_url_2.host_str().unwrap(), base_url_2.port().unwrap() ), username_2, password_2, ), ]))), ) .build(); // Both servers do not work without a username assert_eq!( client.get(base_url_1.clone()).send().await?.status(), 401, "Requests should require a username" ); assert_eq!( client.get(base_url_2.clone()).send().await?.status(), 401, "Requests should require a username" ); let mut url_1 = base_url_1.clone(); url_1.set_username(username_1).unwrap(); assert_eq!( client.get(url_1.clone()).send().await?.status(), 200, "Requests with a username should succeed" ); assert_eq!( client.get(base_url_2.clone()).send().await?.status(), 401, "Credentials should not be re-used for the second prefix" ); let mut url_2 = base_url_2.clone(); url_2.set_username(username_2).unwrap(); assert_eq!( client.get(url_2.clone()).send().await?.status(), 200, "Requests with a username should succeed" ); assert_eq!( client .get(base_url.join("prefix_1/foo")?) .send() .await? .status(), 200, "Requests can be to different paths in the same prefix" ); assert_eq!( client .get(base_url.join("prefix_2/foo")?) .send() .await? .status(), 200, "Requests can be to different paths in the same prefix" ); assert_eq!( client .get(base_url.join("prefix_1_foo")?) .send() .await? .status(), 401, "Requests to paths with a matching prefix but different resource segments should fail" ); assert_eq!( client.get(base_url_3.clone()).send().await?.status(), 200, "Requests to the 'public' prefix should not use credentials" ); Ok(()) } /// Demonstrates "incorrect" behavior in our cache which avoids an expensive fetch of /// credentials for _every_ request URL at the cost of inconsistent behavior when /// credentials are not scoped to a realm. #[test(tokio::test)] async fn test_credentials_from_keyring_mixed_authentication_in_realm_same_username() -> Result<(), Error> { let username = "user"; let password_1 = "password1"; let password_2 = "password2"; let server = MockServer::start().await; Mock::given(method("GET")) .and(path_regex("/prefix_1.*")) .and(basic_auth(username, password_1)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .and(path_regex("/prefix_2.*")) .and(basic_auth(username, password_2)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; let base_url = Url::parse(&server.uri())?; let base_url_1 = base_url.join("prefix_1")?; let base_url_2 = base_url.join("prefix_2")?; let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_keyring(Some(KeyringProvider::dummy([ (base_url_1.clone(), username, password_1), (base_url_2.clone(), username, password_2), ]))), ) .build(); // Both servers do not work without a username assert_eq!( client.get(base_url_1.clone()).send().await?.status(), 401, "Requests should require a username" ); assert_eq!( client.get(base_url_2.clone()).send().await?.status(), 401, "Requests should require a username" ); let mut url_1 = base_url_1.clone(); url_1.set_username(username).unwrap(); assert_eq!( client.get(url_1.clone()).send().await?.status(), 200, "The first request with a username will succeed" ); assert_eq!( client.get(base_url_2.clone()).send().await?.status(), 401, "Credentials should not be re-used for the second prefix" ); assert_eq!( client .get(base_url.join("prefix_1/foo")?) .send() .await? .status(), 200, "Subsequent requests can be to different paths in the same prefix" ); let mut url_2 = base_url_2.clone(); url_2.set_username(username).unwrap(); assert_eq!( client.get(url_2.clone()).send().await?.status(), 401, // INCORRECT BEHAVIOR "A request with the same username and realm for a URL that needs a different password will fail" ); assert_eq!( client .get(base_url.join("prefix_2/foo")?) .send() .await? .status(), 401, // INCORRECT BEHAVIOR "Requests to other paths in the failing prefix will also fail" ); Ok(()) } /// Demonstrates that when an index URL is provided, we avoid "incorrect" behavior /// where multiple URLs with the same username and realm share the same realm-level /// credentials cache entry. #[test(tokio::test)] async fn test_credentials_from_keyring_mixed_authentication_different_indexes_same_realm() -> Result<(), Error> { let username = "user"; let password_1 = "password1"; let password_2 = "password2"; let server = MockServer::start().await; Mock::given(method("GET")) .and(path_regex("/prefix_1.*")) .and(basic_auth(username, password_1)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .and(path_regex("/prefix_2.*")) .and(basic_auth(username, password_2)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; let base_url = Url::parse(&server.uri())?; let base_url_1 = base_url.join("prefix_1")?; let base_url_2 = base_url.join("prefix_2")?; let indexes = Indexes::from_indexes(vec![ Index { url: DisplaySafeUrl::from_url(base_url_1.clone()), root_url: DisplaySafeUrl::from_url(base_url_1.clone()), auth_policy: AuthPolicy::Auto, }, Index { url: DisplaySafeUrl::from_url(base_url_2.clone()), root_url: DisplaySafeUrl::from_url(base_url_2.clone()), auth_policy: AuthPolicy::Auto, }, ]); let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_keyring(Some(KeyringProvider::dummy([ (base_url_1.clone(), username, password_1), (base_url_2.clone(), username, password_2), ]))) .with_indexes(indexes), ) .build(); // Both servers do not work without a username assert_eq!( client.get(base_url_1.clone()).send().await?.status(), 401, "Requests should require a username" ); assert_eq!( client.get(base_url_2.clone()).send().await?.status(), 401, "Requests should require a username" ); let mut url_1 = base_url_1.clone(); url_1.set_username(username).unwrap(); assert_eq!( client.get(url_1.clone()).send().await?.status(), 200, "The first request with a username will succeed" ); assert_eq!( client.get(base_url_2.clone()).send().await?.status(), 401, "Credentials should not be re-used for the second prefix" ); assert_eq!( client .get(base_url.join("prefix_1/foo")?) .send() .await? .status(), 200, "Subsequent requests can be to different paths in the same prefix" ); let mut url_2 = base_url_2.clone(); url_2.set_username(username).unwrap(); assert_eq!( client.get(url_2.clone()).send().await?.status(), 200, "A request with the same username and realm for a URL will use index-specific password" ); assert_eq!( client .get(base_url.join("prefix_2/foo")?) .send() .await? .status(), 200, "Requests to other paths with that prefix will also succeed" ); Ok(()) } /// Demonstrates that when an index' credentials are cached for its realm, we /// find those credentials if they're not present in the keyring. #[test(tokio::test)] async fn test_credentials_from_keyring_shared_authentication_different_indexes_same_realm() -> Result<(), Error> { let username = "user"; let password = "password"; let server = MockServer::start().await; Mock::given(method("GET")) .and(basic_auth(username, password)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .and(path_regex("/prefix_1.*")) .and(basic_auth(username, password)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; let base_url = Url::parse(&server.uri())?; let index_url = base_url.join("prefix_1")?; let indexes = Indexes::from_indexes(vec![Index { url: DisplaySafeUrl::from_url(index_url.clone()), root_url: DisplaySafeUrl::from_url(index_url.clone()), auth_policy: AuthPolicy::Auto, }]); let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_keyring(Some(KeyringProvider::dummy([( base_url.clone(), username, password, )]))) .with_indexes(indexes), ) .build(); // Index server does not work without a username assert_eq!( client.get(index_url.clone()).send().await?.status(), 401, "Requests should require a username" ); // Send a request that will cache realm credentials. let mut realm_url = base_url.clone(); realm_url.set_username(username).unwrap(); assert_eq!( client.get(realm_url.clone()).send().await?.status(), 200, "The first realm request with a username will succeed" ); let mut url = index_url.clone(); url.set_username(username).unwrap(); assert_eq!( client.get(url.clone()).send().await?.status(), 200, "A request with the same username and realm for a URL will use the realm if there is no index-specific password" ); assert_eq!( client .get(base_url.join("prefix_1/foo")?) .send() .await? .status(), 200, "Requests to other paths with that prefix will also succeed" ); Ok(()) } fn indexes_for(url: &Url, policy: AuthPolicy) -> Indexes { let mut url = DisplaySafeUrl::from_url(url.clone()); url.set_password(None).ok(); url.set_username("").ok(); Indexes::from_indexes(vec![Index { url: url.clone(), root_url: url.clone(), auth_policy: policy, }]) } /// With the "always" auth policy, requests should succeed on /// authenticated requests with the correct credentials. #[test(tokio::test)] async fn test_auth_policy_always_with_credentials() -> Result<(), Error> { let username = "user"; let password = "password"; let server = start_test_server(username, password).await; let base_url = Url::parse(&server.uri())?; let indexes = indexes_for(&base_url, AuthPolicy::Always); let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_indexes(indexes), ) .build(); Mock::given(method("GET")) .and(path_regex("/*")) .and(basic_auth(username, password)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; let mut url = base_url.clone(); url.set_username(username).unwrap(); url.set_password(Some(password)).unwrap(); assert_eq!(client.get(url).send().await?.status(), 200); assert_eq!( client .get(format!("{}/foo", server.uri())) .send() .await? .status(), 200, "Requests can be to different paths with index URL as prefix" ); let mut url = base_url.clone(); url.set_username(username).unwrap(); url.set_password(Some("invalid")).unwrap(); assert_eq!( client.get(url).send().await?.status(), 401, "Incorrect credentials should fail" ); Ok(()) } /// With the "always" auth policy, requests should fail if only /// unauthenticated requests are supported. #[test(tokio::test)] async fn test_auth_policy_always_unauthenticated() -> Result<(), Error> { let server = MockServer::start().await; Mock::given(method("GET")) .and(path_regex("/*")) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; let base_url = Url::parse(&server.uri())?; let indexes = indexes_for(&base_url, AuthPolicy::Always); let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_indexes(indexes), ) .build(); // Unauthenticated requests are not allowed. assert!(matches!( client.get(server.uri()).send().await, Err(reqwest_middleware::Error::Middleware(_)) )); Ok(()) } /// With the "never" auth policy, requests should fail if /// an endpoint requires authentication. #[test(tokio::test)] async fn test_auth_policy_never_with_credentials() -> Result<(), Error> { let username = "user"; let password = "password"; let server = start_test_server(username, password).await; let base_url = Url::parse(&server.uri())?; Mock::given(method("GET")) .and(path_regex("/*")) .and(basic_auth(username, password)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; let indexes = indexes_for(&base_url, AuthPolicy::Never); let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_indexes(indexes), ) .build(); let mut url = base_url.clone(); url.set_username(username).unwrap(); url.set_password(Some(password)).unwrap(); assert_eq!( client .get(format!("{}/foo", server.uri())) .send() .await? .status(), 401, "Requests should not be completed if credentials are required" ); Ok(()) } /// With the "never" auth policy, requests should succeed if /// unauthenticated requests succeed. #[test(tokio::test)] async fn test_auth_policy_never_unauthenticated() -> Result<(), Error> { let server = MockServer::start().await; Mock::given(method("GET")) .and(path_regex("/*")) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; let base_url = Url::parse(&server.uri())?; let indexes = indexes_for(&base_url, AuthPolicy::Never); let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_indexes(indexes), ) .build(); assert_eq!( client.get(server.uri()).send().await?.status(), 200, "Requests should succeed if unauthenticated requests can succeed" ); Ok(()) } #[test] fn test_tracing_url() { // No credentials let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple"); assert_eq!( tracing_url(&req, None), DisplaySafeUrl::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap() ); let creds = Authentication::from(Credentials::Basic { username: Username::new(Some(String::from("user"))), password: None, }); let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple"); assert_eq!( tracing_url(&req, Some(&creds)), DisplaySafeUrl::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap() ); let creds = Authentication::from(Credentials::Basic { username: Username::new(Some(String::from("user"))), password: Some(Password::new(String::from("password"))), }); let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple"); assert_eq!( tracing_url(&req, Some(&creds)), DisplaySafeUrl::parse("https://user:password@pypi-proxy.fly.dev/basic-auth/simple") .unwrap() ); } #[test(tokio::test)] async fn test_text_store_basic_auth() -> Result<(), Error> { let username = "user"; let password = "password"; let server = start_test_server(username, password).await; let base_url = Url::parse(&server.uri())?; // Create a text credential store with matching credentials let mut store = TextCredentialStore::default(); let service = crate::Service::try_from(base_url.to_string()).unwrap(); let credentials = Credentials::basic(Some(username.to_string()), Some(password.to_string())); store.insert(service.clone(), credentials); let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_text_store(Some(store)), ) .build(); assert_eq!( client.get(server.uri()).send().await?.status(), 200, "Credentials should be pulled from the text store" ); Ok(()) } #[test(tokio::test)] async fn test_text_store_disabled() -> Result<(), Error> { let username = "user"; let password = "password"; let server = start_test_server(username, password).await; let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_text_store(None), // Explicitly disable text store ) .build(); assert_eq!( client.get(server.uri()).send().await?.status(), 401, "Credentials should not be found when text store is disabled" ); Ok(()) } #[test(tokio::test)] async fn test_text_store_by_username() -> Result<(), Error> { let username = "testuser"; let password = "testpass"; let wrong_username = "wronguser"; let server = start_test_server(username, password).await; let base_url = Url::parse(&server.uri())?; let mut store = TextCredentialStore::default(); let service = crate::Service::try_from(base_url.to_string()).unwrap(); let credentials = crate::Credentials::basic(Some(username.to_string()), Some(password.to_string())); store.insert(service.clone(), credentials); let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) .with_text_store(Some(store)), ) .build(); // Request with matching username should succeed let url_with_username = format!( "{}://{}@{}", base_url.scheme(), username, base_url.host_str().unwrap() ); let url_with_port = if let Some(port) = base_url.port() { format!("{}:{}{}", url_with_username, port, base_url.path()) } else { format!("{}{}", url_with_username, base_url.path()) }; assert_eq!( client.get(&url_with_port).send().await?.status(), 200, "Request with matching username should succeed" ); // Request with non-matching username should fail let url_with_wrong_username = format!( "{}://{}@{}", base_url.scheme(), wrong_username, base_url.host_str().unwrap() ); let url_with_port = if let Some(port) = base_url.port() { format!("{}:{}{}", url_with_wrong_username, port, base_url.path()) } else { format!("{}{}", url_with_wrong_username, base_url.path()) }; assert_eq!( client.get(&url_with_port).send().await?.status(), 401, "Request with non-matching username should fail" ); // Request without username should succeed assert_eq!( client.get(server.uri()).send().await?.status(), 200, "Request with no username should succeed" ); Ok(()) } fn create_request(url: &str) -> Request { Request::new(Method::GET, Url::parse(url).unwrap()) } } uv-0.9.17+ds1/crates/uv-auth/src/providers.rs000066400000000000000000000072241520155276700207700ustar00rootroot00000000000000use std::borrow::Cow; use std::sync::LazyLock; use reqsign::aws::DefaultSigner; use tracing::debug; use url::Url; use uv_preview::{Preview, PreviewFeatures}; use uv_static::EnvVars; use uv_warnings::warn_user_once; use crate::Credentials; use crate::credentials::Token; use crate::realm::{Realm, RealmRef}; /// The [`Realm`] for the Hugging Face platform. static HUGGING_FACE_REALM: LazyLock = LazyLock::new(|| { let url = Url::parse("https://huggingface.co").expect("Failed to parse Hugging Face URL"); Realm::from(&url) }); /// The authentication token for the Hugging Face platform, if set. static HUGGING_FACE_TOKEN: LazyLock>> = LazyLock::new(|| { // Extract the Hugging Face token from the environment variable, if it exists. let hf_token = std::env::var(EnvVars::HF_TOKEN) .ok() .map(String::into_bytes) .filter(|token| !token.is_empty())?; if std::env::var_os(EnvVars::UV_NO_HF_TOKEN).is_some() { debug!("Ignoring Hugging Face token from environment due to `UV_NO_HF_TOKEN`"); return None; } debug!("Found Hugging Face token in environment"); Some(hf_token) }); /// A provider for authentication credentials for the Hugging Face platform. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct HuggingFaceProvider; impl HuggingFaceProvider { /// Returns the credentials for the Hugging Face platform, if available. pub(crate) fn credentials_for(url: &Url) -> Option { if RealmRef::from(url) == *HUGGING_FACE_REALM { if let Some(token) = HUGGING_FACE_TOKEN.as_ref() { return Some(Credentials::Bearer { token: Token::new(token.clone()), }); } } None } } /// The [`Url`] for the S3 endpoint, if set. static S3_ENDPOINT_REALM: LazyLock> = LazyLock::new(|| { let s3_endpoint_url = std::env::var(EnvVars::UV_S3_ENDPOINT_URL).ok()?; let url = Url::parse(&s3_endpoint_url).expect("Failed to parse S3 endpoint URL"); Some(Realm::from(&url)) }); /// A provider for authentication credentials for S3 endpoints. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct S3EndpointProvider; impl S3EndpointProvider { /// Returns the credentials for the S3 endpoint, if available. pub(crate) fn credentials_for(url: &Url, preview: Preview) -> Option { if let Some(s3_endpoint_realm) = S3_ENDPOINT_REALM.as_ref().map(RealmRef::from) { if !preview.is_enabled(PreviewFeatures::S3_ENDPOINT) { warn_user_once!( "The `s3-endpoint` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", PreviewFeatures::S3_ENDPOINT ); } // Treat any URL on the same domain or subdomain as available for S3 signing. let realm = RealmRef::from(url); if realm == s3_endpoint_realm || realm.is_subdomain_of(s3_endpoint_realm) { // TODO(charlie): Can `reqsign` infer the region for us? Profiles, for example, // often have a region set already. let region = std::env::var(EnvVars::AWS_REGION) .map(Cow::Owned) .unwrap_or_else(|_| { std::env::var(EnvVars::AWS_DEFAULT_REGION) .map(Cow::Owned) .unwrap_or_else(|_| Cow::Borrowed("us-east-1")) }); let signer = reqsign::aws::default_signer("s3", ®ion); return Some(signer); } } None } } uv-0.9.17+ds1/crates/uv-auth/src/pyx.rs000066400000000000000000000604441520155276700175760ustar00rootroot00000000000000use std::io; use std::path::{Path, PathBuf}; use std::time::Duration; use base64::Engine; use base64::prelude::BASE64_URL_SAFE_NO_PAD; use etcetera::BaseStrategy; use reqwest_middleware::ClientWithMiddleware; use tracing::debug; use url::Url; use uv_cache_key::CanonicalUrl; use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError}; use uv_small_str::SmallString; use uv_state::{StateBucket, StateStore}; use uv_static::EnvVars; use crate::credentials::Token; use crate::{AccessToken, Credentials, Realm}; /// Retrieve the pyx API key from the environment variable, or return `None`. fn read_pyx_api_key() -> Option { std::env::var(EnvVars::PYX_API_KEY) .ok() .or_else(|| std::env::var(EnvVars::UV_API_KEY).ok()) } /// Retrieve the pyx authentication token (JWT) from the environment variable, or return `None`. fn read_pyx_auth_token() -> Option { std::env::var(EnvVars::PYX_AUTH_TOKEN) .ok() .or_else(|| std::env::var(EnvVars::UV_AUTH_TOKEN).ok()) .map(AccessToken::from) } /// An access token with an accompanying refresh token. /// /// Refresh tokens are single-use tokens that can be exchanged for a renewed access token /// and a new refresh token. #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct PyxOAuthTokens { pub access_token: AccessToken, pub refresh_token: String, } /// An access token with an accompanying API key. #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct PyxApiKeyTokens { pub access_token: AccessToken, pub api_key: String, } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub enum PyxTokens { /// An access token with an accompanying refresh token. /// /// Refresh tokens are single-use tokens that can be exchanged for a renewed access token /// and a new refresh token. OAuth(PyxOAuthTokens), /// An access token with an accompanying API key. /// /// API keys are long-lived tokens that can be exchanged for an access token. ApiKey(PyxApiKeyTokens), } impl From for AccessToken { fn from(tokens: PyxTokens) -> Self { match tokens { PyxTokens::OAuth(PyxOAuthTokens { access_token, .. }) => access_token, PyxTokens::ApiKey(PyxApiKeyTokens { access_token, .. }) => access_token, } } } impl From for Credentials { fn from(tokens: PyxTokens) -> Self { let access_token = match tokens { PyxTokens::OAuth(PyxOAuthTokens { access_token, .. }) => access_token, PyxTokens::ApiKey(PyxApiKeyTokens { access_token, .. }) => access_token, }; Self::from(access_token) } } impl From for Credentials { fn from(access_token: AccessToken) -> Self { Self::Bearer { token: Token::new(access_token.into_bytes()), } } } /// The default tolerance for the access token expiration. pub const DEFAULT_TOLERANCE_SECS: u64 = 60 * 5; #[derive(Debug, Clone)] struct PyxDirectories { /// The root directory for the token store (e.g., `/Users/ferris/.local/share/pyx/credentials`). root: PathBuf, /// The subdirectory for the token store (e.g., `/Users/ferris/.local/share/uv/credentials/3859a629b26fda96`). subdirectory: PathBuf, } impl PyxDirectories { /// Detect the [`PyxDirectories`] for a given API URL. fn from_api(api: &DisplaySafeUrl) -> Result { // Store credentials in a subdirectory based on the API URL. let digest = uv_cache_key::cache_digest(&CanonicalUrl::new(api)); // If the user explicitly set `PYX_CREDENTIALS_DIR`, use that. if let Some(root) = std::env::var_os(EnvVars::PYX_CREDENTIALS_DIR) { let root = std::path::absolute(root)?; let subdirectory = root.join(&digest); return Ok(Self { root, subdirectory }); } // If the user has pyx credentials in their uv credentials directory, read them for // backwards compatibility. let root = if let Some(tool_dir) = std::env::var_os(EnvVars::UV_CREDENTIALS_DIR) { std::path::absolute(tool_dir)? } else { StateStore::from_settings(None)?.bucket(StateBucket::Credentials) }; let subdirectory = root.join(&digest); if subdirectory.exists() { return Ok(Self { root, subdirectory }); } // Otherwise, use (e.g.) `~/.local/share/pyx`. let Ok(xdg) = etcetera::base_strategy::choose_base_strategy() else { return Err(io::Error::new( io::ErrorKind::NotFound, "Could not determine user data directory", )); }; let root = xdg.data_dir().join("pyx").join("credentials"); let subdirectory = root.join(&digest); Ok(Self { root, subdirectory }) } } #[derive(Debug, Clone)] pub struct PyxTokenStore { /// The root directory for the token store (e.g., `/Users/ferris/.local/share/pyx/credentials`). root: PathBuf, /// The subdirectory for the token store (e.g., `/Users/ferris/.local/share/uv/credentials/3859a629b26fda96`). subdirectory: PathBuf, /// The API URL for the token store (e.g., `https://api.pyx.dev`). api: DisplaySafeUrl, /// The CDN domain for the token store (e.g., `astralhosted.com`). cdn: SmallString, } impl PyxTokenStore { /// Create a new [`PyxTokenStore`] from settings. pub fn from_settings() -> Result { // Read the API URL and CDN domain from the environment variables, or fallback to the // defaults. let api = if let Ok(api_url) = std::env::var(EnvVars::PYX_API_URL) { DisplaySafeUrl::parse(&api_url) } else { DisplaySafeUrl::parse("https://api.pyx.dev") }?; let cdn = std::env::var(EnvVars::PYX_CDN_DOMAIN) .ok() .map(SmallString::from) .unwrap_or_else(|| SmallString::from(arcstr::literal!("astralhosted.com"))); // Determine the root directory for the token store. let PyxDirectories { root, subdirectory } = PyxDirectories::from_api(&api)?; Ok(Self { root, subdirectory, api, cdn, }) } /// Return the root directory for the token store. pub fn root(&self) -> &Path { &self.root } /// Return the API URL for the token store. pub fn api(&self) -> &DisplaySafeUrl { &self.api } /// Get or initialize an [`AccessToken`] from the store. /// /// If an access token is set in the environment, it will be returned as-is. /// /// If an access token is present on-disk, it will be returned (and refreshed, if necessary). /// /// If no access token is found, but an API key is present, the API key will be used to /// bootstrap an access token. pub async fn access_token( &self, client: &ClientWithMiddleware, tolerance_secs: u64, ) -> Result, TokenStoreError> { // If the access token is already set in the environment, return it. if let Some(access_token) = read_pyx_auth_token() { return Ok(Some(access_token)); } // Initialize the tokens from the store. let tokens = self.init(client, tolerance_secs).await?; // Extract the access token from the OAuth tokens or API key. Ok(tokens.map(AccessToken::from)) } /// Initialize the [`PyxTokens`] from the store. /// /// If an access token is already present, it will be returned (and refreshed, if necessary). /// /// If no access token is found, but an API key is present, the API key will be used to /// bootstrap an access token. pub async fn init( &self, client: &ClientWithMiddleware, tolerance_secs: u64, ) -> Result, TokenStoreError> { match self.read().await? { Some(tokens) => { // Refresh the tokens if they are expired. let tokens = self.refresh(tokens, client, tolerance_secs).await?; Ok(Some(tokens)) } None => { // If no tokens are present, bootstrap them from an API key. self.bootstrap(client).await } } } /// Write the tokens to the store. pub async fn write(&self, tokens: &PyxTokens) -> Result<(), TokenStoreError> { fs_err::tokio::create_dir_all(&self.subdirectory).await?; match tokens { PyxTokens::OAuth(tokens) => { // Write OAuth tokens to a generic `tokens.json` file. fs_err::tokio::write( self.subdirectory.join("tokens.json"), serde_json::to_vec(tokens)?, ) .await?; } PyxTokens::ApiKey(tokens) => { // Write API key tokens to a file based on the API key. let digest = uv_cache_key::cache_digest(&tokens.api_key); fs_err::tokio::write( self.subdirectory.join(format!("{digest}.json")), &tokens.access_token, ) .await?; } } Ok(()) } /// Returns `true` if the user appears to have an authentication token set. pub fn has_auth_token(&self) -> bool { read_pyx_auth_token().is_some() } /// Returns `true` if the user appears to have an API key set. pub fn has_api_key(&self) -> bool { read_pyx_api_key().is_some() } /// Returns `true` if the user appears to have OAuth tokens stored on disk. pub fn has_oauth_tokens(&self) -> bool { self.subdirectory.join("tokens.json").is_file() } /// Returns `true` if the user appears to have credentials (which may be invalid). pub fn has_credentials(&self) -> bool { self.has_auth_token() || self.has_api_key() || self.has_oauth_tokens() } /// Read the tokens from the store. pub async fn read(&self) -> Result, TokenStoreError> { if let Some(api_key) = read_pyx_api_key() { // Read the API key tokens from a file based on the API key. let digest = uv_cache_key::cache_digest(&api_key); match fs_err::tokio::read(self.subdirectory.join(format!("{digest}.json"))).await { Ok(data) => { let access_token = AccessToken::from(String::from_utf8(data).expect("Invalid UTF-8")); Ok(Some(PyxTokens::ApiKey(PyxApiKeyTokens { access_token, api_key, }))) } Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None), Err(err) => Err(err.into()), } } else { match fs_err::tokio::read(self.subdirectory.join("tokens.json")).await { Ok(data) => { let tokens: PyxOAuthTokens = serde_json::from_slice(&data)?; Ok(Some(PyxTokens::OAuth(tokens))) } Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None), Err(err) => Err(err.into()), } } } /// Remove the tokens from the store. pub async fn delete(&self) -> Result<(), io::Error> { fs_err::tokio::remove_dir_all(&self.subdirectory).await?; Ok(()) } /// Bootstrap the tokens from the store. async fn bootstrap( &self, client: &ClientWithMiddleware, ) -> Result, TokenStoreError> { #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] struct Payload { access_token: AccessToken, } // Retrieve the API key from the environment variable, if set. let Some(api_key) = read_pyx_api_key() else { return Ok(None); }; debug!("Bootstrapping access token from an API key"); // Parse the API URL. let mut url = self.api.clone(); url.set_path("auth/cli/access-token"); let mut request = reqwest::Request::new(reqwest::Method::POST, Url::from(url)); request.headers_mut().insert( "Authorization", reqwest::header::HeaderValue::from_str(&format!("Bearer {api_key}"))?, ); let response = client.execute(request).await?; let Payload { access_token } = response.error_for_status()?.json::().await?; let tokens = PyxTokens::ApiKey(PyxApiKeyTokens { access_token, api_key, }); // Write the tokens to disk. self.write(&tokens).await?; Ok(Some(tokens)) } /// Refresh the tokens in the store, if they are expired. /// /// In theory, we should _also_ refresh if we hit a 401; but for now, we only refresh ahead of /// time. async fn refresh( &self, tokens: PyxTokens, client: &ClientWithMiddleware, tolerance_secs: u64, ) -> Result { // Decode the access token. let jwt = PyxJwt::decode(match &tokens { PyxTokens::OAuth(PyxOAuthTokens { access_token, .. }) => access_token, PyxTokens::ApiKey(PyxApiKeyTokens { access_token, .. }) => access_token, })?; // If the access token is expired, refresh it. let is_up_to_date = match jwt.exp { None => { debug!("Access token has no expiration; refreshing..."); false } Some(..) if tolerance_secs == 0 => { debug!("Refreshing access token due to zero tolerance..."); false } Some(jwt) => { let exp = jiff::Timestamp::from_second(jwt)?; let now = jiff::Timestamp::now(); if exp < now { debug!("Access token is expired (`{exp}`); refreshing..."); false } else if exp < now + Duration::from_secs(tolerance_secs) { debug!( "Access token will expire within the tolerance (`{exp}`); refreshing..." ); false } else { debug!("Access token is up-to-date (`{exp}`)"); true } } }; if is_up_to_date { return Ok(tokens); } let tokens = match tokens { PyxTokens::OAuth(PyxOAuthTokens { refresh_token, .. }) => { // Parse the API URL. let mut url = self.api.clone(); url.set_path("auth/cli/refresh"); let mut request = reqwest::Request::new(reqwest::Method::POST, Url::from(url)); let body = serde_json::json!({ "refresh_token": refresh_token }); *request.body_mut() = Some(body.to_string().into()); let response = client.execute(request).await?; let tokens = response .error_for_status()? .json::() .await?; PyxTokens::OAuth(tokens) } PyxTokens::ApiKey(PyxApiKeyTokens { api_key, .. }) => { #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] struct Payload { access_token: AccessToken, } // Parse the API URL. let mut url = self.api.clone(); url.set_path("auth/cli/access-token"); let mut request = reqwest::Request::new(reqwest::Method::POST, Url::from(url)); request.headers_mut().insert( "Authorization", reqwest::header::HeaderValue::from_str(&format!("Bearer {api_key}"))?, ); let response = client.execute(request).await?; let Payload { access_token } = response.error_for_status()?.json::().await?; PyxTokens::ApiKey(PyxApiKeyTokens { access_token, api_key, }) } }; // Write the new tokens to disk. self.write(&tokens).await?; Ok(tokens) } /// Returns `true` if the given URL is "known" to this token store (i.e., should be /// authenticated using the store's tokens). pub fn is_known_url(&self, url: &Url) -> bool { is_known_url(url, &self.api, &self.cdn) } /// Returns `true` if the URL is on a "known" domain (i.e., the same domain as the API or CDN). /// /// Like [`is_known_url`](Self::is_known_url), but also returns `true` if the API is on the /// subdomain of the URL (e.g., if the API is `api.pyx.dev` and the URL is `pyx.dev`). pub fn is_known_domain(&self, url: &Url) -> bool { is_known_domain(url, &self.api, &self.cdn) } } #[derive(thiserror::Error, Debug)] pub enum TokenStoreError { #[error(transparent)] Url(#[from] DisplaySafeUrlError), #[error(transparent)] Io(#[from] io::Error), #[error(transparent)] Serialization(#[from] serde_json::Error), #[error(transparent)] Reqwest(#[from] reqwest::Error), #[error(transparent)] ReqwestMiddleware(#[from] reqwest_middleware::Error), #[error(transparent)] InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue), #[error(transparent)] Jiff(#[from] jiff::Error), #[error(transparent)] Jwt(#[from] JwtError), } impl TokenStoreError { /// Returns `true` if the error is a 401 (Unauthorized) error. pub fn is_unauthorized(&self) -> bool { match self { Self::Reqwest(err) => err.status() == Some(reqwest::StatusCode::UNAUTHORIZED), Self::ReqwestMiddleware(err) => err.status() == Some(reqwest::StatusCode::UNAUTHORIZED), _ => false, } } } /// The payload of the JWT. #[derive(Debug, serde::Deserialize)] pub struct PyxJwt { /// The expiration time of the JWT, as a Unix timestamp. pub exp: Option, /// The issuer of the JWT. pub iss: Option, /// The name of the organization, if any. #[serde(rename = "urn:pyx:org_name")] pub name: Option, } impl PyxJwt { /// Decode the JWT from the access token. pub fn decode(access_token: &AccessToken) -> Result { let mut token_segments = access_token.as_str().splitn(3, '.'); let _header = token_segments.next().ok_or(JwtError::MissingHeader)?; let payload = token_segments.next().ok_or(JwtError::MissingPayload)?; let _signature = token_segments.next().ok_or(JwtError::MissingSignature)?; if token_segments.next().is_some() { return Err(JwtError::TooManySegments); } let decoded = BASE64_URL_SAFE_NO_PAD.decode(payload)?; let jwt = serde_json::from_slice::(&decoded)?; Ok(jwt) } } #[derive(thiserror::Error, Debug)] pub enum JwtError { #[error("JWT is missing a header")] MissingHeader, #[error("JWT is missing a payload")] MissingPayload, #[error("JWT is missing a signature")] MissingSignature, #[error("JWT has too many segments")] TooManySegments, #[error(transparent)] Base64(#[from] base64::DecodeError), #[error(transparent)] Serde(#[from] serde_json::Error), } fn is_known_url(url: &Url, api: &DisplaySafeUrl, cdn: &str) -> bool { // Determine whether the URL matches the API realm. if Realm::from(url) == Realm::from(&**api) { return true; } // Determine whether the URL matches the CDN domain (or a subdomain of it). // // For example, if URL is on `files.astralhosted.com` and the CDN domain is // `astralhosted.com`, consider it known. if matches!(url.scheme(), "https") && matches_domain(url, cdn) { return true; } false } fn is_known_domain(url: &Url, api: &DisplaySafeUrl, cdn: &str) -> bool { // Determine whether the URL matches the API domain. if let Some(domain) = url.domain() { if matches_domain(api, domain) { return true; } } is_known_url(url, api, cdn) } /// Returns `true` if the target URL is on the given domain. fn matches_domain(url: &Url, domain: &str) -> bool { url.domain().is_some_and(|subdomain| { subdomain == domain || subdomain .strip_suffix(domain) .is_some_and(|prefix| prefix.ends_with('.')) }) } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_known_url() { let api_url = DisplaySafeUrl::parse("https://api.pyx.dev").unwrap(); let cdn_domain = "astralhosted.com"; // Same realm as API. assert!(is_known_url( &Url::parse("https://api.pyx.dev/simple/").unwrap(), &api_url, cdn_domain )); // Different path on same API domain assert!(is_known_url( &Url::parse("https://api.pyx.dev/v1/").unwrap(), &api_url, cdn_domain )); // CDN domain. assert!(is_known_url( &Url::parse("https://astralhosted.com/packages/").unwrap(), &api_url, cdn_domain )); // CDN subdomain. assert!(is_known_url( &Url::parse("https://files.astralhosted.com/packages/").unwrap(), &api_url, cdn_domain )); // CDN on HTTP. assert!(!is_known_url( &Url::parse("http://astralhosted.com/packages/").unwrap(), &api_url, cdn_domain )); // Unknown domain. assert!(!is_known_url( &Url::parse("https://pypi.org/simple/").unwrap(), &api_url, cdn_domain )); // Similar but not matching domain. assert!(!is_known_url( &Url::parse("https://badastralhosted.com/packages/").unwrap(), &api_url, cdn_domain )); } #[test] fn test_is_known_domain() { let api_url = DisplaySafeUrl::parse("https://api.pyx.dev").unwrap(); let cdn_domain = "astralhosted.com"; // Same realm as API. assert!(is_known_domain( &Url::parse("https://api.pyx.dev/simple/").unwrap(), &api_url, cdn_domain )); // API super-domain. assert!(is_known_domain( &Url::parse("https://pyx.dev").unwrap(), &api_url, cdn_domain )); // API subdomain. assert!(!is_known_domain( &Url::parse("https://foo.api.pyx.dev").unwrap(), &api_url, cdn_domain )); // Different subdomain. assert!(!is_known_domain( &Url::parse("https://beta.pyx.dev/").unwrap(), &api_url, cdn_domain )); // CDN domain. assert!(is_known_domain( &Url::parse("https://astralhosted.com/packages/").unwrap(), &api_url, cdn_domain )); // CDN subdomain. assert!(is_known_domain( &Url::parse("https://files.astralhosted.com/packages/").unwrap(), &api_url, cdn_domain )); // Unknown domain. assert!(!is_known_domain( &Url::parse("https://pypi.org/simple/").unwrap(), &api_url, cdn_domain )); // Different TLD. assert!(!is_known_domain( &Url::parse("https://pyx.com/").unwrap(), &api_url, cdn_domain )); } #[test] fn test_matches_domain() { assert!(matches_domain( &Url::parse("https://example.com").unwrap(), "example.com" )); assert!(matches_domain( &Url::parse("https://foo.example.com").unwrap(), "example.com" )); assert!(matches_domain( &Url::parse("https://bar.foo.example.com").unwrap(), "example.com" )); assert!(!matches_domain( &Url::parse("https://example.com").unwrap(), "other.com" )); assert!(!matches_domain( &Url::parse("https://example.org").unwrap(), "example.com" )); assert!(!matches_domain( &Url::parse("https://badexample.com").unwrap(), "example.com" )); } } uv-0.9.17+ds1/crates/uv-auth/src/realm.rs000066400000000000000000000263171520155276700200570ustar00rootroot00000000000000use std::hash::{Hash, Hasher}; use std::{fmt::Display, fmt::Formatter}; use url::Url; use uv_redacted::DisplaySafeUrl; use uv_small_str::SmallString; /// Used to determine if authentication information should be retained on a new URL. /// Based on the specification defined in RFC 7235 and 7230. /// /// /// // // The "scheme" and "authority" components must match to retain authentication // The "authority", is composed of the host and port. // // The scheme must always be an exact match. // Note some clients such as Python's `requests` library allow an upgrade // from `http` to `https` but this is not spec-compliant. // // // The host must always be an exact match. // // The port is only allowed to differ if it matches the "default port" for the scheme. // However, `url` (and therefore `reqwest`) sets the `port` to `None` if it matches the default port // so we do not need any special handling here. #[derive(Debug, Clone)] pub struct Realm { scheme: SmallString, host: Option, port: Option, } impl From<&DisplaySafeUrl> for Realm { fn from(url: &DisplaySafeUrl) -> Self { Self::from(&**url) } } impl From<&Url> for Realm { fn from(url: &Url) -> Self { Self { scheme: SmallString::from(url.scheme()), host: url.host_str().map(SmallString::from), port: url.port(), } } } impl Display for Realm { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if let Some(port) = self.port { write!( f, "{}://{}:{port}", self.scheme, self.host.as_deref().unwrap_or_default() ) } else { write!( f, "{}://{}", self.scheme, self.host.as_deref().unwrap_or_default() ) } } } impl PartialEq for Realm { fn eq(&self, other: &Self) -> bool { RealmRef::from(self) == RealmRef::from(other) } } impl Eq for Realm {} impl Hash for Realm { fn hash(&self, state: &mut H) { RealmRef::from(self).hash(state); } } /// A reference to a [`Realm`] that can be used for zero-allocation comparisons. #[derive(Debug, Copy, Clone)] pub struct RealmRef<'a> { scheme: &'a str, host: Option<&'a str>, port: Option, } impl RealmRef<'_> { /// Returns true if this realm is a subdomain of the other realm. pub(crate) fn is_subdomain_of(&self, other: Self) -> bool { other.scheme == self.scheme && other.port == self.port && other.host.is_some_and(|other_host| { self.host.is_some_and(|self_host| { self_host .strip_suffix(other_host) .is_some_and(|prefix| prefix.ends_with('.')) }) }) } } impl<'a> From<&'a Url> for RealmRef<'a> { fn from(url: &'a Url) -> Self { Self { scheme: url.scheme(), host: url.host_str(), port: url.port(), } } } impl PartialEq for RealmRef<'_> { fn eq(&self, other: &Self) -> bool { self.scheme == other.scheme && self.host == other.host && self.port == other.port } } impl Eq for RealmRef<'_> {} impl Hash for RealmRef<'_> { fn hash(&self, state: &mut H) { self.scheme.hash(state); self.host.hash(state); self.port.hash(state); } } impl<'a> PartialEq> for Realm { fn eq(&self, rhs: &RealmRef<'a>) -> bool { RealmRef::from(self) == *rhs } } impl PartialEq for RealmRef<'_> { fn eq(&self, rhs: &Realm) -> bool { *self == RealmRef::from(rhs) } } impl<'a> From<&'a Realm> for RealmRef<'a> { fn from(realm: &'a Realm) -> Self { Self { scheme: &realm.scheme, host: realm.host.as_deref(), port: realm.port, } } } #[cfg(test)] mod tests { use url::{ParseError, Url}; use crate::Realm; #[test] fn test_should_retain_auth() -> Result<(), ParseError> { // Exact match (https) assert_eq!( Realm::from(&Url::parse("https://example.com")?), Realm::from(&Url::parse("https://example.com")?) ); // Exact match (with port) assert_eq!( Realm::from(&Url::parse("https://example.com:1234")?), Realm::from(&Url::parse("https://example.com:1234")?) ); // Exact match (http) assert_eq!( Realm::from(&Url::parse("http://example.com")?), Realm::from(&Url::parse("http://example.com")?) ); // Okay, path differs assert_eq!( Realm::from(&Url::parse("http://example.com/foo")?), Realm::from(&Url::parse("http://example.com/bar")?) ); // Okay, default port differs (https) assert_eq!( Realm::from(&Url::parse("https://example.com:443")?), Realm::from(&Url::parse("https://example.com")?) ); // Okay, default port differs (http) assert_eq!( Realm::from(&Url::parse("http://example.com:80")?), Realm::from(&Url::parse("http://example.com")?) ); // Mismatched scheme assert_ne!( Realm::from(&Url::parse("https://example.com")?), Realm::from(&Url::parse("http://example.com")?) ); // Mismatched scheme, we explicitly do not allow upgrade to https assert_ne!( Realm::from(&Url::parse("http://example.com")?), Realm::from(&Url::parse("https://example.com")?) ); // Mismatched host assert_ne!( Realm::from(&Url::parse("https://foo.com")?), Realm::from(&Url::parse("https://bar.com")?) ); // Mismatched port assert_ne!( Realm::from(&Url::parse("https://example.com:1234")?), Realm::from(&Url::parse("https://example.com:5678")?) ); // Mismatched port, with one as default for scheme assert_ne!( Realm::from(&Url::parse("https://example.com:443")?), Realm::from(&Url::parse("https://example.com:5678")?) ); assert_ne!( Realm::from(&Url::parse("https://example.com:1234")?), Realm::from(&Url::parse("https://example.com:443")?) ); // Mismatched port, with default for a different scheme assert_ne!( Realm::from(&Url::parse("https://example.com:80")?), Realm::from(&Url::parse("https://example.com")?) ); Ok(()) } #[test] fn test_is_subdomain_of() -> Result<(), ParseError> { use crate::realm::RealmRef; // Subdomain relationship: sub.example.com is a subdomain of example.com let subdomain_url = Url::parse("https://sub.example.com")?; let domain_url = Url::parse("https://example.com")?; let subdomain = RealmRef::from(&subdomain_url); let domain = RealmRef::from(&domain_url); assert!(subdomain.is_subdomain_of(domain)); // Deeper subdomain: foo.bar.example.com is a subdomain of example.com let deep_subdomain_url = Url::parse("https://foo.bar.example.com")?; let deep_subdomain = RealmRef::from(&deep_subdomain_url); assert!(deep_subdomain.is_subdomain_of(domain)); // Deeper subdomain: foo.bar.example.com is also a subdomain of bar.example.com let parent_subdomain_url = Url::parse("https://bar.example.com")?; let parent_subdomain = RealmRef::from(&parent_subdomain_url); assert!(deep_subdomain.is_subdomain_of(parent_subdomain)); // Not a subdomain: example.com is not a subdomain of sub.example.com assert!(!domain.is_subdomain_of(subdomain)); // Same domain is not a subdomain of itself assert!(!domain.is_subdomain_of(domain)); // Different TLD: example.org is not a subdomain of example.com let different_tld_url = Url::parse("https://example.org")?; let different_tld = RealmRef::from(&different_tld_url); assert!(!different_tld.is_subdomain_of(domain)); // Partial match but not a subdomain: notexample.com is not a subdomain of example.com let partial_match_url = Url::parse("https://notexample.com")?; let partial_match = RealmRef::from(&partial_match_url); assert!(!partial_match.is_subdomain_of(domain)); // Different scheme: http subdomain is not a subdomain of https domain let http_subdomain_url = Url::parse("http://sub.example.com")?; let https_domain_url = Url::parse("https://example.com")?; let http_subdomain = RealmRef::from(&http_subdomain_url); let https_domain = RealmRef::from(&https_domain_url); assert!(!http_subdomain.is_subdomain_of(https_domain)); // Different port: same subdomain with different port is not a subdomain let subdomain_port_8080_url = Url::parse("https://sub.example.com:8080")?; let domain_port_9090_url = Url::parse("https://example.com:9090")?; let subdomain_port_8080 = RealmRef::from(&subdomain_port_8080_url); let domain_port_9090 = RealmRef::from(&domain_port_9090_url); assert!(!subdomain_port_8080.is_subdomain_of(domain_port_9090)); // Same port: subdomain with same explicit port is a subdomain let subdomain_with_port_url = Url::parse("https://sub.example.com:8080")?; let domain_with_port_url = Url::parse("https://example.com:8080")?; let subdomain_with_port = RealmRef::from(&subdomain_with_port_url); let domain_with_port = RealmRef::from(&domain_with_port_url); assert!(subdomain_with_port.is_subdomain_of(domain_with_port)); // Default port handling: subdomain with implicit port is a subdomain let subdomain_default_url = Url::parse("https://sub.example.com")?; let domain_explicit_443_url = Url::parse("https://example.com:443")?; let subdomain_default = RealmRef::from(&subdomain_default_url); let domain_explicit_443 = RealmRef::from(&domain_explicit_443_url); assert!(subdomain_default.is_subdomain_of(domain_explicit_443)); // Edge case: empty host (shouldn't happen with valid URLs but testing defensive code) let file_url = Url::parse("file:///path/to/file")?; let https_url = Url::parse("https://example.com")?; let file_realm = RealmRef::from(&file_url); let https_realm = RealmRef::from(&https_url); assert!(!file_realm.is_subdomain_of(https_realm)); assert!(!https_realm.is_subdomain_of(file_realm)); // Subdomain with path (path should be ignored) let subdomain_with_path_url = Url::parse("https://sub.example.com/path")?; let domain_with_path_url = Url::parse("https://example.com/other")?; let subdomain_with_path = RealmRef::from(&subdomain_with_path_url); let domain_with_path = RealmRef::from(&domain_with_path_url); assert!(subdomain_with_path.is_subdomain_of(domain_with_path)); Ok(()) } } uv-0.9.17+ds1/crates/uv-auth/src/service.rs000066400000000000000000000052571520155276700204170ustar00rootroot00000000000000use serde::{Deserialize, Serialize}; use std::str::FromStr; use thiserror::Error; use url::Url; use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError}; #[derive(Error, Debug)] pub enum ServiceParseError { #[error(transparent)] InvalidUrl(#[from] DisplaySafeUrlError), #[error("Unsupported scheme: {0}")] UnsupportedScheme(String), #[error("HTTPS is required for non-local hosts")] HttpsRequired, } /// A service URL that wraps [`DisplaySafeUrl`] for CLI usage. /// /// This type provides automatic URL parsing and validation when used as a CLI argument, /// eliminating the need for manual parsing in command functions. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(transparent)] pub struct Service(DisplaySafeUrl); impl Service { /// Get the underlying [`DisplaySafeUrl`]. pub fn url(&self) -> &DisplaySafeUrl { &self.0 } /// Convert into the underlying [`DisplaySafeUrl`]. pub fn into_url(self) -> DisplaySafeUrl { self.0 } /// Validate that the URL scheme is supported. fn check_scheme(url: &Url) -> Result<(), ServiceParseError> { match url.scheme() { "https" => Ok(()), "http" if matches!(url.host_str(), Some("localhost" | "127.0.0.1")) => Ok(()), "http" => Err(ServiceParseError::HttpsRequired), value => Err(ServiceParseError::UnsupportedScheme(value.to_string())), } } } impl FromStr for Service { type Err = ServiceParseError; fn from_str(s: &str) -> Result { // First try parsing as-is let url = match DisplaySafeUrl::parse(s) { Ok(url) => url, Err(DisplaySafeUrlError::Url(url::ParseError::RelativeUrlWithoutBase)) => { // If it's a relative URL, try prepending https:// let with_https = format!("https://{s}"); DisplaySafeUrl::parse(&with_https)? } Err(err) => return Err(err.into()), }; Self::check_scheme(&url)?; Ok(Self(url)) } } impl std::fmt::Display for Service { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } impl TryFrom for Service { type Error = ServiceParseError; fn try_from(value: String) -> Result { Self::from_str(&value) } } impl From for String { fn from(service: Service) -> Self { service.to_string() } } impl TryFrom for Service { type Error = ServiceParseError; fn try_from(value: DisplaySafeUrl) -> Result { Self::check_scheme(&value)?; Ok(Self(value)) } } uv-0.9.17+ds1/crates/uv-auth/src/store.rs000066400000000000000000000611301520155276700201030ustar00rootroot00000000000000use std::ops::Deref; use std::path::{Path, PathBuf}; use fs_err as fs; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use thiserror::Error; use uv_fs::{LockedFile, LockedFileError, LockedFileMode, with_added_extension}; use uv_preview::{Preview, PreviewFeatures}; use uv_redacted::DisplaySafeUrl; use uv_state::{StateBucket, StateStore}; use uv_static::EnvVars; use crate::credentials::{Password, Token, Username}; use crate::realm::Realm; use crate::service::Service; use crate::{Credentials, KeyringProvider}; /// The storage backend to use in `uv auth` commands. #[derive(Debug)] pub enum AuthBackend { // TODO(zanieb): Right now, we're using a keyring provider for the system store but that's just // where the native implementation is living at the moment. We should consider refactoring these // into a shared API in the future. System(KeyringProvider), TextStore(TextCredentialStore, LockedFile), } impl AuthBackend { pub async fn from_settings(preview: Preview) -> Result { // If preview is enabled, we'll use the system-native store if preview.is_enabled(PreviewFeatures::NATIVE_AUTH) { return Ok(Self::System(KeyringProvider::native())); } // Otherwise, we'll use the plaintext credential store let path = TextCredentialStore::default_file()?; match TextCredentialStore::read(&path).await { Ok((store, lock)) => Ok(Self::TextStore(store, lock)), Err(err) if err .as_io_error() .is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound) => { Ok(Self::TextStore( TextCredentialStore::default(), TextCredentialStore::lock(&path).await?, )) } Err(err) => Err(err), } } } /// Authentication scheme to use. #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum AuthScheme { /// HTTP Basic Authentication /// /// Uses a username and password. #[default] Basic, /// Bearer token authentication. /// /// Uses a token provided as `Bearer ` in the `Authorization` header. Bearer, } /// Errors that can occur when working with TOML credential storage. #[derive(Debug, Error)] pub enum TomlCredentialError { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] LockedFile(#[from] LockedFileError), #[error("Failed to parse TOML credential file: {0}")] ParseError(#[from] toml::de::Error), #[error("Failed to serialize credentials to TOML")] SerializeError(#[from] toml::ser::Error), #[error(transparent)] BasicAuthError(#[from] BasicAuthError), #[error(transparent)] BearerAuthError(#[from] BearerAuthError), #[error("Failed to determine credentials directory")] CredentialsDirError, #[error("Token is not valid unicode")] TokenNotUnicode(#[from] std::string::FromUtf8Error), } impl TomlCredentialError { pub fn as_io_error(&self) -> Option<&std::io::Error> { match self { Self::Io(err) => Some(err), Self::LockedFile(err) => err.as_io_error(), Self::ParseError(_) | Self::SerializeError(_) | Self::BasicAuthError(_) | Self::BearerAuthError(_) | Self::CredentialsDirError | Self::TokenNotUnicode(_) => None, } } } #[derive(Debug, Error)] pub enum BasicAuthError { #[error("`username` is required with `scheme = basic`")] MissingUsername, #[error("`token` cannot be provided with `scheme = basic`")] UnexpectedToken, } #[derive(Debug, Error)] pub enum BearerAuthError { #[error("`token` is required with `scheme = bearer`")] MissingToken, #[error("`username` cannot be provided with `scheme = bearer`")] UnexpectedUsername, #[error("`password` cannot be provided with `scheme = bearer`")] UnexpectedPassword, } /// A single credential entry in a TOML credentials file. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(try_from = "TomlCredentialWire", into = "TomlCredentialWire")] struct TomlCredential { /// The service URL for this credential. service: Service, /// The credentials for this entry. credentials: Credentials, } #[derive(Debug, Clone, Serialize, Deserialize)] struct TomlCredentialWire { /// The service URL for this credential. service: Service, /// The username to use. Only allowed with [`AuthScheme::Basic`]. username: Username, /// The authentication scheme. #[serde(default)] scheme: AuthScheme, /// The password to use. Only allowed with [`AuthScheme::Basic`]. password: Option, /// The token to use. Only allowed with [`AuthScheme::Bearer`]. token: Option, } impl From for TomlCredentialWire { fn from(value: TomlCredential) -> Self { match value.credentials { Credentials::Basic { username, password } => Self { service: value.service, username, scheme: AuthScheme::Basic, password, token: None, }, Credentials::Bearer { token } => Self { service: value.service, username: Username::new(None), scheme: AuthScheme::Bearer, password: None, token: Some(String::from_utf8(token.into_bytes()).expect("Token is valid UTF-8")), }, } } } impl TryFrom for TomlCredential { type Error = TomlCredentialError; fn try_from(value: TomlCredentialWire) -> Result { match value.scheme { AuthScheme::Basic => { if value.username.as_deref().is_none() { return Err(TomlCredentialError::BasicAuthError( BasicAuthError::MissingUsername, )); } if value.token.is_some() { return Err(TomlCredentialError::BasicAuthError( BasicAuthError::UnexpectedToken, )); } let credentials = Credentials::Basic { username: value.username, password: value.password, }; Ok(Self { service: value.service, credentials, }) } AuthScheme::Bearer => { if value.username.is_some() { return Err(TomlCredentialError::BearerAuthError( BearerAuthError::UnexpectedUsername, )); } if value.password.is_some() { return Err(TomlCredentialError::BearerAuthError( BearerAuthError::UnexpectedPassword, )); } if value.token.is_none() { return Err(TomlCredentialError::BearerAuthError( BearerAuthError::MissingToken, )); } let credentials = Credentials::Bearer { token: Token::new(value.token.unwrap().into_bytes()), }; Ok(Self { service: value.service, credentials, }) } } } } #[derive(Debug, Clone, Serialize, Deserialize, Default)] struct TomlCredentials { /// Array of credential entries. #[serde(rename = "credential")] credentials: Vec, } /// A credential store with a plain text storage backend. #[derive(Debug, Default)] pub struct TextCredentialStore { credentials: FxHashMap<(Service, Username), Credentials>, } impl TextCredentialStore { /// Return the directory for storing credentials. pub fn directory_path() -> Result { if let Some(dir) = std::env::var_os(EnvVars::UV_CREDENTIALS_DIR) .filter(|s| !s.is_empty()) .map(PathBuf::from) { return Ok(dir); } Ok(StateStore::from_settings(None)?.bucket(StateBucket::Credentials)) } /// Return the standard file path for storing credentials. pub fn default_file() -> Result { let dir = Self::directory_path()?; Ok(dir.join("credentials.toml")) } /// Acquire a lock on the credentials file at the given path. pub async fn lock(path: &Path) -> Result { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } let lock = with_added_extension(path, ".lock"); Ok(LockedFile::acquire(lock, LockedFileMode::Exclusive, "credentials store").await?) } /// Read credentials from a file. fn from_file>(path: P) -> Result { let content = fs::read_to_string(path)?; let credentials: TomlCredentials = toml::from_str(&content)?; let credentials: FxHashMap<(Service, Username), Credentials> = credentials .credentials .into_iter() .map(|credential| { let username = match &credential.credentials { Credentials::Basic { username, .. } => username.clone(), Credentials::Bearer { .. } => Username::none(), }; ( (credential.service.clone(), username), credential.credentials, ) }) .collect(); Ok(Self { credentials }) } /// Read credentials from a file. /// /// Returns [`TextCredentialStore`] and a [`LockedFile`] to hold if mutating the store. /// /// If the store will not be written to following the read, the lock can be dropped. pub async fn read>(path: P) -> Result<(Self, LockedFile), TomlCredentialError> { let lock = Self::lock(path.as_ref()).await?; let store = Self::from_file(path)?; Ok((store, lock)) } /// Persist credentials to a file. /// /// Requires a [`LockedFile`] from [`TextCredentialStore::lock`] or /// [`TextCredentialStore::read`] to ensure exclusive access. pub fn write>( self, path: P, _lock: LockedFile, ) -> Result<(), TomlCredentialError> { let credentials = self .credentials .into_iter() .map(|((service, _username), credentials)| TomlCredential { service, credentials, }) .collect::>(); let toml_creds = TomlCredentials { credentials }; let content = toml::to_string_pretty(&toml_creds)?; fs::create_dir_all( path.as_ref() .parent() .ok_or(TomlCredentialError::CredentialsDirError)?, )?; // TODO(zanieb): We should use an atomic write here fs::write(path, content)?; Ok(()) } /// Get credentials for a given URL and username. /// /// The most specific URL prefix match in the same [`Realm`] is returned, if any. pub fn get_credentials( &self, url: &DisplaySafeUrl, username: Option<&str>, ) -> Option<&Credentials> { let request_realm = Realm::from(url); // Perform an exact lookup first // TODO(zanieb): Consider adding `DisplaySafeUrlRef` so we can avoid this clone // TODO(zanieb): We could also return early here if we can't normalize to a `Service` if let Ok(url_service) = Service::try_from(url.clone()) { if let Some(credential) = self.credentials.get(&( url_service.clone(), Username::from(username.map(str::to_string)), )) { return Some(credential); } } // If that fails, iterate through to find a prefix match let mut best: Option<(usize, &Service, &Credentials)> = None; for ((service, stored_username), credential) in &self.credentials { let service_realm = Realm::from(service.url().deref()); // Only consider services in the same realm if service_realm != request_realm { continue; } // Service path must be a prefix of request path if !url.path().starts_with(service.url().path()) { continue; } // If a username is provided, it must match if let Some(request_username) = username { if Some(request_username) != stored_username.as_deref() { continue; } } // Update our best matching credential based on prefix length let specificity = service.url().path().len(); if best.is_none_or(|(best_specificity, _, _)| specificity > best_specificity) { best = Some((specificity, service, credential)); } } // Return the most specific match if let Some((_, _, credential)) = best { return Some(credential); } None } /// Store credentials for a given service. pub fn insert(&mut self, service: Service, credentials: Credentials) -> Option { let username = match &credentials { Credentials::Basic { username, .. } => username.clone(), Credentials::Bearer { .. } => Username::none(), }; self.credentials.insert((service, username), credentials) } /// Remove credentials for a given service. pub fn remove(&mut self, service: &Service, username: Username) -> Option { // Remove the specific credential for this service and username self.credentials.remove(&(service.clone(), username)) } } #[cfg(test)] mod tests { use std::io::Write; use std::str::FromStr; use tempfile::NamedTempFile; use super::*; #[test] fn test_toml_serialization() { let credentials = TomlCredentials { credentials: vec![ TomlCredential { service: Service::from_str("https://example.com").unwrap(), credentials: Credentials::Basic { username: Username::new(Some("user1".to_string())), password: Some(Password::new("pass1".to_string())), }, }, TomlCredential { service: Service::from_str("https://test.org").unwrap(), credentials: Credentials::Basic { username: Username::new(Some("user2".to_string())), password: Some(Password::new("pass2".to_string())), }, }, ], }; let toml_str = toml::to_string_pretty(&credentials).unwrap(); let parsed: TomlCredentials = toml::from_str(&toml_str).unwrap(); assert_eq!(parsed.credentials.len(), 2); assert_eq!( parsed.credentials[0].service.to_string(), "https://example.com/" ); assert_eq!( parsed.credentials[1].service.to_string(), "https://test.org/" ); } #[test] fn test_credential_store_operations() { let mut store = TextCredentialStore::default(); let credentials = Credentials::basic(Some("user".to_string()), Some("pass".to_string())); let service = Service::from_str("https://example.com").unwrap(); store.insert(service.clone(), credentials.clone()); let url = DisplaySafeUrl::parse("https://example.com/").unwrap(); assert!(store.get_credentials(&url, None).is_some()); let url = DisplaySafeUrl::parse("https://example.com/path").unwrap(); let retrieved = store.get_credentials(&url, None).unwrap(); assert_eq!(retrieved.username(), Some("user")); assert_eq!(retrieved.password(), Some("pass")); assert!( store .remove(&service, Username::from(Some("user".to_string()))) .is_some() ); let url = DisplaySafeUrl::parse("https://example.com/").unwrap(); assert!(store.get_credentials(&url, None).is_none()); } #[tokio::test] async fn test_file_operations() { let mut temp_file = NamedTempFile::new().unwrap(); writeln!( temp_file, r#" [[credential]] service = "https://example.com" username = "testuser" scheme = "basic" password = "testpass" [[credential]] service = "https://test.org" username = "user2" password = "pass2" "# ) .unwrap(); let store = TextCredentialStore::from_file(temp_file.path()).unwrap(); let url = DisplaySafeUrl::parse("https://example.com/").unwrap(); assert!(store.get_credentials(&url, None).is_some()); let url = DisplaySafeUrl::parse("https://test.org/").unwrap(); assert!(store.get_credentials(&url, None).is_some()); let url = DisplaySafeUrl::parse("https://example.com").unwrap(); let cred = store.get_credentials(&url, None).unwrap(); assert_eq!(cred.username(), Some("testuser")); assert_eq!(cred.password(), Some("testpass")); // Test saving let temp_output = NamedTempFile::new().unwrap(); store .write( temp_output.path(), TextCredentialStore::lock(temp_file.path()).await.unwrap(), ) .unwrap(); let content = fs::read_to_string(temp_output.path()).unwrap(); assert!(content.contains("example.com")); assert!(content.contains("testuser")); } #[test] fn test_prefix_matching() { let mut store = TextCredentialStore::default(); let credentials = Credentials::basic(Some("user".to_string()), Some("pass".to_string())); // Store credentials for a specific path prefix let service = Service::from_str("https://example.com/api").unwrap(); store.insert(service.clone(), credentials.clone()); // Should match URLs that are prefixes of the stored service let matching_urls = [ "https://example.com/api", "https://example.com/api/v1", "https://example.com/api/v1/users", ]; for url_str in matching_urls { let url = DisplaySafeUrl::parse(url_str).unwrap(); let cred = store.get_credentials(&url, None); assert!(cred.is_some(), "Failed to match URL with prefix: {url_str}"); } // Should NOT match URLs that are not prefixes let non_matching_urls = [ "https://example.com/different", "https://example.com/ap", // Not a complete path segment match "https://example.com", // Shorter than the stored prefix ]; for url_str in non_matching_urls { let url = DisplaySafeUrl::parse(url_str).unwrap(); let cred = store.get_credentials(&url, None); assert!(cred.is_none(), "Should not match non-prefix URL: {url_str}"); } } #[test] fn test_realm_based_matching() { let mut store = TextCredentialStore::default(); let credentials = Credentials::basic(Some("user".to_string()), Some("pass".to_string())); // Store by full URL (realm) let service = Service::from_str("https://example.com").unwrap(); store.insert(service.clone(), credentials.clone()); // Should match URLs in the same realm let matching_urls = [ "https://example.com", "https://example.com/path", "https://example.com/different/path", "https://example.com:443/path", // Default HTTPS port ]; for url_str in matching_urls { let url = DisplaySafeUrl::parse(url_str).unwrap(); let cred = store.get_credentials(&url, None); assert!( cred.is_some(), "Failed to match URL in same realm: {url_str}" ); } // Should NOT match URLs in different realms let non_matching_urls = [ "http://example.com", // Different scheme "https://different.com", // Different host "https://example.com:8080", // Different port ]; for url_str in non_matching_urls { let url = DisplaySafeUrl::parse(url_str).unwrap(); let cred = store.get_credentials(&url, None); assert!( cred.is_none(), "Should not match URL in different realm: {url_str}" ); } } #[test] fn test_most_specific_prefix_matching() { let mut store = TextCredentialStore::default(); let general_cred = Credentials::basic(Some("general".to_string()), Some("pass1".to_string())); let specific_cred = Credentials::basic(Some("specific".to_string()), Some("pass2".to_string())); // Store credentials with different prefix lengths let general_service = Service::from_str("https://example.com/api").unwrap(); let specific_service = Service::from_str("https://example.com/api/v1").unwrap(); store.insert(general_service.clone(), general_cred); store.insert(specific_service.clone(), specific_cred); // Should match the most specific prefix let url = DisplaySafeUrl::parse("https://example.com/api/v1/users").unwrap(); let cred = store.get_credentials(&url, None).unwrap(); assert_eq!(cred.username(), Some("specific")); // Should match the general prefix for non-specific paths let url = DisplaySafeUrl::parse("https://example.com/api/v2").unwrap(); let cred = store.get_credentials(&url, None).unwrap(); assert_eq!(cred.username(), Some("general")); } #[test] fn test_username_exact_url_match() { let mut store = TextCredentialStore::default(); let url = DisplaySafeUrl::parse("https://example.com").unwrap(); let service = Service::from_str("https://example.com").unwrap(); let user1_creds = Credentials::basic(Some("user1".to_string()), Some("pass1".to_string())); store.insert(service.clone(), user1_creds.clone()); // Should return credentials when username matches let result = store.get_credentials(&url, Some("user1")); assert!(result.is_some()); assert_eq!(result.unwrap().username(), Some("user1")); assert_eq!(result.unwrap().password(), Some("pass1")); // Should not return credentials when username doesn't match let result = store.get_credentials(&url, Some("user2")); assert!(result.is_none()); // Should return credentials when no username is specified let result = store.get_credentials(&url, None); assert!(result.is_some()); assert_eq!(result.unwrap().username(), Some("user1")); } #[test] fn test_username_prefix_url_match() { let mut store = TextCredentialStore::default(); // Add credentials with different usernames for overlapping URL prefixes let general_service = Service::from_str("https://example.com/api").unwrap(); let specific_service = Service::from_str("https://example.com/api/v1").unwrap(); let general_creds = Credentials::basic( Some("general_user".to_string()), Some("general_pass".to_string()), ); let specific_creds = Credentials::basic( Some("specific_user".to_string()), Some("specific_pass".to_string()), ); store.insert(general_service, general_creds); store.insert(specific_service, specific_creds); let url = DisplaySafeUrl::parse("https://example.com/api/v1/users").unwrap(); // Should match specific credentials when username matches let result = store.get_credentials(&url, Some("specific_user")); assert!(result.is_some()); assert_eq!(result.unwrap().username(), Some("specific_user")); // Should match the general credentials when requesting general_user (falls back to less specific prefix) let result = store.get_credentials(&url, Some("general_user")); assert!( result.is_some(), "Should match general_user from less specific prefix" ); assert_eq!(result.unwrap().username(), Some("general_user")); // Should match most specific when no username specified let result = store.get_credentials(&url, None); assert!(result.is_some()); assert_eq!(result.unwrap().username(), Some("specific_user")); } } uv-0.9.17+ds1/crates/uv-bench/000077500000000000000000000000001520155276700157275ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-bench/Cargo.toml000066400000000000000000000025671520155276700176710ustar00rootroot00000000000000[package] name = "uv-bench" version = "0.0.7" description = "This is an internal component crate of uv" publish = false authors = { workspace = true } edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } license = { workspace = true } [lints] workspace = true [lib] doctest = false bench = false [[bench]] name = "uv" path = "benches/uv.rs" harness = false [dependencies] uv-cache = { workspace = true } uv-client = { workspace = true } uv-configuration = { workspace = true } uv-dispatch = { workspace = true } uv-distribution = { workspace = true } uv-distribution-types = { workspace = true } uv-extract = { workspace = true, optional = true } uv-install-wheel = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-platform-tags = { workspace = true } uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-resolver = { workspace = true } uv-types = { workspace = true } uv-workspace = { workspace = true } anyhow = { workspace = true } criterion = { version = "4.0.3", default-features = false, package = "codspeed-criterion-compat", features = ["async_tokio"] } jiff = { workspace = true } tokio = { workspace = true } [package.metadata.cargo-shear] ignored = ["uv-extract"] [features] static = ["uv-extract/static"] uv-0.9.17+ds1/crates/uv-bench/README.md000066400000000000000000000010231520155276700172020ustar00rootroot00000000000000 # uv-bench This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-bench). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-bench/benches/000077500000000000000000000000001520155276700173365ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-bench/benches/uv.rs000066400000000000000000000201151520155276700203350ustar00rootroot00000000000000use std::hint::black_box; use std::str::FromStr; use criterion::{Criterion, criterion_group, criterion_main, measurement::WallTime}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; use uv_distribution_types::Requirement; use uv_python::PythonEnvironment; use uv_resolver::Manifest; fn resolve_warm_jupyter(c: &mut Criterion) { let run = setup(Manifest::simple(vec![Requirement::from( uv_pep508::Requirement::from_str("jupyter==1.0.0").unwrap(), )])); c.bench_function("resolve_warm_jupyter", |b| b.iter(|| run(false))); } fn resolve_warm_jupyter_universal(c: &mut Criterion) { let run = setup(Manifest::simple(vec![Requirement::from( uv_pep508::Requirement::from_str("jupyter==1.0.0").unwrap(), )])); c.bench_function("resolve_warm_jupyter_universal", |b| b.iter(|| run(true))); } fn resolve_warm_airflow(c: &mut Criterion) { let run = setup(Manifest::simple(vec![ Requirement::from(uv_pep508::Requirement::from_str("apache-airflow[all]==2.9.3").unwrap()), Requirement::from( uv_pep508::Requirement::from_str("apache-airflow-providers-apache-beam>3.0.0").unwrap(), ), ])); c.bench_function("resolve_warm_airflow", |b| b.iter(|| run(false))); } // This takes >5m to run in CodSpeed. // fn resolve_warm_airflow_universal(c: &mut Criterion) { // let run = setup(Manifest::simple(vec![ // Requirement::from(uv_pep508::Requirement::from_str("apache-airflow[all]").unwrap()), // Requirement::from( // uv_pep508::Requirement::from_str("apache-airflow-providers-apache-beam>3.0.0").unwrap(), // ), // ])); // c.bench_function("resolve_warm_airflow_universal", |b| b.iter(|| run(true))); // } criterion_group!( uv, resolve_warm_jupyter, resolve_warm_jupyter_universal, resolve_warm_airflow ); criterion_main!(uv); fn setup(manifest: Manifest) -> impl Fn(bool) { let runtime = tokio::runtime::Builder::new_current_thread() // CodSpeed limits the total number of threads to 500 .max_blocking_threads(256) .enable_all() .build() .unwrap(); let cache = Cache::from_path("../../.cache") .init_no_wait() .expect("No cache contention when running benchmarks") .unwrap(); let interpreter = PythonEnvironment::from_root("../../.venv", &cache) .unwrap() .into_interpreter(); let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache.clone()).build(); move |universal| { runtime .block_on(resolver::resolve( black_box(manifest.clone()), black_box(cache.clone()), black_box(&client), &interpreter, universal, )) .unwrap(); } } mod resolver { use std::sync::LazyLock; use anyhow::Result; use uv_cache::Cache; use uv_client::RegistryClient; use uv_configuration::{BuildOptions, Concurrency, Constraints, IndexStrategy, SourceStrategy}; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution::DistributionDatabase; use uv_distribution_types::{ ConfigSettings, DependencyMetadata, ExtraBuildRequires, ExtraBuildVariables, IndexLocations, PackageConfigSettings, RequiresPython, }; use uv_install_wheel::LinkMode; use uv_pep440::Version; use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder}; use uv_platform_tags::{Arch, Os, Platform, Tags}; use uv_preview::Preview; use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment}; use uv_python::Interpreter; use uv_resolver::{ ExcludeNewer, FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, PythonRequirement, Resolver, ResolverEnvironment, ResolverOutput, }; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_workspace::WorkspaceCache; static MARKERS: LazyLock = LazyLock::new(|| { MarkerEnvironment::try_from(MarkerEnvironmentBuilder { implementation_name: "cpython", implementation_version: "3.11.5", os_name: "posix", platform_machine: "arm64", platform_python_implementation: "CPython", platform_release: "21.6.0", platform_system: "Darwin", platform_version: "Darwin Kernel Version 21.6.0: Mon Aug 22 20:19:52 PDT 2022; root:xnu-8020.140.49~2/RELEASE_ARM64_T6000", python_full_version: "3.11.5", python_version: "3.11", sys_platform: "darwin", }).unwrap() }); static PLATFORM: Platform = Platform::new( Os::Macos { major: 21, minor: 6, }, Arch::Aarch64, ); static TAGS: LazyLock = LazyLock::new(|| { Tags::from_env(&PLATFORM, (3, 11), "cpython", (3, 11), false, false, false).unwrap() }); pub(crate) async fn resolve( manifest: Manifest, cache: Cache, client: &RegistryClient, interpreter: &Interpreter, universal: bool, ) -> Result { let build_isolation = BuildIsolation::default(); let extra_build_requires = ExtraBuildRequires::default(); let extra_build_variables = ExtraBuildVariables::default(); let build_options = BuildOptions::default(); let concurrency = Concurrency::default(); let config_settings = ConfigSettings::default(); let config_settings_package = PackageConfigSettings::default(); let exclude_newer = ExcludeNewer::global( jiff::civil::date(2024, 9, 1) .to_zoned(jiff::tz::TimeZone::UTC) .unwrap() .timestamp() .into(), ); let build_constraints = Constraints::default(); let flat_index = FlatIndex::default(); let hashes = HashStrategy::default(); let state = SharedState::default(); let index = InMemoryIndex::default(); let index_locations = IndexLocations::default(); let installed_packages = EmptyInstalledPackages; let options = OptionsBuilder::new() .exclude_newer(exclude_newer.clone()) .build(); let sources = SourceStrategy::default(); let dependency_metadata = DependencyMetadata::default(); let conflicts = Conflicts::empty(); let workspace_cache = WorkspaceCache::default(); let python_requirement = if universal { PythonRequirement::from_requires_python( interpreter, RequiresPython::greater_than_equal_version(&Version::new([3, 11])), ) } else { PythonRequirement::from_interpreter(interpreter) }; let build_context = BuildDispatch::new( client, &cache, &build_constraints, interpreter, &index_locations, &flat_index, &dependency_metadata, state, IndexStrategy::default(), &config_settings, &config_settings_package, build_isolation, &extra_build_requires, &extra_build_variables, LinkMode::default(), &build_options, &hashes, exclude_newer, sources, workspace_cache, concurrency, Preview::default(), ); let markers = if universal { ResolverEnvironment::universal(vec![]) } else { ResolverEnvironment::specific(ResolverMarkerEnvironment::from(MARKERS.clone())) }; let resolver = Resolver::new( manifest, options, &python_requirement, markers, interpreter.markers(), conflicts, Some(&TAGS), &flat_index, &index, &hashes, &build_context, installed_packages, DistributionDatabase::new(client, &build_context, concurrency.downloads), )?; Ok(resolver.resolve().await?) } } uv-0.9.17+ds1/crates/uv-bench/inputs/000077500000000000000000000000001520155276700172515ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-bench/inputs/platform_tags.rs000066400000000000000000001177121520155276700224720ustar00rootroot00000000000000&[("cp310", "abi3", "linux_x86_64"), ("cp310", "abi3", "manylinux1_x86_64"), ("cp310", "abi3", "manylinux2010_x86_64"), ("cp310", "abi3", "manylinux2014_x86_64"), ("cp310", "abi3", "manylinux_2_10_x86_64"), ("cp310", "abi3", "manylinux_2_11_x86_64"), ("cp310", "abi3", "manylinux_2_12_x86_64"), ("cp310", "abi3", "manylinux_2_13_x86_64"), ("cp310", "abi3", "manylinux_2_14_x86_64"), ("cp310", "abi3", "manylinux_2_15_x86_64"), ("cp310", "abi3", "manylinux_2_16_x86_64"), ("cp310", "abi3", "manylinux_2_17_x86_64"), ("cp310", "abi3", "manylinux_2_18_x86_64"), ("cp310", "abi3", "manylinux_2_19_x86_64"), ("cp310", "abi3", "manylinux_2_20_x86_64"), ("cp310", "abi3", "manylinux_2_21_x86_64"), ("cp310", "abi3", "manylinux_2_22_x86_64"), ("cp310", "abi3", "manylinux_2_23_x86_64"), ("cp310", "abi3", "manylinux_2_24_x86_64"), ("cp310", "abi3", "manylinux_2_25_x86_64"), ("cp310", "abi3", "manylinux_2_26_x86_64"), ("cp310", "abi3", "manylinux_2_27_x86_64"), ("cp310", "abi3", "manylinux_2_28_x86_64"), ("cp310", "abi3", "manylinux_2_29_x86_64"), ("cp310", "abi3", "manylinux_2_30_x86_64"), ("cp310", "abi3", "manylinux_2_31_x86_64"), ("cp310", "abi3", "manylinux_2_32_x86_64"), ("cp310", "abi3", "manylinux_2_33_x86_64"), ("cp310", "abi3", "manylinux_2_34_x86_64"), ("cp310", "abi3", "manylinux_2_35_x86_64"), ("cp310", "abi3", "manylinux_2_36_x86_64"), ("cp310", "abi3", "manylinux_2_37_x86_64"), ("cp310", "abi3", "manylinux_2_38_x86_64"), ("cp310", "abi3", "manylinux_2_5_x86_64"), ("cp310", "abi3", "manylinux_2_6_x86_64"), ("cp310", "abi3", "manylinux_2_7_x86_64"), ("cp310", "abi3", "manylinux_2_8_x86_64"), ("cp310", "abi3", "manylinux_2_9_x86_64"), ("cp311", "abi3", "linux_x86_64"), ("cp311", "abi3", "manylinux1_x86_64"), ("cp311", "abi3", "manylinux2010_x86_64"), ("cp311", "abi3", "manylinux2014_x86_64"), ("cp311", "abi3", "manylinux_2_10_x86_64"), ("cp311", "abi3", "manylinux_2_11_x86_64"), ("cp311", "abi3", "manylinux_2_12_x86_64"), ("cp311", "abi3", "manylinux_2_13_x86_64"), ("cp311", "abi3", "manylinux_2_14_x86_64"), ("cp311", "abi3", "manylinux_2_15_x86_64"), ("cp311", "abi3", "manylinux_2_16_x86_64"), ("cp311", "abi3", "manylinux_2_17_x86_64"), ("cp311", "abi3", "manylinux_2_18_x86_64"), ("cp311", "abi3", "manylinux_2_19_x86_64"), ("cp311", "abi3", "manylinux_2_20_x86_64"), ("cp311", "abi3", "manylinux_2_21_x86_64"), ("cp311", "abi3", "manylinux_2_22_x86_64"), ("cp311", "abi3", "manylinux_2_23_x86_64"), ("cp311", "abi3", "manylinux_2_24_x86_64"), ("cp311", "abi3", "manylinux_2_25_x86_64"), ("cp311", "abi3", "manylinux_2_26_x86_64"), ("cp311", "abi3", "manylinux_2_27_x86_64"), ("cp311", "abi3", "manylinux_2_28_x86_64"), ("cp311", "abi3", "manylinux_2_29_x86_64"), ("cp311", "abi3", "manylinux_2_30_x86_64"), ("cp311", "abi3", "manylinux_2_31_x86_64"), ("cp311", "abi3", "manylinux_2_32_x86_64"), ("cp311", "abi3", "manylinux_2_33_x86_64"), ("cp311", "abi3", "manylinux_2_34_x86_64"), ("cp311", "abi3", "manylinux_2_35_x86_64"), ("cp311", "abi3", "manylinux_2_36_x86_64"), ("cp311", "abi3", "manylinux_2_37_x86_64"), ("cp311", "abi3", "manylinux_2_38_x86_64"), ("cp311", "abi3", "manylinux_2_5_x86_64"), ("cp311", "abi3", "manylinux_2_6_x86_64"), ("cp311", "abi3", "manylinux_2_7_x86_64"), ("cp311", "abi3", "manylinux_2_8_x86_64"), ("cp311", "abi3", "manylinux_2_9_x86_64"), ("cp311", "cp311", "linux_x86_64"), ("cp311", "cp311", "manylinux1_x86_64"), ("cp311", "cp311", "manylinux2010_x86_64"), ("cp311", "cp311", "manylinux2014_x86_64"), ("cp311", "cp311", "manylinux_2_10_x86_64"), ("cp311", "cp311", "manylinux_2_11_x86_64"), ("cp311", "cp311", "manylinux_2_12_x86_64"), ("cp311", "cp311", "manylinux_2_13_x86_64"), ("cp311", "cp311", "manylinux_2_14_x86_64"), ("cp311", "cp311", "manylinux_2_15_x86_64"), ("cp311", "cp311", "manylinux_2_16_x86_64"), ("cp311", "cp311", "manylinux_2_17_x86_64"), ("cp311", "cp311", "manylinux_2_18_x86_64"), ("cp311", "cp311", "manylinux_2_19_x86_64"), ("cp311", "cp311", "manylinux_2_20_x86_64"), ("cp311", "cp311", "manylinux_2_21_x86_64"), ("cp311", "cp311", "manylinux_2_22_x86_64"), ("cp311", "cp311", "manylinux_2_23_x86_64"), ("cp311", "cp311", "manylinux_2_24_x86_64"), ("cp311", "cp311", "manylinux_2_25_x86_64"), ("cp311", "cp311", "manylinux_2_26_x86_64"), ("cp311", "cp311", "manylinux_2_27_x86_64"), ("cp311", "cp311", "manylinux_2_28_x86_64"), ("cp311", "cp311", "manylinux_2_29_x86_64"), ("cp311", "cp311", "manylinux_2_30_x86_64"), ("cp311", "cp311", "manylinux_2_31_x86_64"), ("cp311", "cp311", "manylinux_2_32_x86_64"), ("cp311", "cp311", "manylinux_2_33_x86_64"), ("cp311", "cp311", "manylinux_2_34_x86_64"), ("cp311", "cp311", "manylinux_2_35_x86_64"), ("cp311", "cp311", "manylinux_2_36_x86_64"), ("cp311", "cp311", "manylinux_2_37_x86_64"), ("cp311", "cp311", "manylinux_2_38_x86_64"), ("cp311", "cp311", "manylinux_2_5_x86_64"), ("cp311", "cp311", "manylinux_2_6_x86_64"), ("cp311", "cp311", "manylinux_2_7_x86_64"), ("cp311", "cp311", "manylinux_2_8_x86_64"), ("cp311", "cp311", "manylinux_2_9_x86_64"), ("cp311", "none", "linux_x86_64"), ("cp311", "none", "manylinux1_x86_64"), ("cp311", "none", "manylinux2010_x86_64"), ("cp311", "none", "manylinux2014_x86_64"), ("cp311", "none", "manylinux_2_10_x86_64"), ("cp311", "none", "manylinux_2_11_x86_64"), ("cp311", "none", "manylinux_2_12_x86_64"), ("cp311", "none", "manylinux_2_13_x86_64"), ("cp311", "none", "manylinux_2_14_x86_64"), ("cp311", "none", "manylinux_2_15_x86_64"), ("cp311", "none", "manylinux_2_16_x86_64"), ("cp311", "none", "manylinux_2_17_x86_64"), ("cp311", "none", "manylinux_2_18_x86_64"), ("cp311", "none", "manylinux_2_19_x86_64"), ("cp311", "none", "manylinux_2_20_x86_64"), ("cp311", "none", "manylinux_2_21_x86_64"), ("cp311", "none", "manylinux_2_22_x86_64"), ("cp311", "none", "manylinux_2_23_x86_64"), ("cp311", "none", "manylinux_2_24_x86_64"), ("cp311", "none", "manylinux_2_25_x86_64"), ("cp311", "none", "manylinux_2_26_x86_64"), ("cp311", "none", "manylinux_2_27_x86_64"), ("cp311", "none", "manylinux_2_28_x86_64"), ("cp311", "none", "manylinux_2_29_x86_64"), ("cp311", "none", "manylinux_2_30_x86_64"), ("cp311", "none", "manylinux_2_31_x86_64"), ("cp311", "none", "manylinux_2_32_x86_64"), ("cp311", "none", "manylinux_2_33_x86_64"), ("cp311", "none", "manylinux_2_34_x86_64"), ("cp311", "none", "manylinux_2_35_x86_64"), ("cp311", "none", "manylinux_2_36_x86_64"), ("cp311", "none", "manylinux_2_37_x86_64"), ("cp311", "none", "manylinux_2_38_x86_64"), ("cp311", "none", "manylinux_2_5_x86_64"), ("cp311", "none", "manylinux_2_6_x86_64"), ("cp311", "none", "manylinux_2_7_x86_64"), ("cp311", "none", "manylinux_2_8_x86_64"), ("cp311", "none", "manylinux_2_9_x86_64"), ("cp32", "abi3", "linux_x86_64"), ("cp32", "abi3", "manylinux1_x86_64"), ("cp32", "abi3", "manylinux2010_x86_64"), ("cp32", "abi3", "manylinux2014_x86_64"), ("cp32", "abi3", "manylinux_2_10_x86_64"), ("cp32", "abi3", "manylinux_2_11_x86_64"), ("cp32", "abi3", "manylinux_2_12_x86_64"), ("cp32", "abi3", "manylinux_2_13_x86_64"), ("cp32", "abi3", "manylinux_2_14_x86_64"), ("cp32", "abi3", "manylinux_2_15_x86_64"), ("cp32", "abi3", "manylinux_2_16_x86_64"), ("cp32", "abi3", "manylinux_2_17_x86_64"), ("cp32", "abi3", "manylinux_2_18_x86_64"), ("cp32", "abi3", "manylinux_2_19_x86_64"), ("cp32", "abi3", "manylinux_2_20_x86_64"), ("cp32", "abi3", "manylinux_2_21_x86_64"), ("cp32", "abi3", "manylinux_2_22_x86_64"), ("cp32", "abi3", "manylinux_2_23_x86_64"), ("cp32", "abi3", "manylinux_2_24_x86_64"), ("cp32", "abi3", "manylinux_2_25_x86_64"), ("cp32", "abi3", "manylinux_2_26_x86_64"), ("cp32", "abi3", "manylinux_2_27_x86_64"), ("cp32", "abi3", "manylinux_2_28_x86_64"), ("cp32", "abi3", "manylinux_2_29_x86_64"), ("cp32", "abi3", "manylinux_2_30_x86_64"), ("cp32", "abi3", "manylinux_2_31_x86_64"), ("cp32", "abi3", "manylinux_2_32_x86_64"), ("cp32", "abi3", "manylinux_2_33_x86_64"), ("cp32", "abi3", "manylinux_2_34_x86_64"), ("cp32", "abi3", "manylinux_2_35_x86_64"), ("cp32", "abi3", "manylinux_2_36_x86_64"), ("cp32", "abi3", "manylinux_2_37_x86_64"), ("cp32", "abi3", "manylinux_2_38_x86_64"), ("cp32", "abi3", "manylinux_2_5_x86_64"), ("cp32", "abi3", "manylinux_2_6_x86_64"), ("cp32", "abi3", "manylinux_2_7_x86_64"), ("cp32", "abi3", "manylinux_2_8_x86_64"), ("cp32", "abi3", "manylinux_2_9_x86_64"), ("cp33", "abi3", "linux_x86_64"), ("cp33", "abi3", "manylinux1_x86_64"), ("cp33", "abi3", "manylinux2010_x86_64"), ("cp33", "abi3", "manylinux2014_x86_64"), ("cp33", "abi3", "manylinux_2_10_x86_64"), ("cp33", "abi3", "manylinux_2_11_x86_64"), ("cp33", "abi3", "manylinux_2_12_x86_64"), ("cp33", "abi3", "manylinux_2_13_x86_64"), ("cp33", "abi3", "manylinux_2_14_x86_64"), ("cp33", "abi3", "manylinux_2_15_x86_64"), ("cp33", "abi3", "manylinux_2_16_x86_64"), ("cp33", "abi3", "manylinux_2_17_x86_64"), ("cp33", "abi3", "manylinux_2_18_x86_64"), ("cp33", "abi3", "manylinux_2_19_x86_64"), ("cp33", "abi3", "manylinux_2_20_x86_64"), ("cp33", "abi3", "manylinux_2_21_x86_64"), ("cp33", "abi3", "manylinux_2_22_x86_64"), ("cp33", "abi3", "manylinux_2_23_x86_64"), ("cp33", "abi3", "manylinux_2_24_x86_64"), ("cp33", "abi3", "manylinux_2_25_x86_64"), ("cp33", "abi3", "manylinux_2_26_x86_64"), ("cp33", "abi3", "manylinux_2_27_x86_64"), ("cp33", "abi3", "manylinux_2_28_x86_64"), ("cp33", "abi3", "manylinux_2_29_x86_64"), ("cp33", "abi3", "manylinux_2_30_x86_64"), ("cp33", "abi3", "manylinux_2_31_x86_64"), ("cp33", "abi3", "manylinux_2_32_x86_64"), ("cp33", "abi3", "manylinux_2_33_x86_64"), ("cp33", "abi3", "manylinux_2_34_x86_64"), ("cp33", "abi3", "manylinux_2_35_x86_64"), ("cp33", "abi3", "manylinux_2_36_x86_64"), ("cp33", "abi3", "manylinux_2_37_x86_64"), ("cp33", "abi3", "manylinux_2_38_x86_64"), ("cp33", "abi3", "manylinux_2_5_x86_64"), ("cp33", "abi3", "manylinux_2_6_x86_64"), ("cp33", "abi3", "manylinux_2_7_x86_64"), ("cp33", "abi3", "manylinux_2_8_x86_64"), ("cp33", "abi3", "manylinux_2_9_x86_64"), ("cp34", "abi3", "linux_x86_64"), ("cp34", "abi3", "manylinux1_x86_64"), ("cp34", "abi3", "manylinux2010_x86_64"), ("cp34", "abi3", "manylinux2014_x86_64"), ("cp34", "abi3", "manylinux_2_10_x86_64"), ("cp34", "abi3", "manylinux_2_11_x86_64"), ("cp34", "abi3", "manylinux_2_12_x86_64"), ("cp34", "abi3", "manylinux_2_13_x86_64"), ("cp34", "abi3", "manylinux_2_14_x86_64"), ("cp34", "abi3", "manylinux_2_15_x86_64"), ("cp34", "abi3", "manylinux_2_16_x86_64"), ("cp34", "abi3", "manylinux_2_17_x86_64"), ("cp34", "abi3", "manylinux_2_18_x86_64"), ("cp34", "abi3", "manylinux_2_19_x86_64"), ("cp34", "abi3", "manylinux_2_20_x86_64"), ("cp34", "abi3", "manylinux_2_21_x86_64"), ("cp34", "abi3", "manylinux_2_22_x86_64"), ("cp34", "abi3", "manylinux_2_23_x86_64"), ("cp34", "abi3", "manylinux_2_24_x86_64"), ("cp34", "abi3", "manylinux_2_25_x86_64"), ("cp34", "abi3", "manylinux_2_26_x86_64"), ("cp34", "abi3", "manylinux_2_27_x86_64"), ("cp34", "abi3", "manylinux_2_28_x86_64"), ("cp34", "abi3", "manylinux_2_29_x86_64"), ("cp34", "abi3", "manylinux_2_30_x86_64"), ("cp34", "abi3", "manylinux_2_31_x86_64"), ("cp34", "abi3", "manylinux_2_32_x86_64"), ("cp34", "abi3", "manylinux_2_33_x86_64"), ("cp34", "abi3", "manylinux_2_34_x86_64"), ("cp34", "abi3", "manylinux_2_35_x86_64"), ("cp34", "abi3", "manylinux_2_36_x86_64"), ("cp34", "abi3", "manylinux_2_37_x86_64"), ("cp34", "abi3", "manylinux_2_38_x86_64"), ("cp34", "abi3", "manylinux_2_5_x86_64"), ("cp34", "abi3", "manylinux_2_6_x86_64"), ("cp34", "abi3", "manylinux_2_7_x86_64"), ("cp34", "abi3", "manylinux_2_8_x86_64"), ("cp34", "abi3", "manylinux_2_9_x86_64"), ("cp35", "abi3", "linux_x86_64"), ("cp35", "abi3", "manylinux1_x86_64"), ("cp35", "abi3", "manylinux2010_x86_64"), ("cp35", "abi3", "manylinux2014_x86_64"), ("cp35", "abi3", "manylinux_2_10_x86_64"), ("cp35", "abi3", "manylinux_2_11_x86_64"), ("cp35", "abi3", "manylinux_2_12_x86_64"), ("cp35", "abi3", "manylinux_2_13_x86_64"), ("cp35", "abi3", "manylinux_2_14_x86_64"), ("cp35", "abi3", "manylinux_2_15_x86_64"), ("cp35", "abi3", "manylinux_2_16_x86_64"), ("cp35", "abi3", "manylinux_2_17_x86_64"), ("cp35", "abi3", "manylinux_2_18_x86_64"), ("cp35", "abi3", "manylinux_2_19_x86_64"), ("cp35", "abi3", "manylinux_2_20_x86_64"), ("cp35", "abi3", "manylinux_2_21_x86_64"), ("cp35", "abi3", "manylinux_2_22_x86_64"), ("cp35", "abi3", "manylinux_2_23_x86_64"), ("cp35", "abi3", "manylinux_2_24_x86_64"), ("cp35", "abi3", "manylinux_2_25_x86_64"), ("cp35", "abi3", "manylinux_2_26_x86_64"), ("cp35", "abi3", "manylinux_2_27_x86_64"), ("cp35", "abi3", "manylinux_2_28_x86_64"), ("cp35", "abi3", "manylinux_2_29_x86_64"), ("cp35", "abi3", "manylinux_2_30_x86_64"), ("cp35", "abi3", "manylinux_2_31_x86_64"), ("cp35", "abi3", "manylinux_2_32_x86_64"), ("cp35", "abi3", "manylinux_2_33_x86_64"), ("cp35", "abi3", "manylinux_2_34_x86_64"), ("cp35", "abi3", "manylinux_2_35_x86_64"), ("cp35", "abi3", "manylinux_2_36_x86_64"), ("cp35", "abi3", "manylinux_2_37_x86_64"), ("cp35", "abi3", "manylinux_2_38_x86_64"), ("cp35", "abi3", "manylinux_2_5_x86_64"), ("cp35", "abi3", "manylinux_2_6_x86_64"), ("cp35", "abi3", "manylinux_2_7_x86_64"), ("cp35", "abi3", "manylinux_2_8_x86_64"), ("cp35", "abi3", "manylinux_2_9_x86_64"), ("cp36", "abi3", "linux_x86_64"), ("cp36", "abi3", "manylinux1_x86_64"), ("cp36", "abi3", "manylinux2010_x86_64"), ("cp36", "abi3", "manylinux2014_x86_64"), ("cp36", "abi3", "manylinux_2_10_x86_64"), ("cp36", "abi3", "manylinux_2_11_x86_64"), ("cp36", "abi3", "manylinux_2_12_x86_64"), ("cp36", "abi3", "manylinux_2_13_x86_64"), ("cp36", "abi3", "manylinux_2_14_x86_64"), ("cp36", "abi3", "manylinux_2_15_x86_64"), ("cp36", "abi3", "manylinux_2_16_x86_64"), ("cp36", "abi3", "manylinux_2_17_x86_64"), ("cp36", "abi3", "manylinux_2_18_x86_64"), ("cp36", "abi3", "manylinux_2_19_x86_64"), ("cp36", "abi3", "manylinux_2_20_x86_64"), ("cp36", "abi3", "manylinux_2_21_x86_64"), ("cp36", "abi3", "manylinux_2_22_x86_64"), ("cp36", "abi3", "manylinux_2_23_x86_64"), ("cp36", "abi3", "manylinux_2_24_x86_64"), ("cp36", "abi3", "manylinux_2_25_x86_64"), ("cp36", "abi3", "manylinux_2_26_x86_64"), ("cp36", "abi3", "manylinux_2_27_x86_64"), ("cp36", "abi3", "manylinux_2_28_x86_64"), ("cp36", "abi3", "manylinux_2_29_x86_64"), ("cp36", "abi3", "manylinux_2_30_x86_64"), ("cp36", "abi3", "manylinux_2_31_x86_64"), ("cp36", "abi3", "manylinux_2_32_x86_64"), ("cp36", "abi3", "manylinux_2_33_x86_64"), ("cp36", "abi3", "manylinux_2_34_x86_64"), ("cp36", "abi3", "manylinux_2_35_x86_64"), ("cp36", "abi3", "manylinux_2_36_x86_64"), ("cp36", "abi3", "manylinux_2_37_x86_64"), ("cp36", "abi3", "manylinux_2_38_x86_64"), ("cp36", "abi3", "manylinux_2_5_x86_64"), ("cp36", "abi3", "manylinux_2_6_x86_64"), ("cp36", "abi3", "manylinux_2_7_x86_64"), ("cp36", "abi3", "manylinux_2_8_x86_64"), ("cp36", "abi3", "manylinux_2_9_x86_64"), ("cp37", "abi3", "linux_x86_64"), ("cp37", "abi3", "manylinux1_x86_64"), ("cp37", "abi3", "manylinux2010_x86_64"), ("cp37", "abi3", "manylinux2014_x86_64"), ("cp37", "abi3", "manylinux_2_10_x86_64"), ("cp37", "abi3", "manylinux_2_11_x86_64"), ("cp37", "abi3", "manylinux_2_12_x86_64"), ("cp37", "abi3", "manylinux_2_13_x86_64"), ("cp37", "abi3", "manylinux_2_14_x86_64"), ("cp37", "abi3", "manylinux_2_15_x86_64"), ("cp37", "abi3", "manylinux_2_16_x86_64"), ("cp37", "abi3", "manylinux_2_17_x86_64"), ("cp37", "abi3", "manylinux_2_18_x86_64"), ("cp37", "abi3", "manylinux_2_19_x86_64"), ("cp37", "abi3", "manylinux_2_20_x86_64"), ("cp37", "abi3", "manylinux_2_21_x86_64"), ("cp37", "abi3", "manylinux_2_22_x86_64"), ("cp37", "abi3", "manylinux_2_23_x86_64"), ("cp37", "abi3", "manylinux_2_24_x86_64"), ("cp37", "abi3", "manylinux_2_25_x86_64"), ("cp37", "abi3", "manylinux_2_26_x86_64"), ("cp37", "abi3", "manylinux_2_27_x86_64"), ("cp37", "abi3", "manylinux_2_28_x86_64"), ("cp37", "abi3", "manylinux_2_29_x86_64"), ("cp37", "abi3", "manylinux_2_30_x86_64"), ("cp37", "abi3", "manylinux_2_31_x86_64"), ("cp37", "abi3", "manylinux_2_32_x86_64"), ("cp37", "abi3", "manylinux_2_33_x86_64"), ("cp37", "abi3", "manylinux_2_34_x86_64"), ("cp37", "abi3", "manylinux_2_35_x86_64"), ("cp37", "abi3", "manylinux_2_36_x86_64"), ("cp37", "abi3", "manylinux_2_37_x86_64"), ("cp37", "abi3", "manylinux_2_38_x86_64"), ("cp37", "abi3", "manylinux_2_5_x86_64"), ("cp37", "abi3", "manylinux_2_6_x86_64"), ("cp37", "abi3", "manylinux_2_7_x86_64"), ("cp37", "abi3", "manylinux_2_8_x86_64"), ("cp37", "abi3", "manylinux_2_9_x86_64"), ("cp38", "abi3", "linux_x86_64"), ("cp38", "abi3", "manylinux1_x86_64"), ("cp38", "abi3", "manylinux2010_x86_64"), ("cp38", "abi3", "manylinux2014_x86_64"), ("cp38", "abi3", "manylinux_2_10_x86_64"), ("cp38", "abi3", "manylinux_2_11_x86_64"), ("cp38", "abi3", "manylinux_2_12_x86_64"), ("cp38", "abi3", "manylinux_2_13_x86_64"), ("cp38", "abi3", "manylinux_2_14_x86_64"), ("cp38", "abi3", "manylinux_2_15_x86_64"), ("cp38", "abi3", "manylinux_2_16_x86_64"), ("cp38", "abi3", "manylinux_2_17_x86_64"), ("cp38", "abi3", "manylinux_2_18_x86_64"), ("cp38", "abi3", "manylinux_2_19_x86_64"), ("cp38", "abi3", "manylinux_2_20_x86_64"), ("cp38", "abi3", "manylinux_2_21_x86_64"), ("cp38", "abi3", "manylinux_2_22_x86_64"), ("cp38", "abi3", "manylinux_2_23_x86_64"), ("cp38", "abi3", "manylinux_2_24_x86_64"), ("cp38", "abi3", "manylinux_2_25_x86_64"), ("cp38", "abi3", "manylinux_2_26_x86_64"), ("cp38", "abi3", "manylinux_2_27_x86_64"), ("cp38", "abi3", "manylinux_2_28_x86_64"), ("cp38", "abi3", "manylinux_2_29_x86_64"), ("cp38", "abi3", "manylinux_2_30_x86_64"), ("cp38", "abi3", "manylinux_2_31_x86_64"), ("cp38", "abi3", "manylinux_2_32_x86_64"), ("cp38", "abi3", "manylinux_2_33_x86_64"), ("cp38", "abi3", "manylinux_2_34_x86_64"), ("cp38", "abi3", "manylinux_2_35_x86_64"), ("cp38", "abi3", "manylinux_2_36_x86_64"), ("cp38", "abi3", "manylinux_2_37_x86_64"), ("cp38", "abi3", "manylinux_2_38_x86_64"), ("cp38", "abi3", "manylinux_2_5_x86_64"), ("cp38", "abi3", "manylinux_2_6_x86_64"), ("cp38", "abi3", "manylinux_2_7_x86_64"), ("cp38", "abi3", "manylinux_2_8_x86_64"), ("cp38", "abi3", "manylinux_2_9_x86_64"), ("cp39", "abi3", "linux_x86_64"), ("cp39", "abi3", "manylinux1_x86_64"), ("cp39", "abi3", "manylinux2010_x86_64"), ("cp39", "abi3", "manylinux2014_x86_64"), ("cp39", "abi3", "manylinux_2_10_x86_64"), ("cp39", "abi3", "manylinux_2_11_x86_64"), ("cp39", "abi3", "manylinux_2_12_x86_64"), ("cp39", "abi3", "manylinux_2_13_x86_64"), ("cp39", "abi3", "manylinux_2_14_x86_64"), ("cp39", "abi3", "manylinux_2_15_x86_64"), ("cp39", "abi3", "manylinux_2_16_x86_64"), ("cp39", "abi3", "manylinux_2_17_x86_64"), ("cp39", "abi3", "manylinux_2_18_x86_64"), ("cp39", "abi3", "manylinux_2_19_x86_64"), ("cp39", "abi3", "manylinux_2_20_x86_64"), ("cp39", "abi3", "manylinux_2_21_x86_64"), ("cp39", "abi3", "manylinux_2_22_x86_64"), ("cp39", "abi3", "manylinux_2_23_x86_64"), ("cp39", "abi3", "manylinux_2_24_x86_64"), ("cp39", "abi3", "manylinux_2_25_x86_64"), ("cp39", "abi3", "manylinux_2_26_x86_64"), ("cp39", "abi3", "manylinux_2_27_x86_64"), ("cp39", "abi3", "manylinux_2_28_x86_64"), ("cp39", "abi3", "manylinux_2_29_x86_64"), ("cp39", "abi3", "manylinux_2_30_x86_64"), ("cp39", "abi3", "manylinux_2_31_x86_64"), ("cp39", "abi3", "manylinux_2_32_x86_64"), ("cp39", "abi3", "manylinux_2_33_x86_64"), ("cp39", "abi3", "manylinux_2_34_x86_64"), ("cp39", "abi3", "manylinux_2_35_x86_64"), ("cp39", "abi3", "manylinux_2_36_x86_64"), ("cp39", "abi3", "manylinux_2_37_x86_64"), ("cp39", "abi3", "manylinux_2_38_x86_64"), ("cp39", "abi3", "manylinux_2_5_x86_64"), ("cp39", "abi3", "manylinux_2_6_x86_64"), ("cp39", "abi3", "manylinux_2_7_x86_64"), ("cp39", "abi3", "manylinux_2_8_x86_64"), ("cp39", "abi3", "manylinux_2_9_x86_64"), ("py3", "none", "any"), ("py3", "none", "linux_x86_64"), ("py3", "none", "manylinux1_x86_64"), ("py3", "none", "manylinux2010_x86_64"), ("py3", "none", "manylinux2014_x86_64"), ("py3", "none", "manylinux_2_10_x86_64"), ("py3", "none", "manylinux_2_11_x86_64"), ("py3", "none", "manylinux_2_12_x86_64"), ("py3", "none", "manylinux_2_13_x86_64"), ("py3", "none", "manylinux_2_14_x86_64"), ("py3", "none", "manylinux_2_15_x86_64"), ("py3", "none", "manylinux_2_16_x86_64"), ("py3", "none", "manylinux_2_17_x86_64"), ("py3", "none", "manylinux_2_18_x86_64"), ("py3", "none", "manylinux_2_19_x86_64"), ("py3", "none", "manylinux_2_20_x86_64"), ("py3", "none", "manylinux_2_21_x86_64"), ("py3", "none", "manylinux_2_22_x86_64"), ("py3", "none", "manylinux_2_23_x86_64"), ("py3", "none", "manylinux_2_24_x86_64"), ("py3", "none", "manylinux_2_25_x86_64"), ("py3", "none", "manylinux_2_26_x86_64"), ("py3", "none", "manylinux_2_27_x86_64"), ("py3", "none", "manylinux_2_28_x86_64"), ("py3", "none", "manylinux_2_29_x86_64"), ("py3", "none", "manylinux_2_30_x86_64"), ("py3", "none", "manylinux_2_31_x86_64"), ("py3", "none", "manylinux_2_32_x86_64"), ("py3", "none", "manylinux_2_33_x86_64"), ("py3", "none", "manylinux_2_34_x86_64"), ("py3", "none", "manylinux_2_35_x86_64"), ("py3", "none", "manylinux_2_36_x86_64"), ("py3", "none", "manylinux_2_37_x86_64"), ("py3", "none", "manylinux_2_38_x86_64"), ("py3", "none", "manylinux_2_5_x86_64"), ("py3", "none", "manylinux_2_6_x86_64"), ("py3", "none", "manylinux_2_7_x86_64"), ("py3", "none", "manylinux_2_8_x86_64"), ("py3", "none", "manylinux_2_9_x86_64"), ("py30", "none", "any"), ("py30", "none", "linux_x86_64"), ("py30", "none", "manylinux1_x86_64"), ("py30", "none", "manylinux2010_x86_64"), ("py30", "none", "manylinux2014_x86_64"), ("py30", "none", "manylinux_2_10_x86_64"), ("py30", "none", "manylinux_2_11_x86_64"), ("py30", "none", "manylinux_2_12_x86_64"), ("py30", "none", "manylinux_2_13_x86_64"), ("py30", "none", "manylinux_2_14_x86_64"), ("py30", "none", "manylinux_2_15_x86_64"), ("py30", "none", "manylinux_2_16_x86_64"), ("py30", "none", "manylinux_2_17_x86_64"), ("py30", "none", "manylinux_2_18_x86_64"), ("py30", "none", "manylinux_2_19_x86_64"), ("py30", "none", "manylinux_2_20_x86_64"), ("py30", "none", "manylinux_2_21_x86_64"), ("py30", "none", "manylinux_2_22_x86_64"), ("py30", "none", "manylinux_2_23_x86_64"), ("py30", "none", "manylinux_2_24_x86_64"), ("py30", "none", "manylinux_2_25_x86_64"), ("py30", "none", "manylinux_2_26_x86_64"), ("py30", "none", "manylinux_2_27_x86_64"), ("py30", "none", "manylinux_2_28_x86_64"), ("py30", "none", "manylinux_2_29_x86_64"), ("py30", "none", "manylinux_2_30_x86_64"), ("py30", "none", "manylinux_2_31_x86_64"), ("py30", "none", "manylinux_2_32_x86_64"), ("py30", "none", "manylinux_2_33_x86_64"), ("py30", "none", "manylinux_2_34_x86_64"), ("py30", "none", "manylinux_2_35_x86_64"), ("py30", "none", "manylinux_2_36_x86_64"), ("py30", "none", "manylinux_2_37_x86_64"), ("py30", "none", "manylinux_2_38_x86_64"), ("py30", "none", "manylinux_2_5_x86_64"), ("py30", "none", "manylinux_2_6_x86_64"), ("py30", "none", "manylinux_2_7_x86_64"), ("py30", "none", "manylinux_2_8_x86_64"), ("py30", "none", "manylinux_2_9_x86_64"), ("py31", "none", "any"), ("py31", "none", "linux_x86_64"), ("py31", "none", "manylinux1_x86_64"), ("py31", "none", "manylinux2010_x86_64"), ("py31", "none", "manylinux2014_x86_64"), ("py31", "none", "manylinux_2_10_x86_64"), ("py31", "none", "manylinux_2_11_x86_64"), ("py31", "none", "manylinux_2_12_x86_64"), ("py31", "none", "manylinux_2_13_x86_64"), ("py31", "none", "manylinux_2_14_x86_64"), ("py31", "none", "manylinux_2_15_x86_64"), ("py31", "none", "manylinux_2_16_x86_64"), ("py31", "none", "manylinux_2_17_x86_64"), ("py31", "none", "manylinux_2_18_x86_64"), ("py31", "none", "manylinux_2_19_x86_64"), ("py31", "none", "manylinux_2_20_x86_64"), ("py31", "none", "manylinux_2_21_x86_64"), ("py31", "none", "manylinux_2_22_x86_64"), ("py31", "none", "manylinux_2_23_x86_64"), ("py31", "none", "manylinux_2_24_x86_64"), ("py31", "none", "manylinux_2_25_x86_64"), ("py31", "none", "manylinux_2_26_x86_64"), ("py31", "none", "manylinux_2_27_x86_64"), ("py31", "none", "manylinux_2_28_x86_64"), ("py31", "none", "manylinux_2_29_x86_64"), ("py31", "none", "manylinux_2_30_x86_64"), ("py31", "none", "manylinux_2_31_x86_64"), ("py31", "none", "manylinux_2_32_x86_64"), ("py31", "none", "manylinux_2_33_x86_64"), ("py31", "none", "manylinux_2_34_x86_64"), ("py31", "none", "manylinux_2_35_x86_64"), ("py31", "none", "manylinux_2_36_x86_64"), ("py31", "none", "manylinux_2_37_x86_64"), ("py31", "none", "manylinux_2_38_x86_64"), ("py31", "none", "manylinux_2_5_x86_64"), ("py31", "none", "manylinux_2_6_x86_64"), ("py31", "none", "manylinux_2_7_x86_64"), ("py31", "none", "manylinux_2_8_x86_64"), ("py31", "none", "manylinux_2_9_x86_64"), ("py310", "none", "any"), ("py310", "none", "linux_x86_64"), ("py310", "none", "manylinux1_x86_64"), ("py310", "none", "manylinux2010_x86_64"), ("py310", "none", "manylinux2014_x86_64"), ("py310", "none", "manylinux_2_10_x86_64"), ("py310", "none", "manylinux_2_11_x86_64"), ("py310", "none", "manylinux_2_12_x86_64"), ("py310", "none", "manylinux_2_13_x86_64"), ("py310", "none", "manylinux_2_14_x86_64"), ("py310", "none", "manylinux_2_15_x86_64"), ("py310", "none", "manylinux_2_16_x86_64"), ("py310", "none", "manylinux_2_17_x86_64"), ("py310", "none", "manylinux_2_18_x86_64"), ("py310", "none", "manylinux_2_19_x86_64"), ("py310", "none", "manylinux_2_20_x86_64"), ("py310", "none", "manylinux_2_21_x86_64"), ("py310", "none", "manylinux_2_22_x86_64"), ("py310", "none", "manylinux_2_23_x86_64"), ("py310", "none", "manylinux_2_24_x86_64"), ("py310", "none", "manylinux_2_25_x86_64"), ("py310", "none", "manylinux_2_26_x86_64"), ("py310", "none", "manylinux_2_27_x86_64"), ("py310", "none", "manylinux_2_28_x86_64"), ("py310", "none", "manylinux_2_29_x86_64"), ("py310", "none", "manylinux_2_30_x86_64"), ("py310", "none", "manylinux_2_31_x86_64"), ("py310", "none", "manylinux_2_32_x86_64"), ("py310", "none", "manylinux_2_33_x86_64"), ("py310", "none", "manylinux_2_34_x86_64"), ("py310", "none", "manylinux_2_35_x86_64"), ("py310", "none", "manylinux_2_36_x86_64"), ("py310", "none", "manylinux_2_37_x86_64"), ("py310", "none", "manylinux_2_38_x86_64"), ("py310", "none", "manylinux_2_5_x86_64"), ("py310", "none", "manylinux_2_6_x86_64"), ("py310", "none", "manylinux_2_7_x86_64"), ("py310", "none", "manylinux_2_8_x86_64"), ("py310", "none", "manylinux_2_9_x86_64"), ("py311", "none", "any"), ("py311", "none", "linux_x86_64"), ("py311", "none", "manylinux1_x86_64"), ("py311", "none", "manylinux2010_x86_64"), ("py311", "none", "manylinux2014_x86_64"), ("py311", "none", "manylinux_2_10_x86_64"), ("py311", "none", "manylinux_2_11_x86_64"), ("py311", "none", "manylinux_2_12_x86_64"), ("py311", "none", "manylinux_2_13_x86_64"), ("py311", "none", "manylinux_2_14_x86_64"), ("py311", "none", "manylinux_2_15_x86_64"), ("py311", "none", "manylinux_2_16_x86_64"), ("py311", "none", "manylinux_2_17_x86_64"), ("py311", "none", "manylinux_2_18_x86_64"), ("py311", "none", "manylinux_2_19_x86_64"), ("py311", "none", "manylinux_2_20_x86_64"), ("py311", "none", "manylinux_2_21_x86_64"), ("py311", "none", "manylinux_2_22_x86_64"), ("py311", "none", "manylinux_2_23_x86_64"), ("py311", "none", "manylinux_2_24_x86_64"), ("py311", "none", "manylinux_2_25_x86_64"), ("py311", "none", "manylinux_2_26_x86_64"), ("py311", "none", "manylinux_2_27_x86_64"), ("py311", "none", "manylinux_2_28_x86_64"), ("py311", "none", "manylinux_2_29_x86_64"), ("py311", "none", "manylinux_2_30_x86_64"), ("py311", "none", "manylinux_2_31_x86_64"), ("py311", "none", "manylinux_2_32_x86_64"), ("py311", "none", "manylinux_2_33_x86_64"), ("py311", "none", "manylinux_2_34_x86_64"), ("py311", "none", "manylinux_2_35_x86_64"), ("py311", "none", "manylinux_2_36_x86_64"), ("py311", "none", "manylinux_2_37_x86_64"), ("py311", "none", "manylinux_2_38_x86_64"), ("py311", "none", "manylinux_2_5_x86_64"), ("py311", "none", "manylinux_2_6_x86_64"), ("py311", "none", "manylinux_2_7_x86_64"), ("py311", "none", "manylinux_2_8_x86_64"), ("py311", "none", "manylinux_2_9_x86_64"), ("py32", "none", "any"), ("py32", "none", "linux_x86_64"), ("py32", "none", "manylinux1_x86_64"), ("py32", "none", "manylinux2010_x86_64"), ("py32", "none", "manylinux2014_x86_64"), ("py32", "none", "manylinux_2_10_x86_64"), ("py32", "none", "manylinux_2_11_x86_64"), ("py32", "none", "manylinux_2_12_x86_64"), ("py32", "none", "manylinux_2_13_x86_64"), ("py32", "none", "manylinux_2_14_x86_64"), ("py32", "none", "manylinux_2_15_x86_64"), ("py32", "none", "manylinux_2_16_x86_64"), ("py32", "none", "manylinux_2_17_x86_64"), ("py32", "none", "manylinux_2_18_x86_64"), ("py32", "none", "manylinux_2_19_x86_64"), ("py32", "none", "manylinux_2_20_x86_64"), ("py32", "none", "manylinux_2_21_x86_64"), ("py32", "none", "manylinux_2_22_x86_64"), ("py32", "none", "manylinux_2_23_x86_64"), ("py32", "none", "manylinux_2_24_x86_64"), ("py32", "none", "manylinux_2_25_x86_64"), ("py32", "none", "manylinux_2_26_x86_64"), ("py32", "none", "manylinux_2_27_x86_64"), ("py32", "none", "manylinux_2_28_x86_64"), ("py32", "none", "manylinux_2_29_x86_64"), ("py32", "none", "manylinux_2_30_x86_64"), ("py32", "none", "manylinux_2_31_x86_64"), ("py32", "none", "manylinux_2_32_x86_64"), ("py32", "none", "manylinux_2_33_x86_64"), ("py32", "none", "manylinux_2_34_x86_64"), ("py32", "none", "manylinux_2_35_x86_64"), ("py32", "none", "manylinux_2_36_x86_64"), ("py32", "none", "manylinux_2_37_x86_64"), ("py32", "none", "manylinux_2_38_x86_64"), ("py32", "none", "manylinux_2_5_x86_64"), ("py32", "none", "manylinux_2_6_x86_64"), ("py32", "none", "manylinux_2_7_x86_64"), ("py32", "none", "manylinux_2_8_x86_64"), ("py32", "none", "manylinux_2_9_x86_64"), ("py33", "none", "any"), ("py33", "none", "linux_x86_64"), ("py33", "none", "manylinux1_x86_64"), ("py33", "none", "manylinux2010_x86_64"), ("py33", "none", "manylinux2014_x86_64"), ("py33", "none", "manylinux_2_10_x86_64"), ("py33", "none", "manylinux_2_11_x86_64"), ("py33", "none", "manylinux_2_12_x86_64"), ("py33", "none", "manylinux_2_13_x86_64"), ("py33", "none", "manylinux_2_14_x86_64"), ("py33", "none", "manylinux_2_15_x86_64"), ("py33", "none", "manylinux_2_16_x86_64"), ("py33", "none", "manylinux_2_17_x86_64"), ("py33", "none", "manylinux_2_18_x86_64"), ("py33", "none", "manylinux_2_19_x86_64"), ("py33", "none", "manylinux_2_20_x86_64"), ("py33", "none", "manylinux_2_21_x86_64"), ("py33", "none", "manylinux_2_22_x86_64"), ("py33", "none", "manylinux_2_23_x86_64"), ("py33", "none", "manylinux_2_24_x86_64"), ("py33", "none", "manylinux_2_25_x86_64"), ("py33", "none", "manylinux_2_26_x86_64"), ("py33", "none", "manylinux_2_27_x86_64"), ("py33", "none", "manylinux_2_28_x86_64"), ("py33", "none", "manylinux_2_29_x86_64"), ("py33", "none", "manylinux_2_30_x86_64"), ("py33", "none", "manylinux_2_31_x86_64"), ("py33", "none", "manylinux_2_32_x86_64"), ("py33", "none", "manylinux_2_33_x86_64"), ("py33", "none", "manylinux_2_34_x86_64"), ("py33", "none", "manylinux_2_35_x86_64"), ("py33", "none", "manylinux_2_36_x86_64"), ("py33", "none", "manylinux_2_37_x86_64"), ("py33", "none", "manylinux_2_38_x86_64"), ("py33", "none", "manylinux_2_5_x86_64"), ("py33", "none", "manylinux_2_6_x86_64"), ("py33", "none", "manylinux_2_7_x86_64"), ("py33", "none", "manylinux_2_8_x86_64"), ("py33", "none", "manylinux_2_9_x86_64"), ("py34", "none", "any"), ("py34", "none", "linux_x86_64"), ("py34", "none", "manylinux1_x86_64"), ("py34", "none", "manylinux2010_x86_64"), ("py34", "none", "manylinux2014_x86_64"), ("py34", "none", "manylinux_2_10_x86_64"), ("py34", "none", "manylinux_2_11_x86_64"), ("py34", "none", "manylinux_2_12_x86_64"), ("py34", "none", "manylinux_2_13_x86_64"), ("py34", "none", "manylinux_2_14_x86_64"), ("py34", "none", "manylinux_2_15_x86_64"), ("py34", "none", "manylinux_2_16_x86_64"), ("py34", "none", "manylinux_2_17_x86_64"), ("py34", "none", "manylinux_2_18_x86_64"), ("py34", "none", "manylinux_2_19_x86_64"), ("py34", "none", "manylinux_2_20_x86_64"), ("py34", "none", "manylinux_2_21_x86_64"), ("py34", "none", "manylinux_2_22_x86_64"), ("py34", "none", "manylinux_2_23_x86_64"), ("py34", "none", "manylinux_2_24_x86_64"), ("py34", "none", "manylinux_2_25_x86_64"), ("py34", "none", "manylinux_2_26_x86_64"), ("py34", "none", "manylinux_2_27_x86_64"), ("py34", "none", "manylinux_2_28_x86_64"), ("py34", "none", "manylinux_2_29_x86_64"), ("py34", "none", "manylinux_2_30_x86_64"), ("py34", "none", "manylinux_2_31_x86_64"), ("py34", "none", "manylinux_2_32_x86_64"), ("py34", "none", "manylinux_2_33_x86_64"), ("py34", "none", "manylinux_2_34_x86_64"), ("py34", "none", "manylinux_2_35_x86_64"), ("py34", "none", "manylinux_2_36_x86_64"), ("py34", "none", "manylinux_2_37_x86_64"), ("py34", "none", "manylinux_2_38_x86_64"), ("py34", "none", "manylinux_2_5_x86_64"), ("py34", "none", "manylinux_2_6_x86_64"), ("py34", "none", "manylinux_2_7_x86_64"), ("py34", "none", "manylinux_2_8_x86_64"), ("py34", "none", "manylinux_2_9_x86_64"), ("py35", "none", "any"), ("py35", "none", "linux_x86_64"), ("py35", "none", "manylinux1_x86_64"), ("py35", "none", "manylinux2010_x86_64"), ("py35", "none", "manylinux2014_x86_64"), ("py35", "none", "manylinux_2_10_x86_64"), ("py35", "none", "manylinux_2_11_x86_64"), ("py35", "none", "manylinux_2_12_x86_64"), ("py35", "none", "manylinux_2_13_x86_64"), ("py35", "none", "manylinux_2_14_x86_64"), ("py35", "none", "manylinux_2_15_x86_64"), ("py35", "none", "manylinux_2_16_x86_64"), ("py35", "none", "manylinux_2_17_x86_64"), ("py35", "none", "manylinux_2_18_x86_64"), ("py35", "none", "manylinux_2_19_x86_64"), ("py35", "none", "manylinux_2_20_x86_64"), ("py35", "none", "manylinux_2_21_x86_64"), ("py35", "none", "manylinux_2_22_x86_64"), ("py35", "none", "manylinux_2_23_x86_64"), ("py35", "none", "manylinux_2_24_x86_64"), ("py35", "none", "manylinux_2_25_x86_64"), ("py35", "none", "manylinux_2_26_x86_64"), ("py35", "none", "manylinux_2_27_x86_64"), ("py35", "none", "manylinux_2_28_x86_64"), ("py35", "none", "manylinux_2_29_x86_64"), ("py35", "none", "manylinux_2_30_x86_64"), ("py35", "none", "manylinux_2_31_x86_64"), ("py35", "none", "manylinux_2_32_x86_64"), ("py35", "none", "manylinux_2_33_x86_64"), ("py35", "none", "manylinux_2_34_x86_64"), ("py35", "none", "manylinux_2_35_x86_64"), ("py35", "none", "manylinux_2_36_x86_64"), ("py35", "none", "manylinux_2_37_x86_64"), ("py35", "none", "manylinux_2_38_x86_64"), ("py35", "none", "manylinux_2_5_x86_64"), ("py35", "none", "manylinux_2_6_x86_64"), ("py35", "none", "manylinux_2_7_x86_64"), ("py35", "none", "manylinux_2_8_x86_64"), ("py35", "none", "manylinux_2_9_x86_64"), ("py36", "none", "any"), ("py36", "none", "linux_x86_64"), ("py36", "none", "manylinux1_x86_64"), ("py36", "none", "manylinux2010_x86_64"), ("py36", "none", "manylinux2014_x86_64"), ("py36", "none", "manylinux_2_10_x86_64"), ("py36", "none", "manylinux_2_11_x86_64"), ("py36", "none", "manylinux_2_12_x86_64"), ("py36", "none", "manylinux_2_13_x86_64"), ("py36", "none", "manylinux_2_14_x86_64"), ("py36", "none", "manylinux_2_15_x86_64"), ("py36", "none", "manylinux_2_16_x86_64"), ("py36", "none", "manylinux_2_17_x86_64"), ("py36", "none", "manylinux_2_18_x86_64"), ("py36", "none", "manylinux_2_19_x86_64"), ("py36", "none", "manylinux_2_20_x86_64"), ("py36", "none", "manylinux_2_21_x86_64"), ("py36", "none", "manylinux_2_22_x86_64"), ("py36", "none", "manylinux_2_23_x86_64"), ("py36", "none", "manylinux_2_24_x86_64"), ("py36", "none", "manylinux_2_25_x86_64"), ("py36", "none", "manylinux_2_26_x86_64"), ("py36", "none", "manylinux_2_27_x86_64"), ("py36", "none", "manylinux_2_28_x86_64"), ("py36", "none", "manylinux_2_29_x86_64"), ("py36", "none", "manylinux_2_30_x86_64"), ("py36", "none", "manylinux_2_31_x86_64"), ("py36", "none", "manylinux_2_32_x86_64"), ("py36", "none", "manylinux_2_33_x86_64"), ("py36", "none", "manylinux_2_34_x86_64"), ("py36", "none", "manylinux_2_35_x86_64"), ("py36", "none", "manylinux_2_36_x86_64"), ("py36", "none", "manylinux_2_37_x86_64"), ("py36", "none", "manylinux_2_38_x86_64"), ("py36", "none", "manylinux_2_5_x86_64"), ("py36", "none", "manylinux_2_6_x86_64"), ("py36", "none", "manylinux_2_7_x86_64"), ("py36", "none", "manylinux_2_8_x86_64"), ("py36", "none", "manylinux_2_9_x86_64"), ("py37", "none", "any"), ("py37", "none", "linux_x86_64"), ("py37", "none", "manylinux1_x86_64"), ("py37", "none", "manylinux2010_x86_64"), ("py37", "none", "manylinux2014_x86_64"), ("py37", "none", "manylinux_2_10_x86_64"), ("py37", "none", "manylinux_2_11_x86_64"), ("py37", "none", "manylinux_2_12_x86_64"), ("py37", "none", "manylinux_2_13_x86_64"), ("py37", "none", "manylinux_2_14_x86_64"), ("py37", "none", "manylinux_2_15_x86_64"), ("py37", "none", "manylinux_2_16_x86_64"), ("py37", "none", "manylinux_2_17_x86_64"), ("py37", "none", "manylinux_2_18_x86_64"), ("py37", "none", "manylinux_2_19_x86_64"), ("py37", "none", "manylinux_2_20_x86_64"), ("py37", "none", "manylinux_2_21_x86_64"), ("py37", "none", "manylinux_2_22_x86_64"), ("py37", "none", "manylinux_2_23_x86_64"), ("py37", "none", "manylinux_2_24_x86_64"), ("py37", "none", "manylinux_2_25_x86_64"), ("py37", "none", "manylinux_2_26_x86_64"), ("py37", "none", "manylinux_2_27_x86_64"), ("py37", "none", "manylinux_2_28_x86_64"), ("py37", "none", "manylinux_2_29_x86_64"), ("py37", "none", "manylinux_2_30_x86_64"), ("py37", "none", "manylinux_2_31_x86_64"), ("py37", "none", "manylinux_2_32_x86_64"), ("py37", "none", "manylinux_2_33_x86_64"), ("py37", "none", "manylinux_2_34_x86_64"), ("py37", "none", "manylinux_2_35_x86_64"), ("py37", "none", "manylinux_2_36_x86_64"), ("py37", "none", "manylinux_2_37_x86_64"), ("py37", "none", "manylinux_2_38_x86_64"), ("py37", "none", "manylinux_2_5_x86_64"), ("py37", "none", "manylinux_2_6_x86_64"), ("py37", "none", "manylinux_2_7_x86_64"), ("py37", "none", "manylinux_2_8_x86_64"), ("py37", "none", "manylinux_2_9_x86_64"), ("py38", "none", "any"), ("py38", "none", "linux_x86_64"), ("py38", "none", "manylinux1_x86_64"), ("py38", "none", "manylinux2010_x86_64"), ("py38", "none", "manylinux2014_x86_64"), ("py38", "none", "manylinux_2_10_x86_64"), ("py38", "none", "manylinux_2_11_x86_64"), ("py38", "none", "manylinux_2_12_x86_64"), ("py38", "none", "manylinux_2_13_x86_64"), ("py38", "none", "manylinux_2_14_x86_64"), ("py38", "none", "manylinux_2_15_x86_64"), ("py38", "none", "manylinux_2_16_x86_64"), ("py38", "none", "manylinux_2_17_x86_64"), ("py38", "none", "manylinux_2_18_x86_64"), ("py38", "none", "manylinux_2_19_x86_64"), ("py38", "none", "manylinux_2_20_x86_64"), ("py38", "none", "manylinux_2_21_x86_64"), ("py38", "none", "manylinux_2_22_x86_64"), ("py38", "none", "manylinux_2_23_x86_64"), ("py38", "none", "manylinux_2_24_x86_64"), ("py38", "none", "manylinux_2_25_x86_64"), ("py38", "none", "manylinux_2_26_x86_64"), ("py38", "none", "manylinux_2_27_x86_64"), ("py38", "none", "manylinux_2_28_x86_64"), ("py38", "none", "manylinux_2_29_x86_64"), ("py38", "none", "manylinux_2_30_x86_64"), ("py38", "none", "manylinux_2_31_x86_64"), ("py38", "none", "manylinux_2_32_x86_64"), ("py38", "none", "manylinux_2_33_x86_64"), ("py38", "none", "manylinux_2_34_x86_64"), ("py38", "none", "manylinux_2_35_x86_64"), ("py38", "none", "manylinux_2_36_x86_64"), ("py38", "none", "manylinux_2_37_x86_64"), ("py38", "none", "manylinux_2_38_x86_64"), ("py38", "none", "manylinux_2_5_x86_64"), ("py38", "none", "manylinux_2_6_x86_64"), ("py38", "none", "manylinux_2_7_x86_64"), ("py38", "none", "manylinux_2_8_x86_64"), ("py38", "none", "manylinux_2_9_x86_64"), ("py39", "none", "any"), ("py39", "none", "linux_x86_64"), ("py39", "none", "manylinux1_x86_64"), ("py39", "none", "manylinux2010_x86_64"), ("py39", "none", "manylinux2014_x86_64"), ("py39", "none", "manylinux_2_10_x86_64"), ("py39", "none", "manylinux_2_11_x86_64"), ("py39", "none", "manylinux_2_12_x86_64"), ("py39", "none", "manylinux_2_13_x86_64"), ("py39", "none", "manylinux_2_14_x86_64"), ("py39", "none", "manylinux_2_15_x86_64"), ("py39", "none", "manylinux_2_16_x86_64"), ("py39", "none", "manylinux_2_17_x86_64"), ("py39", "none", "manylinux_2_18_x86_64"), ("py39", "none", "manylinux_2_19_x86_64"), ("py39", "none", "manylinux_2_20_x86_64"), ("py39", "none", "manylinux_2_21_x86_64"), ("py39", "none", "manylinux_2_22_x86_64"), ("py39", "none", "manylinux_2_23_x86_64"), ("py39", "none", "manylinux_2_24_x86_64"), ("py39", "none", "manylinux_2_25_x86_64"), ("py39", "none", "manylinux_2_26_x86_64"), ("py39", "none", "manylinux_2_27_x86_64"), ("py39", "none", "manylinux_2_28_x86_64"), ("py39", "none", "manylinux_2_29_x86_64"), ("py39", "none", "manylinux_2_30_x86_64"), ("py39", "none", "manylinux_2_31_x86_64"), ("py39", "none", "manylinux_2_32_x86_64"), ("py39", "none", "manylinux_2_33_x86_64"), ("py39", "none", "manylinux_2_34_x86_64"), ("py39", "none", "manylinux_2_35_x86_64"), ("py39", "none", "manylinux_2_36_x86_64"), ("py39", "none", "manylinux_2_37_x86_64"), ("py39", "none", "manylinux_2_38_x86_64"), ("py39", "none", "manylinux_2_5_x86_64"), ("py39", "none", "manylinux_2_6_x86_64"), ("py39", "none", "manylinux_2_7_x86_64"), ("py39", "none", "manylinux_2_8_x86_64"), ("py39", "none", "manylinux_2_9_x86_64")] uv-0.9.17+ds1/crates/uv-bench/src/000077500000000000000000000000001520155276700165165ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-bench/src/lib.rs000066400000000000000000000000011520155276700176210ustar00rootroot00000000000000 uv-0.9.17+ds1/crates/uv-bin-install/000077500000000000000000000000001520155276700170645ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-bin-install/Cargo.toml000066400000000000000000000020051520155276700210110ustar00rootroot00000000000000[package] name = "uv-bin-install" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-cache = { workspace = true } uv-client = { workspace = true } uv-distribution-filename = { workspace = true } uv-extract = { workspace = true } uv-fs = { workspace = true } uv-pep440 = { workspace = true } uv-platform = { workspace = true } uv-redacted = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } reqwest = { workspace = true } reqwest-middleware = { workspace = true } reqwest-retry = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } tracing = { workspace = true } url = { workspace = true } uv-0.9.17+ds1/crates/uv-bin-install/README.md000066400000000000000000000010371520155276700203440ustar00rootroot00000000000000 # uv-bin-install This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-bin-install). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-bin-install/src/000077500000000000000000000000001520155276700176535ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-bin-install/src/lib.rs000066400000000000000000000305741520155276700210000ustar00rootroot00000000000000//! Binary download and installation utilities for uv. //! //! These utilities are specifically for consuming distributions that are _not_ Python packages, //! e.g., `ruff` (which does have a Python package, but also has standalone binaries on GitHub). use std::path::PathBuf; use std::pin::Pin; use std::task::{Context, Poll}; use std::time::{Duration, SystemTime}; use futures::TryStreamExt; use reqwest_retry::RetryPolicy; use reqwest_retry::policies::ExponentialBackoff; use std::fmt; use thiserror::Error; use tokio::io::{AsyncRead, ReadBuf}; use tokio_util::compat::FuturesAsyncReadCompatExt; use tracing::debug; use url::Url; use uv_distribution_filename::SourceDistExtension; use uv_cache::{Cache, CacheBucket, CacheEntry}; use uv_client::{BaseClient, is_transient_network_error}; use uv_extract::{Error as ExtractError, stream}; use uv_fs::LockedFileError; use uv_pep440::Version; use uv_platform::Platform; use uv_redacted::DisplaySafeUrl; /// Binary tools that can be installed. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Binary { Ruff, } impl Binary { /// Get the default version for this binary. pub fn default_version(&self) -> Version { match self { // TODO(zanieb): Figure out a nice way to automate updating this Self::Ruff => Version::new([0, 12, 5]), } } /// The name of the binary. /// /// See [`Binary::executable`] for the platform-specific executable name. pub fn name(&self) -> &'static str { match self { Self::Ruff => "ruff", } } /// Get the download URL for a specific version and platform. pub fn download_url( &self, version: &Version, platform: &str, format: ArchiveFormat, ) -> Result { match self { Self::Ruff => { let url = format!( "https://github.com/astral-sh/ruff/releases/download/{version}/ruff-{platform}.{}", format.extension() ); Url::parse(&url).map_err(|err| Error::UrlParse { url, source: err }) } } } /// Get the executable name pub fn executable(&self) -> String { format!("{}{}", self.name(), std::env::consts::EXE_SUFFIX) } } impl fmt::Display for Binary { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.name()) } } /// Archive formats for binary downloads. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ArchiveFormat { Zip, TarGz, } impl ArchiveFormat { /// Get the file extension for this archive format. pub fn extension(&self) -> &'static str { match self { Self::Zip => "zip", Self::TarGz => "tar.gz", } } } impl From for SourceDistExtension { fn from(val: ArchiveFormat) -> Self { match val { ArchiveFormat::Zip => Self::Zip, ArchiveFormat::TarGz => Self::TarGz, } } } /// Errors that can occur during binary download and installation. #[derive(Debug, Error)] pub enum Error { #[error("Failed to download from: {url}")] Download { url: Url, #[source] source: reqwest_middleware::Error, }, #[error("Failed to parse URL: {url}")] UrlParse { url: String, #[source] source: url::ParseError, }, #[error("Failed to extract archive")] Extract { #[source] source: ExtractError, }, #[error("Binary not found in archive at expected location: {expected}")] BinaryNotFound { expected: PathBuf }, #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] LockedFile(#[from] LockedFileError), #[error("Failed to detect platform")] Platform(#[from] uv_platform::Error), #[error("Attempt failed after {retries} {subject}", subject = if *retries > 1 { "retries" } else { "retry" })] RetriedError { #[source] err: Box, retries: u32, }, } impl Error { /// Return the number of attempts that were made to complete this request before this error was /// returned. Note that e.g. 3 retries equates to 4 attempts. fn attempts(&self) -> u32 { if let Self::RetriedError { retries, .. } = self { return retries + 1; } 1 } } /// Install the given binary. pub async fn bin_install( binary: Binary, version: &Version, client: &BaseClient, retry_policy: &ExponentialBackoff, cache: &Cache, reporter: &dyn Reporter, ) -> Result { let platform = Platform::from_env()?; let platform_name = platform.as_cargo_dist_triple(); let cache_entry = CacheEntry::new( cache .bucket(CacheBucket::Binaries) .join(binary.name()) .join(version.to_string()) .join(&platform_name), binary.executable(), ); // Lock the directory to prevent racing installs let _lock = cache_entry.with_file(".lock").lock().await?; if cache_entry.path().exists() { return Ok(cache_entry.into_path_buf()); } let format = if platform.os.is_windows() { ArchiveFormat::Zip } else { ArchiveFormat::TarGz }; let download_url = binary.download_url(version, &platform_name, format)?; let cache_dir = cache_entry.dir(); fs_err::tokio::create_dir_all(&cache_dir).await?; let path = download_and_unpack_with_retry( binary, version, client, retry_policy, cache, reporter, &platform_name, format, &download_url, &cache_entry, ) .await?; // Add executable bit #[cfg(unix)] { use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; let permissions = fs_err::tokio::metadata(&path).await?.permissions(); if permissions.mode() & 0o111 != 0o111 { fs_err::tokio::set_permissions( &path, Permissions::from_mode(permissions.mode() | 0o111), ) .await?; } } Ok(path) } /// Download and unpack a binary with retry on stream failures. async fn download_and_unpack_with_retry( binary: Binary, version: &Version, client: &BaseClient, retry_policy: &ExponentialBackoff, cache: &Cache, reporter: &dyn Reporter, platform_name: &str, format: ArchiveFormat, download_url: &Url, cache_entry: &CacheEntry, ) -> Result { let mut total_attempts = 0; let mut retried_here = false; let start_time = SystemTime::now(); loop { let result = download_and_unpack( binary, version, client, cache, reporter, platform_name, format, download_url, cache_entry, ) .await; let result = match result { Ok(path) => Ok(path), Err(err) => { total_attempts += err.attempts(); let past_retries = total_attempts - 1; if is_transient_network_error(&err) { let retry_decision = retry_policy.should_retry(start_time, past_retries); if let reqwest_retry::RetryDecision::Retry { execute_after } = retry_decision { debug!( "Transient failure while installing {} {}; retrying...", binary.name(), version ); let duration = execute_after .duration_since(SystemTime::now()) .unwrap_or_else(|_| Duration::default()); tokio::time::sleep(duration).await; retried_here = true; continue; } } if retried_here { Err(Error::RetriedError { err: Box::new(err), retries: past_retries, }) } else { Err(err) } } }; return result; } } /// Download and unpackage a binary, /// /// NOTE [`download_and_unpack_with_retry`] should be used instead. async fn download_and_unpack( binary: Binary, version: &Version, client: &BaseClient, cache: &Cache, reporter: &dyn Reporter, platform_name: &str, format: ArchiveFormat, download_url: &Url, cache_entry: &CacheEntry, ) -> Result { // Create a temporary directory for extraction let temp_dir = tempfile::tempdir_in(cache.bucket(CacheBucket::Binaries))?; let response = client .for_host(&DisplaySafeUrl::from_url(download_url.clone())) .get(download_url.clone()) .send() .await .map_err(|err| Error::Download { url: download_url.clone(), source: err, })?; let inner_retries = response .extensions() .get::() .map(|retries| retries.value()); if let Err(status_error) = response.error_for_status_ref() { let err = Error::Download { url: download_url.clone(), source: reqwest_middleware::Error::from(status_error), }; if let Some(retries) = inner_retries { return Err(Error::RetriedError { err: Box::new(err), retries, }); } return Err(err); } // Get the download size from headers if available let size = response .headers() .get(reqwest::header::CONTENT_LENGTH) .and_then(|val| val.to_str().ok()) .and_then(|val| val.parse::().ok()); // Stream download directly to extraction let reader = response .bytes_stream() .map_err(std::io::Error::other) .into_async_read() .compat(); let id = reporter.on_download_start(binary.name(), version, size); let mut progress_reader = ProgressReader::new(reader, id, reporter); stream::archive(&mut progress_reader, format.into(), temp_dir.path()) .await .map_err(|e| Error::Extract { source: e })?; reporter.on_download_complete(id); // Find the binary in the extracted files let extracted_binary = match format { ArchiveFormat::Zip => { // Windows ZIP archives contain the binary directly in the root temp_dir.path().join(binary.executable()) } ArchiveFormat::TarGz => { // tar.gz archives contain the binary in a subdirectory temp_dir .path() .join(format!("{}-{platform_name}", binary.name())) .join(binary.executable()) } }; if !extracted_binary.exists() { return Err(Error::BinaryNotFound { expected: extracted_binary, }); } // Move the binary to its final location before the temp directory is dropped fs_err::tokio::rename(&extracted_binary, cache_entry.path()).await?; Ok(cache_entry.path().to_path_buf()) } /// Progress reporter for binary downloads. pub trait Reporter: Send + Sync { /// Called when a download starts. fn on_download_start(&self, name: &str, version: &Version, size: Option) -> usize; /// Called when download progress is made. fn on_download_progress(&self, id: usize, inc: u64); /// Called when a download completes. fn on_download_complete(&self, id: usize); } /// An asynchronous reader that reports progress as bytes are read. struct ProgressReader<'a, R> { reader: R, index: usize, reporter: &'a dyn Reporter, } impl<'a, R> ProgressReader<'a, R> { /// Create a new [`ProgressReader`] that wraps another reader. fn new(reader: R, index: usize, reporter: &'a dyn Reporter) -> Self { Self { reader, index, reporter, } } } impl AsyncRead for ProgressReader<'_, R> where R: AsyncRead + Unpin, { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { Pin::new(&mut self.as_mut().reader) .poll_read(cx, buf) .map_ok(|()| { self.reporter .on_download_progress(self.index, buf.filled().len() as u64); }) } } uv-0.9.17+ds1/crates/uv-build-backend/000077500000000000000000000000001520155276700173345ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-build-backend/Cargo.toml000066400000000000000000000032711520155276700212670ustar00rootroot00000000000000[package] name = "uv-build-backend" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [dependencies] uv-distribution-filename = { workspace = true } uv-fs = { workspace = true } uv-globfilter = { workspace = true } uv-macros = { workspace = true } uv-normalize = { workspace = true } uv-options-metadata = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } uv-version = { workspace = true } uv-warnings = { workspace = true } base64 = { workspace = true } csv = { workspace = true } flate2 = { workspace = true, default-features = false } fs-err = { workspace = true } globset = { workspace = true } itertools = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true } sha2 = { workspace = true } spdx = { workspace = true } tar = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } tracing = { workspace = true } version-ranges = { workspace = true } walkdir = { workspace = true } zip = { workspace = true } [lints] workspace = true [package.metadata.cargo-shear] # Imported by the `OptionsMetadata` derive macro ignored = ["uv-options-metadata"] [features] schemars = ["dep:schemars", "uv-pypi-types/schemars"] [dev-dependencies] indoc = { workspace = true } insta = { workspace = true } regex = { workspace = true } tempfile = { workspace = true } uv-0.9.17+ds1/crates/uv-build-backend/README.md000066400000000000000000000010431520155276700206110ustar00rootroot00000000000000 # uv-build-backend This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-build-backend). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-build-backend/src/000077500000000000000000000000001520155276700201235ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-build-backend/src/lib.rs000066400000000000000000001650421520155276700212470ustar00rootroot00000000000000use itertools::Itertools; mod metadata; mod serde_verbatim; mod settings; mod source_dist; mod wheel; pub use metadata::{PyProjectToml, check_direct_build}; pub use settings::{BuildBackendSettings, WheelDataIncludes}; pub use source_dist::{build_source_dist, list_source_dist}; use uv_warnings::warn_user_once; pub use wheel::{build_editable, build_wheel, list_wheel, metadata}; use std::collections::HashSet; use std::ffi::OsStr; use std::io; use std::path::{Path, PathBuf}; use std::str::FromStr; use thiserror::Error; use tracing::debug; use walkdir::DirEntry; use uv_fs::Simplified; use uv_globfilter::PortableGlobError; use uv_normalize::PackageName; use uv_pypi_types::{Identifier, IdentifierParseError}; use crate::metadata::ValidationError; use crate::settings::ModuleName; #[derive(Debug, Error)] pub enum Error { #[error(transparent)] Io(#[from] io::Error), #[error("Invalid metadata format in: {}", _0.user_display())] Toml(PathBuf, #[source] toml::de::Error), #[error("Invalid project metadata")] Validation(#[from] ValidationError), #[error("Invalid module name: {0}")] InvalidModuleName(String, #[source] IdentifierParseError), #[error("Unsupported glob expression in: {field}")] PortableGlob { field: String, #[source] source: PortableGlobError, }, /// #[error("Glob expressions caused to large regex in: {field}")] GlobSetTooLarge { field: String, #[source] source: globset::Error, }, #[error("`pyproject.toml` must not be excluded from source distribution build")] PyprojectTomlExcluded, #[error("Failed to walk source tree: {}", root.user_display())] WalkDir { root: PathBuf, #[source] err: walkdir::Error, }, #[error("Failed to write wheel zip archive")] Zip(#[from] zip::result::ZipError), #[error("Failed to write RECORD file")] Csv(#[from] csv::Error), #[error("Expected a Python module at: {}", _0.user_display())] MissingInitPy(PathBuf), #[error("For namespace packages, `__init__.py[i]` is not allowed in parent directory: {}", _0.user_display())] NotANamespace(PathBuf), /// Either an absolute path or a parent path through `..`. #[error("Module root must be inside the project: {}", _0.user_display())] InvalidModuleRoot(PathBuf), /// Either an absolute path or a parent path through `..`. #[error("The path for the data directory {} must be inside the project: {}", name, path.user_display())] InvalidDataRoot { name: String, path: PathBuf }, #[error("Virtual environments must not be added to source distributions or wheels, remove the directory or exclude it from the build: {}", _0.user_display())] VenvInSourceTree(PathBuf), #[error("Inconsistent metadata between prepare and build step: {0}")] InconsistentSteps(&'static str), #[error("Failed to write to {}", _0.user_display())] TarWrite(PathBuf, #[source] io::Error), } /// Dispatcher between writing to a directory, writing to a zip, writing to a `.tar.gz` and /// listing files. /// /// All paths are string types instead of path types since wheels are portable between platforms. /// /// Contract: You must call close before dropping to obtain a valid output (dropping is fine in the /// error case). trait DirectoryWriter { /// Add a file with the given content. /// /// Files added through the method are considered generated when listing included files. fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error>; /// Add the file or directory to the path. fn write_dir_entry(&mut self, entry: &DirEntry, target_path: &str) -> Result<(), Error> { if entry.file_type().is_dir() { self.write_directory(target_path)?; } else { self.write_file(target_path, entry.path())?; } Ok(()) } /// Add a local file. fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error>; /// Create a directory. fn write_directory(&mut self, directory: &str) -> Result<(), Error>; /// Write the `RECORD` file and if applicable, the central directory. fn close(self, dist_info_dir: &str) -> Result<(), Error>; } /// Name of the file in the archive and path outside, if it wasn't generated. pub(crate) type FileList = Vec<(String, Option)>; /// A dummy writer to collect the file names that would be included in a build. pub(crate) struct ListWriter<'a> { files: &'a mut FileList, } impl<'a> ListWriter<'a> { /// Convert the writer to the collected file names. pub(crate) fn new(files: &'a mut FileList) -> Self { Self { files } } } impl DirectoryWriter for ListWriter<'_> { fn write_bytes(&mut self, path: &str, _bytes: &[u8]) -> Result<(), Error> { self.files.push((path.to_string(), None)); Ok(()) } fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> { self.files .push((path.to_string(), Some(file.to_path_buf()))); Ok(()) } fn write_directory(&mut self, _directory: &str) -> Result<(), Error> { Ok(()) } fn close(self, _dist_info_dir: &str) -> Result<(), Error> { Ok(()) } } /// PEP 517 requires that the metadata directory from the prepare metadata call is identical to the /// build wheel call. This method performs a prudence check that `METADATA` and `entry_points.txt` /// match. fn check_metadata_directory( source_tree: &Path, metadata_directory: Option<&Path>, pyproject_toml: &PyProjectToml, ) -> Result<(), Error> { let Some(metadata_directory) = metadata_directory else { return Ok(()); }; debug!( "Checking metadata directory {}", metadata_directory.user_display() ); // `METADATA` is a mandatory file. let current = pyproject_toml .to_metadata(source_tree)? .core_metadata_format(); let previous = fs_err::read_to_string(metadata_directory.join("METADATA"))?; if previous != current { return Err(Error::InconsistentSteps("METADATA")); } // `entry_points.txt` is not written if it would be empty. let entrypoints_path = metadata_directory.join("entry_points.txt"); match pyproject_toml.to_entry_points()? { None => { if entrypoints_path.is_file() { return Err(Error::InconsistentSteps("entry_points.txt")); } } Some(entrypoints) => { if fs_err::read_to_string(&entrypoints_path)? != entrypoints { return Err(Error::InconsistentSteps("entry_points.txt")); } } } Ok(()) } /// Returns the list of module names without names which would be included twice /// /// In normal cases it should do nothing: /// /// * `["aaa"] -> ["aaa"]` /// * `["aaa", "bbb"] -> ["aaa", "bbb"]` /// /// Duplicate elements are removed: /// /// * `["aaa", "aaa"] -> ["aaa"]` /// * `["bbb", "aaa", "bbb"] -> ["aaa", "bbb"]` /// /// Names with more specific paths are removed in favour of more general paths: /// /// * `["aaa.foo", "aaa"] -> ["aaa"]` /// * `["bbb", "aaa", "bbb.foo", "ccc.foo", "ccc.foo.bar", "aaa"] -> ["aaa", "bbb.foo", "ccc.foo"]` /// /// This does not preserve the order of the elements. fn prune_redundant_modules(mut names: Vec) -> Vec { names.sort(); let mut pruned = Vec::with_capacity(names.len()); for name in names { if let Some(last) = pruned.last() { if name == *last { continue; } // This is a more specific (narrow) module name than what came before if name .strip_prefix(last) .is_some_and(|suffix| suffix.starts_with('.')) { continue; } } pruned.push(name); } pruned } /// Wraps [`prune_redundant_modules`] with a conditional warning when modules are ignored fn prune_redundant_modules_warn(names: &[String], show_warnings: bool) -> Vec { let pruned = prune_redundant_modules(names.to_vec()); if show_warnings && names.len() != pruned.len() { let mut pruned: HashSet<_> = pruned.iter().collect(); let ignored: Vec<_> = names.iter().filter(|name| !pruned.remove(name)).collect(); let s = if ignored.len() == 1 { "" } else { "s" }; warn_user_once!( "Ignoring redundant module name{s} in `tool.uv.build-backend.module-name`: `{}`", ignored.into_iter().join("`, `") ); } pruned } /// Returns the source root and the module path(s) with the `__init__.py[i]` below to it while /// checking the project layout and names. /// /// Some target platforms have case-sensitive filesystems, while others have case-insensitive /// filesystems. We always lower case the package name, our default for the module, while some /// users want uppercase letters in their module names. For example, the package name is `pil_util`, /// but the module `PIL_util`. To make the behavior as consistent as possible across platforms as /// possible, we require that an upper case name is given explicitly through /// `tool.uv.build-backend.module-name`. /// /// By default, the dist-info-normalized package name is the module name. For /// dist-info-normalization, the rules are lowercasing, replacing `.` with `_` and /// replace `-` with `_`. Since `.` and `-` are not allowed in identifiers, we can use a string /// comparison with the module name. /// /// While we recommend one module per package, it is possible to declare a list of modules. fn find_roots( source_tree: &Path, pyproject_toml: &PyProjectToml, relative_module_root: &Path, module_name: Option<&ModuleName>, namespace: bool, show_warnings: bool, ) -> Result<(PathBuf, Vec), Error> { let relative_module_root = uv_fs::normalize_path(relative_module_root); // Check that even if a path contains `..`, we only include files below the module root. if !uv_fs::normalize_path(&source_tree.join(&relative_module_root)) .starts_with(uv_fs::normalize_path(source_tree)) { return Err(Error::InvalidModuleRoot(relative_module_root.to_path_buf())); } let src_root = source_tree.join(&relative_module_root); debug!("Source root: {}", src_root.user_display()); if namespace { // `namespace = true` disables module structure checks. let modules_relative = if let Some(module_name) = module_name { match module_name { ModuleName::Name(name) => { vec![name.split('.').collect::()] } ModuleName::Names(names) => prune_redundant_modules_warn(names, show_warnings) .into_iter() .map(|name| name.split('.').collect::()) .collect(), } } else { vec![PathBuf::from( pyproject_toml.name().as_dist_info_name().to_string(), )] }; for module_relative in &modules_relative { debug!("Namespace module path: {}", module_relative.user_display()); } return Ok((src_root, modules_relative)); } let modules_relative = if let Some(module_name) = module_name { match module_name { ModuleName::Name(name) => vec![module_path_from_module_name(&src_root, name)?], ModuleName::Names(names) => prune_redundant_modules_warn(names, show_warnings) .into_iter() .map(|name| module_path_from_module_name(&src_root, &name)) .collect::>()?, } } else { vec![find_module_path_from_package_name( &src_root, pyproject_toml.name(), )?] }; for module_relative in &modules_relative { debug!("Module path: {}", module_relative.user_display()); } Ok((src_root, modules_relative)) } /// Infer stubs packages from package name alone. /// /// There are potential false positives if someone had a regular package with `-stubs`. /// The `Identifier` checks in `module_path_from_module_name` are here covered by the `PackageName` /// validation. fn find_module_path_from_package_name( src_root: &Path, package_name: &PackageName, ) -> Result { if let Some(stem) = package_name.to_string().strip_suffix("-stubs") { debug!("Building stubs package instead of a regular package"); let module_name = PackageName::from_str(stem) .expect("non-empty package name prefix must be valid package name") .as_dist_info_name() .to_string(); let module_relative = PathBuf::from(format!("{module_name}-stubs")); let init_pyi = src_root.join(&module_relative).join("__init__.pyi"); if !init_pyi.is_file() { return Err(Error::MissingInitPy(init_pyi)); } Ok(module_relative) } else { // This name is always lowercase. let module_relative = PathBuf::from(package_name.as_dist_info_name().to_string()); let init_py = src_root.join(&module_relative).join("__init__.py"); if !init_py.is_file() { return Err(Error::MissingInitPy(init_py)); } Ok(module_relative) } } /// Determine the relative module path from an explicit module name. fn module_path_from_module_name(src_root: &Path, module_name: &str) -> Result { // This name can be uppercase. let module_relative = module_name.split('.').collect::(); // Check if we have a regular module or a namespace. let (root_name, namespace_segments) = if let Some((root_name, namespace_segments)) = module_name.split_once('.') { ( root_name, namespace_segments.split('.').collect::>(), ) } else { (module_name, Vec::new()) }; // Check if we have an implementation or a stubs package. // For stubs for a namespace, the `-stubs` prefix must be on the root. let stubs = if let Some(stem) = root_name.strip_suffix("-stubs") { // Check that the stubs belong to a valid module. Identifier::from_str(stem) .map_err(|err| Error::InvalidModuleName(module_name.to_string(), err))?; true } else { Identifier::from_str(root_name) .map_err(|err| Error::InvalidModuleName(module_name.to_string(), err))?; false }; // For a namespace, check that all names below the root is valid. for segment in namespace_segments { Identifier::from_str(segment) .map_err(|err| Error::InvalidModuleName(module_name.to_string(), err))?; } // Check that an `__init__.py[i]` exists for the module. let init_py = src_root .join(&module_relative) .join(if stubs { "__init__.pyi" } else { "__init__.py" }); if !init_py.is_file() { return Err(Error::MissingInitPy(init_py)); } // For a namespace, check that the directories above the lowest are namespace directories. for namespace_dir in module_relative.ancestors().skip(1) { if src_root.join(namespace_dir).join("__init__.py").exists() || src_root.join(namespace_dir).join("__init__.pyi").exists() { return Err(Error::NotANamespace(src_root.join(namespace_dir))); } } Ok(module_relative) } /// Error if we're adding a venv to a distribution. pub(crate) fn error_on_venv(file_name: &OsStr, path: &Path) -> Result<(), Error> { // On 64-bit Unix, `lib64` is a (compatibility) symlink to lib. If we traverse `lib64` before // `pyvenv.cfg`, we show a generic error for symlink directories instead. if !(file_name == "pyvenv.cfg" || file_name == "lib64") { return Ok(()); } let Some(parent) = path.parent() else { return Ok(()); }; if parent.join("bin").join("python").is_symlink() || parent.join("Scripts").join("python.exe").is_file() { return Err(Error::VenvInSourceTree(parent.to_path_buf())); } Ok(()) } #[cfg(test)] mod tests { use super::*; use flate2::bufread::GzDecoder; use fs_err::File; use indoc::indoc; use insta::assert_snapshot; use itertools::Itertools; use regex::Regex; use sha2::Digest; use std::io::{BufReader, Read}; use std::iter; use tempfile::TempDir; use uv_distribution_filename::{SourceDistFilename, WheelFilename}; use uv_fs::{copy_dir_all, relative_to}; const MOCK_UV_VERSION: &str = "1.0.0+test"; fn format_err(err: &Error) -> String { let context = iter::successors(std::error::Error::source(&err), |&err| err.source()) .map(|err| format!(" Caused by: {err}")) .join("\n"); err.to_string() + "\n" + &context } /// File listings, generated archives and archive contents for both a build with /// source tree -> wheel /// and a build with /// source tree -> source dist -> wheel. #[derive(Debug, PartialEq, Eq)] struct BuildResults { source_dist_list_files: FileList, source_dist_filename: SourceDistFilename, source_dist_contents: Vec, wheel_list_files: FileList, wheel_filename: WheelFilename, wheel_contents: Vec, } /// Run both a direct wheel build and an indirect wheel build through a source distribution, /// while checking that directly built wheel and indirectly built wheel are the same. fn build(source_root: &Path, dist: &Path) -> Result { // Build a direct wheel, capture all its properties to compare it with the indirect wheel // latest and remove it since it has the same filename as the indirect wheel. let (_name, direct_wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION, false)?; let direct_wheel_filename = build_wheel(source_root, dist, None, MOCK_UV_VERSION, false)?; let direct_wheel_path = dist.join(direct_wheel_filename.to_string()); let direct_wheel_contents = wheel_contents(&direct_wheel_path); let direct_wheel_hash = sha2::Sha256::digest(fs_err::read(&direct_wheel_path)?); fs_err::remove_file(&direct_wheel_path)?; // Build a source distribution. let (_name, source_dist_list_files) = list_source_dist(source_root, MOCK_UV_VERSION, false)?; // TODO(konsti): This should run in the unpacked source dist tempdir, but we need to // normalize the path. let (_name, wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION, false)?; let source_dist_filename = build_source_dist(source_root, dist, MOCK_UV_VERSION, false)?; let source_dist_path = dist.join(source_dist_filename.to_string()); let source_dist_contents = sdist_contents(&source_dist_path); // Unpack the source distribution and build a wheel from it. let sdist_tree = TempDir::new()?; let sdist_reader = BufReader::new(File::open(&source_dist_path)?); let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader)); source_dist.unpack(sdist_tree.path())?; let sdist_top_level_directory = sdist_tree.path().join(format!( "{}-{}", source_dist_filename.name.as_dist_info_name(), source_dist_filename.version )); let wheel_filename = build_wheel( &sdist_top_level_directory, dist, None, MOCK_UV_VERSION, false, )?; let wheel_contents = wheel_contents(&dist.join(wheel_filename.to_string())); // Check that direct and indirect wheels are identical. assert_eq!(direct_wheel_filename, wheel_filename); assert_eq!(direct_wheel_contents, wheel_contents); assert_eq!(direct_wheel_list_files, wheel_list_files); assert_eq!( direct_wheel_hash, sha2::Sha256::digest(fs_err::read(dist.join(wheel_filename.to_string()))?) ); Ok(BuildResults { source_dist_list_files, source_dist_filename, source_dist_contents, wheel_list_files, wheel_filename, wheel_contents, }) } fn build_err(source_root: &Path) -> String { let dist = TempDir::new().unwrap(); let build_err = build(source_root, dist.path()).unwrap_err(); let err_message: String = format_err(&build_err) .replace(&source_root.user_display().to_string(), "[TEMP_PATH]") .replace('\\', "/"); err_message } fn sdist_contents(source_dist_path: &Path) -> Vec { let sdist_reader = BufReader::new(File::open(source_dist_path).unwrap()); let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader)); let mut source_dist_contents: Vec<_> = source_dist .entries() .unwrap() .map(|entry| { entry .unwrap() .path() .unwrap() .to_str() .unwrap() .replace('\\', "/") }) .collect(); source_dist_contents.sort(); source_dist_contents } fn wheel_contents(direct_output_dir: &Path) -> Vec { let wheel = zip::ZipArchive::new(File::open(direct_output_dir).unwrap()).unwrap(); let mut wheel_contents: Vec<_> = wheel .file_names() .map(|path| path.replace('\\', "/")) .collect(); wheel_contents.sort_unstable(); wheel_contents } fn format_file_list(file_list: FileList, src: &Path) -> String { file_list .into_iter() .map(|(path, source)| { let path = path.replace('\\', "/"); if let Some(source) = source { let source = relative_to(source, src) .unwrap() .portable_display() .to_string(); format!("{path} ({source})") } else { format!("{path} (generated)") } }) .join("\n") } /// Tests that builds are stable and include the right files and. /// /// Tests that both source tree -> source dist -> wheel and source tree -> wheel include the /// right files. Also checks that the resulting archives are byte-by-byte identical /// independent of the build path or platform, with the caveat that we cannot serialize an /// executable bit on Window. This ensures reproducible builds and best-effort /// platform-independent deterministic builds. #[test] fn built_by_uv_building() { let built_by_uv = Path::new("../../test/packages/built-by-uv"); let src = TempDir::new().unwrap(); for dir in [ "src", "tests", "data-dir", "third-party-licenses", "assets", "header", "scripts", ] { copy_dir_all(built_by_uv.join(dir), src.path().join(dir)).unwrap(); } for filename in [ "pyproject.toml", "README.md", "uv.lock", "LICENSE-APACHE", "LICENSE-MIT", ] { fs_err::copy(built_by_uv.join(filename), src.path().join(filename)).unwrap(); } // Clear executable bit on Unix to build the same archive between Unix and Windows. // This is a caveat to the determinism of the uv build backend: When a file has the // executable in the source repository, it only has the executable bit on Unix, as Windows // does not have the concept of the executable bit. #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let path = src.path().join("scripts").join("whoami.sh"); let metadata = fs_err::metadata(&path).unwrap(); let mut perms = metadata.permissions(); perms.set_mode(perms.mode() & !0o111); fs_err::set_permissions(&path, perms).unwrap(); } // Redact the uv_build version to keep the hash stable across releases let pyproject_toml = fs_err::read_to_string(src.path().join("pyproject.toml")).unwrap(); let current_requires = Regex::new(r#"requires = \["uv_build>=[0-9.]+,<[0-9.]+"\]"#).unwrap(); let mocked_requires = r#"requires = ["uv_build>=1,<2"]"#; let pyproject_toml = current_requires.replace(pyproject_toml.as_str(), mocked_requires); fs_err::write(src.path().join("pyproject.toml"), pyproject_toml.as_bytes()).unwrap(); // Add some files to be excluded let module_root = src.path().join("src").join("built_by_uv"); fs_err::create_dir_all(module_root.join("__pycache__")).unwrap(); File::create(module_root.join("__pycache__").join("compiled.pyc")).unwrap(); File::create(module_root.join("arithmetic").join("circle.pyc")).unwrap(); // Perform both the direct and the indirect build. let dist = TempDir::new().unwrap(); let build = build(src.path(), dist.path()).unwrap(); let source_dist_path = dist.path().join(build.source_dist_filename.to_string()); assert_eq!( build.source_dist_filename.to_string(), "built_by_uv-0.1.0.tar.gz" ); // Check that the source dist is reproducible across platforms. assert_snapshot!( format!("{:x}", sha2::Sha256::digest(fs_err::read(&source_dist_path).unwrap())), @"871d1f859140721b67cbeaca074e7a2740c88c38028d0509eba87d1285f1da9e" ); // Check both the files we report and the actual files assert_snapshot!(format_file_list(build.source_dist_list_files, src.path()), @r" built_by_uv-0.1.0/PKG-INFO (generated) built_by_uv-0.1.0/LICENSE-APACHE (LICENSE-APACHE) built_by_uv-0.1.0/LICENSE-MIT (LICENSE-MIT) built_by_uv-0.1.0/README.md (README.md) built_by_uv-0.1.0/assets/data.csv (assets/data.csv) built_by_uv-0.1.0/header/built_by_uv.h (header/built_by_uv.h) built_by_uv-0.1.0/pyproject.toml (pyproject.toml) built_by_uv-0.1.0/scripts/whoami.sh (scripts/whoami.sh) built_by_uv-0.1.0/src/built_by_uv/__init__.py (src/built_by_uv/__init__.py) built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py) built_by_uv-0.1.0/src/built_by_uv/arithmetic/circle.py (src/built_by_uv/arithmetic/circle.py) built_by_uv-0.1.0/src/built_by_uv/arithmetic/pi.txt (src/built_by_uv/arithmetic/pi.txt) built_by_uv-0.1.0/src/built_by_uv/build-only.h (src/built_by_uv/build-only.h) built_by_uv-0.1.0/src/built_by_uv/cli.py (src/built_by_uv/cli.py) built_by_uv-0.1.0/third-party-licenses/PEP-401.txt (third-party-licenses/PEP-401.txt) "); assert_snapshot!(build.source_dist_contents.iter().join("\n"), @r" built_by_uv-0.1.0/ built_by_uv-0.1.0/LICENSE-APACHE built_by_uv-0.1.0/LICENSE-MIT built_by_uv-0.1.0/PKG-INFO built_by_uv-0.1.0/README.md built_by_uv-0.1.0/assets built_by_uv-0.1.0/assets/data.csv built_by_uv-0.1.0/header built_by_uv-0.1.0/header/built_by_uv.h built_by_uv-0.1.0/pyproject.toml built_by_uv-0.1.0/scripts built_by_uv-0.1.0/scripts/whoami.sh built_by_uv-0.1.0/src built_by_uv-0.1.0/src/built_by_uv built_by_uv-0.1.0/src/built_by_uv/__init__.py built_by_uv-0.1.0/src/built_by_uv/arithmetic built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py built_by_uv-0.1.0/src/built_by_uv/arithmetic/circle.py built_by_uv-0.1.0/src/built_by_uv/arithmetic/pi.txt built_by_uv-0.1.0/src/built_by_uv/build-only.h built_by_uv-0.1.0/src/built_by_uv/cli.py built_by_uv-0.1.0/third-party-licenses built_by_uv-0.1.0/third-party-licenses/PEP-401.txt "); let wheel_path = dist.path().join(build.wheel_filename.to_string()); assert_eq!( build.wheel_filename.to_string(), "built_by_uv-0.1.0-py3-none-any.whl" ); // Check that the wheel is reproducible across platforms. assert_snapshot!( format!("{:x}", sha2::Sha256::digest(fs_err::read(&wheel_path).unwrap())), @"319afb04e87caf894b1362b508ec745253c6d241423ea59021694d2015e821da" ); assert_snapshot!(build.wheel_contents.join("\n"), @r" built_by_uv-0.1.0.data/data/ built_by_uv-0.1.0.data/data/data.csv built_by_uv-0.1.0.data/headers/ built_by_uv-0.1.0.data/headers/built_by_uv.h built_by_uv-0.1.0.data/scripts/ built_by_uv-0.1.0.data/scripts/whoami.sh built_by_uv-0.1.0.dist-info/ built_by_uv-0.1.0.dist-info/METADATA built_by_uv-0.1.0.dist-info/RECORD built_by_uv-0.1.0.dist-info/WHEEL built_by_uv-0.1.0.dist-info/entry_points.txt built_by_uv-0.1.0.dist-info/licenses/ built_by_uv-0.1.0.dist-info/licenses/LICENSE-APACHE built_by_uv-0.1.0.dist-info/licenses/LICENSE-MIT built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/ built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/PEP-401.txt built_by_uv/ built_by_uv/__init__.py built_by_uv/arithmetic/ built_by_uv/arithmetic/__init__.py built_by_uv/arithmetic/circle.py built_by_uv/arithmetic/pi.txt built_by_uv/cli.py "); assert_snapshot!(format_file_list(build.wheel_list_files, src.path()), @r" built_by_uv/__init__.py (src/built_by_uv/__init__.py) built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py) built_by_uv/arithmetic/circle.py (src/built_by_uv/arithmetic/circle.py) built_by_uv/arithmetic/pi.txt (src/built_by_uv/arithmetic/pi.txt) built_by_uv/cli.py (src/built_by_uv/cli.py) built_by_uv-0.1.0.dist-info/licenses/LICENSE-APACHE (LICENSE-APACHE) built_by_uv-0.1.0.dist-info/licenses/LICENSE-MIT (LICENSE-MIT) built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/PEP-401.txt (third-party-licenses/PEP-401.txt) built_by_uv-0.1.0.data/headers/built_by_uv.h (header/built_by_uv.h) built_by_uv-0.1.0.data/scripts/whoami.sh (scripts/whoami.sh) built_by_uv-0.1.0.data/data/data.csv (assets/data.csv) built_by_uv-0.1.0.dist-info/WHEEL (generated) built_by_uv-0.1.0.dist-info/entry_points.txt (generated) built_by_uv-0.1.0.dist-info/METADATA (generated) "); let mut wheel = zip::ZipArchive::new(File::open(wheel_path).unwrap()).unwrap(); let mut record = String::new(); wheel .by_name("built_by_uv-0.1.0.dist-info/RECORD") .unwrap() .read_to_string(&mut record) .unwrap(); assert_snapshot!(record, @r###" built_by_uv/__init__.py,sha256=AJ7XpTNWxYktP97ydb81UpnNqoebH7K4sHRakAMQKG4,44 built_by_uv/arithmetic/__init__.py,sha256=x2agwFbJAafc9Z6TdJ0K6b6bLMApQdvRSQjP4iy7IEI,67 built_by_uv/arithmetic/circle.py,sha256=FYZkv6KwrF9nJcwGOKigjke1dm1Fkie7qW1lWJoh3AE,287 built_by_uv/arithmetic/pi.txt,sha256=-4HqoLoIrSKGf0JdTrM8BTTiIz8rq-MSCDL6LeF0iuU,8 built_by_uv/cli.py,sha256=Jcm3PxSb8wTAN3dGm5vKEDQwCgoUXkoeggZeF34QyKM,44 built_by_uv-0.1.0.dist-info/licenses/LICENSE-APACHE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356 built_by_uv-0.1.0.dist-info/licenses/LICENSE-MIT,sha256=F5Z0Cpu8QWyblXwXhrSo0b9WmYXQxd1LwLjVLJZwbiI,1077 built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/PEP-401.txt,sha256=KN-KAx829G2saLjVmByc08RFFtIDWvHulqPyD0qEBZI,270 built_by_uv-0.1.0.data/headers/built_by_uv.h,sha256=p5-HBunJ1dY-xd4dMn03PnRClmGyRosScIp8rT46kg4,144 built_by_uv-0.1.0.data/scripts/whoami.sh,sha256=T2cmhuDFuX-dTkiSkuAmNyIzvv8AKopjnuTCcr9o-eE,20 built_by_uv-0.1.0.data/data/data.csv,sha256=7z7u-wXu7Qr2eBZFVpBILlNUiGSngv_1vYqZHVWOU94,265 built_by_uv-0.1.0.dist-info/WHEEL,sha256=PaG_oOj9G2zCRqoLK0SjWBVZbGAMtIXDmm-MEGw9Wo0,83 built_by_uv-0.1.0.dist-info/entry_points.txt,sha256=-IO6yaq6x6HSl-zWH96rZmgYvfyHlH00L5WQoCpz-YI,50 built_by_uv-0.1.0.dist-info/METADATA,sha256=m6EkVvKrGmqx43b_VR45LHD37IZxPYC0NI6Qx9_UXLE,474 built_by_uv-0.1.0.dist-info/RECORD,, "###); } /// Test that `license = { file = "LICENSE" }` is supported. #[test] fn license_file_pre_pep639() { let src = TempDir::new().unwrap(); fs_err::write( src.path().join("pyproject.toml"), indoc! {r#" [project] name = "pep-pep639-license" version = "1.0.0" license = { file = "license.txt" } [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" "# }, ) .unwrap(); fs_err::create_dir_all(src.path().join("src").join("pep_pep639_license")).unwrap(); File::create( src.path() .join("src") .join("pep_pep639_license") .join("__init__.py"), ) .unwrap(); fs_err::write( src.path().join("license.txt"), "Copy carefully.\nSincerely, the authors", ) .unwrap(); // Build a wheel from a source distribution let output_dir = TempDir::new().unwrap(); build_source_dist(src.path(), output_dir.path(), "0.5.15", false).unwrap(); let sdist_tree = TempDir::new().unwrap(); let source_dist_path = output_dir.path().join("pep_pep639_license-1.0.0.tar.gz"); let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap()); let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader)); source_dist.unpack(sdist_tree.path()).unwrap(); build_wheel( &sdist_tree.path().join("pep_pep639_license-1.0.0"), output_dir.path(), None, "0.5.15", false, ) .unwrap(); let wheel = output_dir .path() .join("pep_pep639_license-1.0.0-py3-none-any.whl"); let mut wheel = zip::ZipArchive::new(File::open(wheel).unwrap()).unwrap(); let mut metadata = String::new(); wheel .by_name("pep_pep639_license-1.0.0.dist-info/METADATA") .unwrap() .read_to_string(&mut metadata) .unwrap(); assert_snapshot!(metadata, @r###" Metadata-Version: 2.3 Name: pep-pep639-license Version: 1.0.0 License: Copy carefully. Sincerely, the authors "###); } /// Test that `build_wheel` works after the `prepare_metadata_for_build_wheel` hook. #[test] fn prepare_metadata_then_build_wheel() { let src = TempDir::new().unwrap(); fs_err::write( src.path().join("pyproject.toml"), indoc! {r#" [project] name = "two-step-build" version = "1.0.0" [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" "# }, ) .unwrap(); fs_err::create_dir_all(src.path().join("src").join("two_step_build")).unwrap(); File::create( src.path() .join("src") .join("two_step_build") .join("__init__.py"), ) .unwrap(); // Prepare the metadata. let metadata_dir = TempDir::new().unwrap(); let dist_info_dir = metadata(src.path(), metadata_dir.path(), "0.5.15").unwrap(); let metadata_prepared = fs_err::read_to_string(metadata_dir.path().join(&dist_info_dir).join("METADATA")) .unwrap(); // Build the wheel, using the prepared metadata directory. let output_dir = TempDir::new().unwrap(); build_wheel( src.path(), output_dir.path(), Some(&metadata_dir.path().join(&dist_info_dir)), "0.5.15", false, ) .unwrap(); let wheel = output_dir .path() .join("two_step_build-1.0.0-py3-none-any.whl"); let mut wheel = zip::ZipArchive::new(File::open(wheel).unwrap()).unwrap(); let mut metadata_wheel = String::new(); wheel .by_name("two_step_build-1.0.0.dist-info/METADATA") .unwrap() .read_to_string(&mut metadata_wheel) .unwrap(); assert_eq!(metadata_prepared, metadata_wheel); assert_snapshot!(metadata_wheel, @r###" Metadata-Version: 2.3 Name: two-step-build Version: 1.0.0 "###); } /// Check that non-normalized paths for `module-root` work with the glob inclusions. #[test] fn test_glob_path_normalization() { let src = TempDir::new().unwrap(); fs_err::write( src.path().join("pyproject.toml"), indoc! {r#" [project] name = "two-step-build" version = "1.0.0" [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" [tool.uv.build-backend] module-root = "./" "# }, ) .unwrap(); fs_err::create_dir_all(src.path().join("two_step_build")).unwrap(); File::create(src.path().join("two_step_build").join("__init__.py")).unwrap(); let dist = TempDir::new().unwrap(); let build1 = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build1.source_dist_contents.join("\n"), @r" two_step_build-1.0.0/ two_step_build-1.0.0/PKG-INFO two_step_build-1.0.0/pyproject.toml two_step_build-1.0.0/two_step_build two_step_build-1.0.0/two_step_build/__init__.py "); assert_snapshot!(build1.wheel_contents.join("\n"), @r" two_step_build-1.0.0.dist-info/ two_step_build-1.0.0.dist-info/METADATA two_step_build-1.0.0.dist-info/RECORD two_step_build-1.0.0.dist-info/WHEEL two_step_build/ two_step_build/__init__.py "); // A path with a parent reference. fs_err::write( src.path().join("pyproject.toml"), indoc! {r#" [project] name = "two-step-build" version = "1.0.0" [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" [tool.uv.build-backend] module-root = "two_step_build/.././" "# }, ) .unwrap(); let dist = TempDir::new().unwrap(); let build2 = build(src.path(), dist.path()).unwrap(); assert_eq!(build1, build2); } /// Check that upper case letters in module names work. #[test] fn test_camel_case() { let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] name = "camelcase" version = "1.0.0" [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" [tool.uv.build-backend] module-name = "camelCase" "# }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); fs_err::create_dir_all(src.path().join("src").join("camelCase")).unwrap(); File::create(src.path().join("src").join("camelCase").join("__init__.py")).unwrap(); let dist = TempDir::new().unwrap(); let build1 = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build1.wheel_contents.join("\n"), @r" camelCase/ camelCase/__init__.py camelcase-1.0.0.dist-info/ camelcase-1.0.0.dist-info/METADATA camelcase-1.0.0.dist-info/RECORD camelcase-1.0.0.dist-info/WHEEL "); // Check that an explicit wrong casing fails to build. fs_err::write( src.path().join("pyproject.toml"), pyproject_toml.replace("camelCase", "camel_case"), ) .unwrap(); let build_err = build(src.path(), dist.path()).unwrap_err(); let err_message = format_err(&build_err) .replace(&src.path().user_display().to_string(), "[TEMP_PATH]") .replace('\\', "/"); assert_snapshot!( err_message, @"Expected a Python module at: [TEMP_PATH]/src/camel_case/__init__.py" ); } #[test] fn invalid_stubs_name() { let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] name = "camelcase" version = "1.0.0" [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" [tool.uv.build-backend] module-name = "django@home-stubs" "# }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); let dist = TempDir::new().unwrap(); let build_err = build(src.path(), dist.path()).unwrap_err(); let err_message = format_err(&build_err); assert_snapshot!( err_message, @r" Invalid module name: django@home-stubs Caused by: Invalid character `@` at position 7 for identifier `django@home`, expected an underscore or an alphanumeric character " ); } /// Stubs packages use a special name and `__init__.pyi`. #[test] fn stubs_package() { let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] name = "stuffed-bird-stubs" version = "1.0.0" [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" "# }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); fs_err::create_dir_all(src.path().join("src").join("stuffed_bird-stubs")).unwrap(); // That's the wrong file, we're expecting a `__init__.pyi`. let regular_init_py = src .path() .join("src") .join("stuffed_bird-stubs") .join("__init__.py"); File::create(®ular_init_py).unwrap(); let dist = TempDir::new().unwrap(); let build_err = build(src.path(), dist.path()).unwrap_err(); let err_message = format_err(&build_err) .replace(&src.path().user_display().to_string(), "[TEMP_PATH]") .replace('\\', "/"); assert_snapshot!( err_message, @"Expected a Python module at: [TEMP_PATH]/src/stuffed_bird-stubs/__init__.pyi" ); // Create the correct file fs_err::remove_file(regular_init_py).unwrap(); File::create( src.path() .join("src") .join("stuffed_bird-stubs") .join("__init__.pyi"), ) .unwrap(); let build1 = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build1.wheel_contents.join("\n"), @r" stuffed_bird-stubs/ stuffed_bird-stubs/__init__.pyi stuffed_bird_stubs-1.0.0.dist-info/ stuffed_bird_stubs-1.0.0.dist-info/METADATA stuffed_bird_stubs-1.0.0.dist-info/RECORD stuffed_bird_stubs-1.0.0.dist-info/WHEEL "); // Check that setting the name manually works equally. let pyproject_toml = indoc! {r#" [project] name = "stuffed-bird-stubs" version = "1.0.0" [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" [tool.uv.build-backend] module-name = "stuffed_bird-stubs" "# }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); let build2 = build(src.path(), dist.path()).unwrap(); assert_eq!(build1.wheel_contents, build2.wheel_contents); } /// A simple namespace package with a single root `__init__.py`. #[test] fn simple_namespace_package() { let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] name = "simple-namespace-part" version = "1.0.0" [tool.uv.build-backend] module-name = "simple_namespace.part" [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" "# }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); fs_err::create_dir_all(src.path().join("src").join("simple_namespace").join("part")) .unwrap(); assert_snapshot!( build_err(src.path()), @"Expected a Python module at: [TEMP_PATH]/src/simple_namespace/part/__init__.py" ); // Create the correct file File::create( src.path() .join("src") .join("simple_namespace") .join("part") .join("__init__.py"), ) .unwrap(); // For a namespace package, there must not be an `__init__.py` here. let bogus_init_py = src .path() .join("src") .join("simple_namespace") .join("__init__.py"); File::create(&bogus_init_py).unwrap(); assert_snapshot!( build_err(src.path()), @"For namespace packages, `__init__.py[i]` is not allowed in parent directory: [TEMP_PATH]/src/simple_namespace" ); fs_err::remove_file(bogus_init_py).unwrap(); let dist = TempDir::new().unwrap(); let build1 = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build1.source_dist_contents.join("\n"), @r" simple_namespace_part-1.0.0/ simple_namespace_part-1.0.0/PKG-INFO simple_namespace_part-1.0.0/pyproject.toml simple_namespace_part-1.0.0/src simple_namespace_part-1.0.0/src/simple_namespace simple_namespace_part-1.0.0/src/simple_namespace/part simple_namespace_part-1.0.0/src/simple_namespace/part/__init__.py "); assert_snapshot!(build1.wheel_contents.join("\n"), @r" simple_namespace/ simple_namespace/part/ simple_namespace/part/__init__.py simple_namespace_part-1.0.0.dist-info/ simple_namespace_part-1.0.0.dist-info/METADATA simple_namespace_part-1.0.0.dist-info/RECORD simple_namespace_part-1.0.0.dist-info/WHEEL "); // Check that `namespace = true` works too. let pyproject_toml = indoc! {r#" [project] name = "simple-namespace-part" version = "1.0.0" [tool.uv.build-backend] module-name = "simple_namespace.part" namespace = true [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" "# }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); let build2 = build(src.path(), dist.path()).unwrap(); assert_eq!(build1, build2); } /// A complex namespace package with a multiple root `__init__.py`. #[test] fn complex_namespace_package() { let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] name = "complex-namespace" version = "1.0.0" [tool.uv.build-backend] namespace = true [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" "# }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); fs_err::create_dir_all( src.path() .join("src") .join("complex_namespace") .join("part_a"), ) .unwrap(); File::create( src.path() .join("src") .join("complex_namespace") .join("part_a") .join("__init__.py"), ) .unwrap(); fs_err::create_dir_all( src.path() .join("src") .join("complex_namespace") .join("part_b"), ) .unwrap(); File::create( src.path() .join("src") .join("complex_namespace") .join("part_b") .join("__init__.py"), ) .unwrap(); let dist = TempDir::new().unwrap(); let build1 = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build1.wheel_contents.join("\n"), @r" complex_namespace-1.0.0.dist-info/ complex_namespace-1.0.0.dist-info/METADATA complex_namespace-1.0.0.dist-info/RECORD complex_namespace-1.0.0.dist-info/WHEEL complex_namespace/ complex_namespace/part_a/ complex_namespace/part_a/__init__.py complex_namespace/part_b/ complex_namespace/part_b/__init__.py "); // Check that setting the name manually works equally. let pyproject_toml = indoc! {r#" [project] name = "complex-namespace" version = "1.0.0" [tool.uv.build-backend] module-name = "complex_namespace" namespace = true [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" "# }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); let build2 = build(src.path(), dist.path()).unwrap(); assert_eq!(build1, build2); } /// Stubs for a namespace package. #[test] fn stubs_namespace() { let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] name = "cloud.db.schema-stubs" version = "1.0.0" [tool.uv.build-backend] module-name = "cloud-stubs.db.schema" [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" "# }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); fs_err::create_dir_all( src.path() .join("src") .join("cloud-stubs") .join("db") .join("schema"), ) .unwrap(); File::create( src.path() .join("src") .join("cloud-stubs") .join("db") .join("schema") .join("__init__.pyi"), ) .unwrap(); let dist = TempDir::new().unwrap(); let build = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build.wheel_contents.join("\n"), @r" cloud-stubs/ cloud-stubs/db/ cloud-stubs/db/schema/ cloud-stubs/db/schema/__init__.pyi cloud_db_schema_stubs-1.0.0.dist-info/ cloud_db_schema_stubs-1.0.0.dist-info/METADATA cloud_db_schema_stubs-1.0.0.dist-info/RECORD cloud_db_schema_stubs-1.0.0.dist-info/WHEEL "); } /// A package with multiple modules, one a regular module and two namespace modules. #[test] fn multiple_module_names() { let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] name = "simple-namespace-part" version = "1.0.0" [tool.uv.build-backend] module-name = ["foo", "simple_namespace.part_a", "simple_namespace.part_b"] [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" "# }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); fs_err::create_dir_all(src.path().join("src").join("foo")).unwrap(); fs_err::create_dir_all( src.path() .join("src") .join("simple_namespace") .join("part_a"), ) .unwrap(); fs_err::create_dir_all( src.path() .join("src") .join("simple_namespace") .join("part_b"), ) .unwrap(); // Most of these checks exist in other tests too, but we want to ensure that they apply // with multiple modules too. // The first module is missing an `__init__.py`. assert_snapshot!( build_err(src.path()), @"Expected a Python module at: [TEMP_PATH]/src/foo/__init__.py" ); // Create the first correct `__init__.py` file File::create(src.path().join("src").join("foo").join("__init__.py")).unwrap(); // The second module, a namespace, is missing an `__init__.py`. assert_snapshot!( build_err(src.path()), @"Expected a Python module at: [TEMP_PATH]/src/simple_namespace/part_a/__init__.py" ); // Create the other two correct `__init__.py` files File::create( src.path() .join("src") .join("simple_namespace") .join("part_a") .join("__init__.py"), ) .unwrap(); File::create( src.path() .join("src") .join("simple_namespace") .join("part_b") .join("__init__.py"), ) .unwrap(); // For the second module, a namespace, there must not be an `__init__.py` here. let bogus_init_py = src .path() .join("src") .join("simple_namespace") .join("__init__.py"); File::create(&bogus_init_py).unwrap(); assert_snapshot!( build_err(src.path()), @"For namespace packages, `__init__.py[i]` is not allowed in parent directory: [TEMP_PATH]/src/simple_namespace" ); fs_err::remove_file(bogus_init_py).unwrap(); let dist = TempDir::new().unwrap(); let build = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build.source_dist_contents.join("\n"), @r" simple_namespace_part-1.0.0/ simple_namespace_part-1.0.0/PKG-INFO simple_namespace_part-1.0.0/pyproject.toml simple_namespace_part-1.0.0/src simple_namespace_part-1.0.0/src/foo simple_namespace_part-1.0.0/src/foo/__init__.py simple_namespace_part-1.0.0/src/simple_namespace simple_namespace_part-1.0.0/src/simple_namespace/part_a simple_namespace_part-1.0.0/src/simple_namespace/part_a/__init__.py simple_namespace_part-1.0.0/src/simple_namespace/part_b simple_namespace_part-1.0.0/src/simple_namespace/part_b/__init__.py "); assert_snapshot!(build.wheel_contents.join("\n"), @r" foo/ foo/__init__.py simple_namespace/ simple_namespace/part_a/ simple_namespace/part_a/__init__.py simple_namespace/part_b/ simple_namespace/part_b/__init__.py simple_namespace_part-1.0.0.dist-info/ simple_namespace_part-1.0.0.dist-info/METADATA simple_namespace_part-1.0.0.dist-info/RECORD simple_namespace_part-1.0.0.dist-info/WHEEL "); } /// `prune_redundant_modules` should remove modules which are already /// included (either directly or via their parent) #[test] fn test_prune_redundant_modules() { fn check(input: &[&str], expect: &[&str]) { let input = input.iter().map(|s| (*s).to_string()).collect(); let expect: Vec<_> = expect.iter().map(|s| (*s).to_string()).collect(); assert_eq!(prune_redundant_modules(input), expect); } // Basic cases check(&[], &[]); check(&["foo"], &["foo"]); check(&["foo", "bar"], &["bar", "foo"]); // Deshadowing check(&["foo", "foo.bar"], &["foo"]); check(&["foo.bar", "foo"], &["foo"]); check( &["foo.bar.a", "foo.bar.b", "foo.bar", "foo", "foo.bar.a.c"], &["foo"], ); check( &["bar.one", "bar.two", "baz", "bar", "baz.one"], &["bar", "baz"], ); // Potential false positives check(&["foo", "foobar"], &["foo", "foobar"]); check( &["foo", "foobar", "foo.bar", "foobar.baz"], &["foo", "foobar"], ); check(&["foo.bar", "foo.baz"], &["foo.bar", "foo.baz"]); check(&["foo", "foo", "foo.bar", "foo.bar"], &["foo"]); // Everything check( &[ "foo.inner", "foo.inner.deeper", "foo", "bar", "bar.sub", "bar.sub.deep", "foobar", "baz.baz.bar", "baz.baz", "qux", ], &["bar", "baz.baz", "foo", "foobar", "qux"], ); } /// A package with duplicate module names. #[test] fn duplicate_module_names() { let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] name = "duplicate" version = "1.0.0" [tool.uv.build-backend] module-name = ["foo", "foo", "bar.baz", "bar.baz.submodule"] [build-system] requires = ["uv_build>=0.5.15,<0.6.0"] build-backend = "uv_build" "# }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); fs_err::create_dir_all(src.path().join("src").join("foo")).unwrap(); File::create(src.path().join("src").join("foo").join("__init__.py")).unwrap(); fs_err::create_dir_all(src.path().join("src").join("bar").join("baz")).unwrap(); File::create( src.path() .join("src") .join("bar") .join("baz") .join("__init__.py"), ) .unwrap(); let dist = TempDir::new().unwrap(); let build = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build.source_dist_contents.join("\n"), @r" duplicate-1.0.0/ duplicate-1.0.0/PKG-INFO duplicate-1.0.0/pyproject.toml duplicate-1.0.0/src duplicate-1.0.0/src/bar duplicate-1.0.0/src/bar/baz duplicate-1.0.0/src/bar/baz/__init__.py duplicate-1.0.0/src/foo duplicate-1.0.0/src/foo/__init__.py "); assert_snapshot!(build.wheel_contents.join("\n"), @r" bar/ bar/baz/ bar/baz/__init__.py duplicate-1.0.0.dist-info/ duplicate-1.0.0.dist-info/METADATA duplicate-1.0.0.dist-info/RECORD duplicate-1.0.0.dist-info/WHEEL foo/ foo/__init__.py "); } } uv-0.9.17+ds1/crates/uv-build-backend/src/metadata.rs000066400000000000000000001563451520155276700222670ustar00rootroot00000000000000use std::collections::{BTreeMap, Bound}; use std::ffi::OsStr; use std::fmt::Display; use std::fmt::Write; use std::path::{Path, PathBuf}; use std::str::{self, FromStr}; use itertools::Itertools; use serde::{Deserialize, Deserializer}; use tracing::{debug, trace, warn}; use version_ranges::Ranges; use walkdir::WalkDir; use uv_fs::Simplified; use uv_globfilter::{GlobDirFilter, PortableGlobParser}; use uv_normalize::{ExtraName, PackageName}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::{ ExtraOperator, MarkerExpression, MarkerTree, MarkerValueExtra, Requirement, VersionOrUrl, }; use uv_pypi_types::{Metadata23, VerbatimParsedUrl}; use crate::serde_verbatim::SerdeVerbatim; use crate::{BuildBackendSettings, Error, error_on_venv}; /// By default, we ignore generated python files. pub(crate) const DEFAULT_EXCLUDES: &[&str] = &["__pycache__", "*.pyc", "*.pyo"]; #[derive(Debug, Error)] pub enum ValidationError { /// The spec isn't clear about what the values in that field would be, and we only support the /// default value (UTF-8). #[error( "Charsets other than UTF-8 are not supported. Please convert your README to UTF-8 and remove `project.readme.charset`." )] ReadmeCharset, #[error( "Unknown Readme extension `{0}`, can't determine content type. Please use a support extension (`.md`, `.rst`, `.txt`) or set the content type manually." )] UnknownExtension(String), #[error("Can't infer content type because `{}` does not have an extension. Please use a support extension (`.md`, `.rst`, `.txt`) or set the content type manually.", _0.user_display())] MissingExtension(PathBuf), #[error("Unsupported content type: {0}")] UnsupportedContentType(String), #[error("`project.description` must be a single line")] DescriptionNewlines, #[error("Dynamic metadata is not supported")] Dynamic, #[error( "When `project.license-files` is defined, `project.license` must be an SPDX expression string" )] MixedLicenseGenerations, #[error( "Entrypoint groups must consist of letters and numbers separated by dots, invalid group: {0}" )] InvalidGroup(String), #[error("Use `project.scripts` instead of `project.entry-points.console_scripts`")] ReservedScripts, #[error("Use `project.gui-scripts` instead of `project.entry-points.gui_scripts`")] ReservedGuiScripts, #[error("`project.license` is not a valid SPDX expression: {0}")] InvalidSpdx(String, #[source] spdx::error::ParseError), #[error("`{field}` glob `{glob}` did not match any files")] LicenseGlobNoMatches { field: String, glob: String }, #[error("License file `{}` must be UTF-8 encoded", _0)] LicenseFileNotUtf8(String), } /// Check if the build backend is matching the currently running uv version. pub fn check_direct_build(source_tree: &Path, name: impl Display) -> bool { #[derive(Deserialize)] #[serde(rename_all = "kebab-case")] struct PyProjectToml { build_system: BuildSystem, } let pyproject_toml: PyProjectToml = match fs_err::read_to_string(source_tree.join("pyproject.toml")) .map_err(|err| err.to_string()) .and_then(|pyproject_toml| { toml::from_str(&pyproject_toml).map_err(|err| err.to_string()) }) { Ok(pyproject_toml) => pyproject_toml, Err(err) => { debug!( "Not using uv build backend direct build for source tree `{name}`, \ failed to parse pyproject.toml: {err}" ); return false; } }; match pyproject_toml .build_system .check_build_system(uv_version::version()) .as_slice() { // No warnings -> match [] => true, // Any warning -> no match [first, others @ ..] => { debug!( "Not using uv build backend direct build of `{name}`, pyproject.toml does not match: {first}" ); for other in others { trace!("Further uv build backend direct build of `{name}` mismatch: {other}"); } false } } } /// A package name as provided in a `pyproject.toml`. #[derive(Debug, Clone)] struct VerbatimPackageName { /// The package name as given in the `pyproject.toml`. given: String, /// The normalized package name. normalized: PackageName, } impl<'de> Deserialize<'de> for VerbatimPackageName { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let given = String::deserialize(deserializer)?; let normalized = PackageName::from_str(&given).map_err(serde::de::Error::custom)?; Ok(Self { given, normalized }) } } /// A `pyproject.toml` as specified in PEP 517. #[derive(Deserialize, Debug, Clone)] #[serde( rename_all = "kebab-case", expecting = "The project table needs to follow \ https://packaging.python.org/en/latest/guides/writing-pyproject-toml" )] pub struct PyProjectToml { /// Project metadata project: Project, /// uv-specific configuration tool: Option, /// Build-related data build_system: BuildSystem, } impl PyProjectToml { pub(crate) fn name(&self) -> &PackageName { &self.project.name.normalized } pub(crate) fn version(&self) -> &Version { &self.project.version } pub(crate) fn parse(path: &Path) -> Result { let contents = fs_err::read_to_string(path)?; let pyproject_toml = toml::from_str(&contents).map_err(|err| Error::Toml(path.to_path_buf(), err))?; Ok(pyproject_toml) } pub(crate) fn readme(&self) -> Option<&Readme> { self.project.readme.as_ref() } /// The license files that need to be included in the source distribution. pub(crate) fn license_files_source_dist(&self) -> impl Iterator { let license_file = self .project .license .as_ref() .and_then(|license| license.file()) .into_iter(); let license_files = self .project .license_files .iter() .flatten() .map(String::as_str); license_files.chain(license_file) } /// The license files that need to be included in the wheel. pub(crate) fn license_files_wheel(&self) -> impl Iterator { // The pre-PEP 639 `license = { file = "..." }` is included inline in `METADATA`. self.project .license_files .iter() .flatten() .map(String::as_str) } pub(crate) fn settings(&self) -> Option<&BuildBackendSettings> { self.tool.as_ref()?.uv.as_ref()?.build_backend.as_ref() } /// See [`BuildSystem::check_build_system`]. pub fn check_build_system(&self, uv_version: &str) -> Vec { self.build_system.check_build_system(uv_version) } /// Validate and convert a `pyproject.toml` to core metadata. /// /// /// /// pub(crate) fn to_metadata(&self, root: &Path) -> Result { let summary = if let Some(description) = &self.project.description { if description.contains('\n') { return Err(ValidationError::DescriptionNewlines.into()); } Some(description.clone()) } else { None }; let supported_content_types = ["text/plain", "text/x-rst", "text/markdown"]; let (description, description_content_type) = match &self.project.readme { Some(Readme::String(path)) => { let content = fs_err::read_to_string(root.join(path))?; let content_type = match path.extension().and_then(OsStr::to_str) { Some("txt") => "text/plain", Some("rst") => "text/x-rst", Some("md") => "text/markdown", Some(unknown) => { return Err(ValidationError::UnknownExtension(unknown.to_owned()).into()); } None => return Err(ValidationError::MissingExtension(path.clone()).into()), } .to_string(); (Some(content), Some(content_type)) } Some(Readme::File { file, content_type, charset, }) => { let content = fs_err::read_to_string(root.join(file))?; if !supported_content_types.contains(&content_type.as_str()) { return Err( ValidationError::UnsupportedContentType(content_type.clone()).into(), ); } if charset.as_ref().is_some_and(|charset| charset != "UTF-8") { return Err(ValidationError::ReadmeCharset.into()); } (Some(content), Some(content_type.clone())) } Some(Readme::Text { text, content_type, charset, }) => { if !supported_content_types.contains(&content_type.as_str()) { return Err( ValidationError::UnsupportedContentType(content_type.clone()).into(), ); } if charset.as_ref().is_some_and(|charset| charset != "UTF-8") { return Err(ValidationError::ReadmeCharset.into()); } (Some(text.clone()), Some(content_type.clone())) } None => (None, None), }; if self .project .dynamic .as_ref() .is_some_and(|dynamic| !dynamic.is_empty()) { return Err(ValidationError::Dynamic.into()); } let author = self .project .authors .as_ref() .map(|authors| { authors .iter() .filter_map(|author| match author { Contact::Name { name } => Some(name), Contact::Email { .. } => None, Contact::NameEmail { name, .. } => Some(name), }) .join(", ") }) .filter(|author| !author.is_empty()); let author_email = self .project .authors .as_ref() .map(|authors| { authors .iter() .filter_map(|author| match author { Contact::Name { .. } => None, Contact::Email { email } => Some(email.clone()), Contact::NameEmail { name, email } => Some(format!("{name} <{email}>")), }) .join(", ") }) .filter(|author_email| !author_email.is_empty()); let maintainer = self .project .maintainers .as_ref() .map(|maintainers| { maintainers .iter() .filter_map(|maintainer| match maintainer { Contact::Name { name } => Some(name), Contact::Email { .. } => None, Contact::NameEmail { name, .. } => Some(name), }) .join(", ") }) .filter(|maintainer| !maintainer.is_empty()); let maintainer_email = self .project .maintainers .as_ref() .map(|maintainers| { maintainers .iter() .filter_map(|maintainer| match maintainer { Contact::Name { .. } => None, Contact::Email { email } => Some(email.clone()), Contact::NameEmail { name, email } => Some(format!("{name} <{email}>")), }) .join(", ") }) .filter(|maintainer_email| !maintainer_email.is_empty()); // Using PEP 639 bumps the METADATA version let metadata_version = if self.project.license_files.is_some() || matches!(self.project.license, Some(License::Spdx(_))) { debug!("Found PEP 639 license declarations, using METADATA 2.4"); "2.4" } else { "2.3" }; let (license, license_expression, license_files) = self.license_metadata(root)?; // TODO(konsti): https://peps.python.org/pep-0753/#label-normalization (Draft) let project_urls = self .project .urls .iter() .flatten() .map(|(key, value)| format!("{key}, {value}")) .collect(); let extras = self .project .optional_dependencies .iter() .flat_map(|optional_dependencies| optional_dependencies.keys()) .collect::>(); let requires_dist = self.project .dependencies .iter() .flatten() .cloned() .chain(self.project.optional_dependencies.iter().flat_map( |optional_dependencies| { optional_dependencies .iter() .flat_map(|(extra, requirements)| { requirements.iter().cloned().map(|mut requirement| { requirement.marker.and(MarkerTree::expression( MarkerExpression::Extra { operator: ExtraOperator::Equal, name: MarkerValueExtra::Extra(extra.clone()), }, )); requirement }) }) }, )) .collect::>(); Ok(Metadata23 { metadata_version: metadata_version.to_string(), name: self.project.name.given.clone(), version: self.project.version.to_string(), // Not supported. platforms: vec![], // Not supported. supported_platforms: vec![], summary, description, description_content_type, keywords: self .project .keywords .as_ref() .map(|keywords| keywords.join(",")), home_page: None, download_url: None, author, author_email, maintainer, maintainer_email, license, license_expression, license_files, classifiers: self.project.classifiers.clone().unwrap_or_default(), requires_dist: requires_dist.iter().map(ToString::to_string).collect(), provides_extra: extras.iter().map(ToString::to_string).collect(), // Not commonly set. provides_dist: vec![], // Not supported. obsoletes_dist: vec![], requires_python: self .project .requires_python .as_ref() .map(ToString::to_string), // Not used by other tools, not supported. requires_external: vec![], project_urls, dynamic: vec![], }) } /// Parse and validate the old (PEP 621) and new (PEP 639) license files. #[allow(clippy::type_complexity)] fn license_metadata( &self, root: &Path, ) -> Result<(Option, Option, Vec), Error> { // TODO(konsti): Issue a warning on old license metadata once PEP 639 is universal. let (license, license_expression, license_files) = if let Some(license_globs) = &self.project.license_files { let license_expression = match &self.project.license { None => None, Some(License::Spdx(license_expression)) => Some(license_expression.clone()), Some(License::Text { .. } | License::File { .. }) => { return Err(ValidationError::MixedLicenseGenerations.into()); } }; let mut license_files = Vec::new(); let mut license_globs_parsed = Vec::with_capacity(license_globs.len()); let mut license_glob_matchers = Vec::with_capacity(license_globs.len()); for license_glob in license_globs { let pep639_glob = PortableGlobParser::Pep639 .parse(license_glob) .map_err(|err| Error::PortableGlob { field: license_glob.to_owned(), source: err, })?; license_glob_matchers.push(pep639_glob.compile_matcher()); license_globs_parsed.push(pep639_glob); } // Track whether each user-specified glob matched so we can flag the unmatched ones. let mut license_globs_matched = vec![false; license_globs_parsed.len()]; let license_globs = GlobDirFilter::from_globs(&license_globs_parsed).map_err(|err| { Error::GlobSetTooLarge { field: "project.license-files".to_string(), source: err, } })?; for entry in WalkDir::new(root) .sort_by_file_name() .into_iter() .filter_entry(|entry| { license_globs.match_directory( entry .path() .strip_prefix(root) .expect("walkdir starts with root"), ) }) { let entry = entry.map_err(|err| Error::WalkDir { root: root.to_path_buf(), err, })?; let relative = entry .path() .strip_prefix(root) .expect("walkdir starts with root"); if !license_globs.match_path(relative) { trace!("Not a license files match: {}", relative.user_display()); continue; } let file_type = entry.file_type(); if !(file_type.is_file() || file_type.is_symlink()) { trace!( "Not a file or symlink in license files match: {}", relative.user_display() ); continue; } error_on_venv(entry.file_name(), entry.path())?; debug!("License files match: {}", relative.user_display()); for (matched, matcher) in license_globs_matched .iter_mut() .zip(license_glob_matchers.iter()) { if *matched { continue; } if matcher.is_match(relative) { *matched = true; } } license_files.push(relative.portable_display().to_string()); } if let Some((pattern, _)) = license_globs_parsed .into_iter() .zip(license_globs_matched) .find(|(_, matched)| !matched) { return Err(ValidationError::LicenseGlobNoMatches { field: "project.license-files".to_string(), glob: pattern.to_string(), } .into()); } for license_file in &license_files { let file_path = root.join(license_file); let bytes = fs_err::read(&file_path)?; if str::from_utf8(&bytes).is_err() { return Err(ValidationError::LicenseFileNotUtf8(license_file.clone()).into()); } } // The glob order may be unstable license_files.sort(); (None, license_expression, license_files) } else { match &self.project.license { None => (None, None, Vec::new()), Some(License::Spdx(license_expression)) => { (None, Some(license_expression.clone()), Vec::new()) } Some(License::Text { text }) => (Some(text.clone()), None, Vec::new()), Some(License::File { file }) => { let text = fs_err::read_to_string(root.join(file))?; (Some(text), None, Vec::new()) } } }; // Check that the license expression is a valid SPDX identifier. if let Some(license_expression) = &license_expression { if let Err(err) = spdx::Expression::parse(license_expression) { return Err(ValidationError::InvalidSpdx(license_expression.clone(), err).into()); } } Ok((license, license_expression, license_files)) } /// Validate and convert the entrypoints in `pyproject.toml`, including console and GUI scripts, /// to an `entry_points.txt`. /// /// /// /// Returns `None` if no entrypoints were defined. pub(crate) fn to_entry_points(&self) -> Result, ValidationError> { let mut writer = String::new(); if self.project.scripts.is_none() && self.project.gui_scripts.is_none() && self.project.entry_points.is_none() { return Ok(None); } if let Some(scripts) = &self.project.scripts { Self::write_group(&mut writer, "console_scripts", scripts)?; } if let Some(gui_scripts) = &self.project.gui_scripts { Self::write_group(&mut writer, "gui_scripts", gui_scripts)?; } for (group, entries) in self.project.entry_points.iter().flatten() { if group == "console_scripts" { return Err(ValidationError::ReservedScripts); } if group == "gui_scripts" { return Err(ValidationError::ReservedGuiScripts); } Self::write_group(&mut writer, group, entries)?; } Ok(Some(writer)) } /// Write a group to `entry_points.txt`. fn write_group<'a>( writer: &mut String, group: &str, entries: impl IntoIterator, ) -> Result<(), ValidationError> { if !group .chars() .next() .map(|c| c.is_alphanumeric() || c == '_') .unwrap_or(false) || !group .chars() .all(|c| c.is_alphanumeric() || c == '.' || c == '_') { return Err(ValidationError::InvalidGroup(group.to_string())); } let _ = writeln!(writer, "[{group}]"); for (name, object_reference) in entries { if !name .chars() .all(|c| c.is_alphanumeric() || c == '.' || c == '-' || c == '_') { warn!( "Entrypoint names should consist of letters, numbers, dots, underscores and \ dashes; non-compliant name: {name}" ); } // TODO(konsti): Validate that the object references are valid Python identifiers. let _ = writeln!(writer, "{name} = {object_reference}"); } writer.push('\n'); Ok(()) } } /// The `[project]` section of a pyproject.toml as specified in /// . /// /// This struct does not have schema export; the schema is shared between all Python tools, and we /// should update the shared schema instead. #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] struct Project { /// The name of the project. name: VerbatimPackageName, /// The version of the project. version: Version, /// The summary description of the project in one line. description: Option, /// The full description of the project (i.e. the README). readme: Option, /// The Python version requirements of the project. requires_python: Option, /// The license under which the project is distributed. /// /// Supports both the current standard and the provisional PEP 639. license: Option, /// The paths to files containing licenses and other legal notices to be distributed with the /// project. /// /// From the provisional PEP 639 license_files: Option>, /// The people or organizations considered to be the "authors" of the project. authors: Option>, /// The people or organizations considered to be the "maintainers" of the project. maintainers: Option>, /// The keywords for the project. keywords: Option>, /// Trove classifiers which apply to the project. classifiers: Option>, /// A table of URLs where the key is the URL label and the value is the URL itself. /// /// PyPI shows all URLs with their name. For some known patterns, they add favicons. /// main: /// archived: urls: Option>, /// The console entrypoints of the project. /// /// The key of the table is the name of the entry point and the value is the object reference. scripts: Option>, /// The GUI entrypoints of the project. /// /// The key of the table is the name of the entry point and the value is the object reference. gui_scripts: Option>, /// Entrypoints groups of the project. /// /// The key of the table is the name of the entry point and the value is the object reference. entry_points: Option>>, /// The dependencies of the project. dependencies: Option>, /// The optional dependencies of the project. optional_dependencies: Option>>, /// Specifies which fields listed by PEP 621 were intentionally unspecified so another tool /// can/will provide such metadata dynamically. /// /// Not supported, an error if anything but the default empty list. dynamic: Option>, } /// The optional `project.readme` key in a pyproject.toml as specified in /// . #[derive(Deserialize, Debug, Clone)] #[serde(untagged, rename_all_fields = "kebab-case")] pub(crate) enum Readme { /// Relative path to the README. String(PathBuf), /// Relative path to the README. File { file: PathBuf, content_type: String, charset: Option, }, /// The full description of the project as an inline value. Text { text: String, content_type: String, charset: Option, }, } impl Readme { /// If the readme is a file, return the path to the file. pub(crate) fn path(&self) -> Option<&Path> { match self { Self::String(path) => Some(path), Self::File { file, .. } => Some(file), Self::Text { .. } => None, } } } /// The optional `project.license` key in a pyproject.toml as specified in /// . #[derive(Deserialize, Debug, Clone)] #[serde(untagged)] pub(crate) enum License { /// An SPDX Expression. /// /// From the provisional PEP 639. Spdx(String), Text { /// The full text of the license. text: String, }, File { /// The file containing the license text. file: String, }, } impl License { fn file(&self) -> Option<&str> { if let Self::File { file } = self { Some(file) } else { None } } } /// A `project.authors` or `project.maintainers` entry as specified in /// . /// /// The entry is derived from the email format of `John Doe `. You need to /// provide at least name or email. #[derive(Deserialize, Debug, Clone)] // deny_unknown_fields prevents using the name field when the email is not a string. #[serde( untagged, deny_unknown_fields, expecting = "a table with 'name' and/or 'email' keys" )] pub(crate) enum Contact { /// TODO(konsti): RFC 822 validation. NameEmail { name: String, email: String }, /// TODO(konsti): RFC 822 validation. Name { name: String }, /// TODO(konsti): RFC 822 validation. Email { email: String }, } /// The `tool` section as specified in PEP 517. #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub(crate) struct Tool { /// uv-specific configuration uv: Option, } /// The `tool.uv` section with build configuration. #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub(crate) struct ToolUv { /// Configuration for building source distributions and wheels with the uv build backend build_backend: Option, } /// The `[build-system]` section of a pyproject.toml as specified in PEP 517. #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] struct BuildSystem { /// PEP 508 dependencies required to execute the build system. requires: Vec>>, /// A string naming a Python object that will be used to perform the build. build_backend: Option, /// backend_path: Option>, } impl BuildSystem { /// Check if the `[build-system]` table matches the uv build backend expectations and return /// a list of warnings if it looks suspicious. /// /// Example of a valid table: /// /// ```toml /// [build-system] /// requires = ["uv_build>=0.4.15,<0.5.0"] /// build-backend = "uv_build" /// ``` pub(crate) fn check_build_system(&self, uv_version: &str) -> Vec { let mut warnings = Vec::new(); if self.build_backend.as_deref() != Some("uv_build") { warnings.push(format!( r#"The value for `build_system.build-backend` should be `"uv_build"`, not `"{}"`"#, self.build_backend.clone().unwrap_or_default() )); } let uv_version = Version::from_str(uv_version).expect("uv's own version is not PEP 440 compliant"); let next_minor = uv_version.release().get(1).copied().unwrap_or_default() + 1; let next_breaking = Version::new([0, next_minor]); let expected = || { format!( "Expected a single uv requirement in `build-system.requires`, found `{}`", toml::to_string(&self.requires).unwrap_or_default() ) }; let [uv_requirement] = &self.requires.as_slice() else { warnings.push(expected()); return warnings; }; if uv_requirement.name.as_str() != "uv-build" { warnings.push(expected()); return warnings; } let bounded = match &uv_requirement.version_or_url { None => false, Some(VersionOrUrl::Url(_)) => { // We can't validate the url true } Some(VersionOrUrl::VersionSpecifier(specifier)) => { // We don't check how wide the range is (that's up to the user), we just // check that the current version is compliant, to avoid accidentally using a // too new or too old uv, and we check that an upper bound exists. The latter // is very important to allow making breaking changes in uv without breaking // the existing immutable source distributions on pypi. if !specifier.contains(&uv_version) { // This is allowed to happen when testing prereleases, but we should still warn. warnings.push(format!( r#"`build_system.requires = ["{uv_requirement}"]` does not contain the current uv version {uv_version}"#, )); } Ranges::from(specifier.clone()) .bounding_range() .map(|bounding_range| bounding_range.1 != Bound::Unbounded) .unwrap_or(false) } }; if !bounded { warnings.push(format!( "`build_system.requires = [\"{}\"]` is missing an \ upper bound on the `uv_build` version such as `<{next_breaking}`. \ Without bounding the `uv_build` version, the source distribution will break \ when a future, breaking version of `uv_build` is released.", // Use an underscore consistently, to avoid confusing users between a package name with dash and a // module name with underscore uv_requirement.verbatim() )); } warnings } } #[cfg(test)] mod tests { use super::*; use indoc::{formatdoc, indoc}; use insta::assert_snapshot; use std::iter; use tempfile::TempDir; fn extend_project(payload: &str) -> String { formatdoc! {r#" [project] name = "hello-world" version = "0.1.0" {payload} [build-system] requires = ["uv_build>=0.4.15,<0.5.0"] build-backend = "uv_build" "# } } fn format_err(err: impl std::error::Error) -> String { let mut formatted = err.to_string(); for source in iter::successors(err.source(), |&err| err.source()) { let _ = write!(formatted, "\n Caused by: {source}"); } formatted } #[test] fn uppercase_package_name() { let contents = r#" [project] name = "Hello-World" version = "0.1.0" [build-system] requires = ["uv_build>=0.4.15,<0.5.0"] build-backend = "uv_build" "#; let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let temp_dir = TempDir::new().unwrap(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); assert_snapshot!(metadata.core_metadata_format(), @r" Metadata-Version: 2.3 Name: Hello-World Version: 0.1.0 "); } #[test] fn valid() { let temp_dir = TempDir::new().unwrap(); fs_err::write( temp_dir.path().join("Readme.md"), indoc! {r" # Foo This is the foo library. "}, ) .unwrap(); fs_err::write( temp_dir.path().join("License.txt"), indoc! {r#" 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. "#}, ) .unwrap(); let contents = indoc! {r#" # See https://github.com/pypa/sampleproject/blob/main/pyproject.toml for another example [project] name = "hello-world" version = "0.1.0" description = "A Python package" readme = "Readme.md" requires_python = ">=3.12" license = { file = "License.txt" } authors = [{ name = "Ferris the crab", email = "ferris@rustacean.net" }] maintainers = [{ name = "Konsti", email = "konstin@mailbox.org" }] keywords = ["demo", "example", "package"] classifiers = [ "Development Status :: 6 - Mature", "License :: OSI Approved :: MIT License", # https://github.com/pypa/trove-classifiers/issues/17 "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", ] dependencies = ["flask>=3,<4", "sqlalchemy[asyncio]>=2.0.35,<3"] # We don't support dynamic fields, the default empty array is the only allowed value. dynamic = [] [project.optional-dependencies] postgres = ["psycopg>=3.2.2,<4"] mysql = ["pymysql>=1.1.1,<2"] [project.urls] "Homepage" = "https://github.com/astral-sh/uv" "Repository" = "https://astral.sh" [project.scripts] foo = "foo.cli:__main__" [project.gui-scripts] foo-gui = "foo.gui" [project.entry-points.bar_group] foo-bar = "foo:bar" [build-system] requires = ["uv_build>=0.4.15,<0.5.0"] build-backend = "uv_build" "# }; let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); assert_snapshot!(metadata.core_metadata_format(), @r###" Metadata-Version: 2.3 Name: hello-world Version: 0.1.0 Summary: A Python package Keywords: demo,example,package Author: Ferris the crab Author-email: Ferris the crab License: 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. Classifier: Development Status :: 6 - Mature Classifier: License :: OSI Approved :: MIT License Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Requires-Dist: flask>=3,<4 Requires-Dist: sqlalchemy[asyncio]>=2.0.35,<3 Requires-Dist: pymysql>=1.1.1,<2 ; extra == 'mysql' Requires-Dist: psycopg>=3.2.2,<4 ; extra == 'postgres' Maintainer: Konsti Maintainer-email: Konsti Project-URL: Homepage, https://github.com/astral-sh/uv Project-URL: Repository, https://astral.sh Provides-Extra: mysql Provides-Extra: postgres Description-Content-Type: text/markdown # Foo This is the foo library. "###); assert_snapshot!(pyproject_toml.to_entry_points().unwrap().unwrap(), @r###" [console_scripts] foo = foo.cli:__main__ [gui_scripts] foo-gui = foo.gui [bar_group] foo-bar = foo:bar "###); } #[test] fn readme() { let temp_dir = TempDir::new().unwrap(); fs_err::write( temp_dir.path().join("Readme.md"), indoc! {r" # Foo This is the foo library. "}, ) .unwrap(); fs_err::write( temp_dir.path().join("License.txt"), indoc! {r#" 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. "#}, ) .unwrap(); let contents = indoc! {r#" # See https://github.com/pypa/sampleproject/blob/main/pyproject.toml for another example [project] name = "hello-world" version = "0.1.0" description = "A Python package" readme = { file = "Readme.md", content-type = "text/markdown" } requires_python = ">=3.12" [build-system] requires = ["uv_build>=0.4.15,<0.5"] build-backend = "uv_build" "# }; let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); assert_snapshot!(metadata.core_metadata_format(), @r" Metadata-Version: 2.3 Name: hello-world Version: 0.1.0 Summary: A Python package Description-Content-Type: text/markdown # Foo This is the foo library. "); } #[test] fn self_extras() { let temp_dir = TempDir::new().unwrap(); fs_err::write( temp_dir.path().join("Readme.md"), indoc! {r" # Foo This is the foo library. "}, ) .unwrap(); fs_err::write( temp_dir.path().join("License.txt"), indoc! {r#" 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. "#}, ) .unwrap(); let contents = indoc! {r#" # See https://github.com/pypa/sampleproject/blob/main/pyproject.toml for another example [project] name = "hello-world" version = "0.1.0" description = "A Python package" readme = "Readme.md" requires_python = ">=3.12" license = { file = "License.txt" } authors = [{ name = "Ferris the crab", email = "ferris@rustacean.net" }] maintainers = [{ name = "Konsti", email = "konstin@mailbox.org" }] keywords = ["demo", "example", "package"] classifiers = [ "Development Status :: 6 - Mature", "License :: OSI Approved :: MIT License", # https://github.com/pypa/trove-classifiers/issues/17 "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", ] dependencies = ["flask>=3,<4", "sqlalchemy[asyncio]>=2.0.35,<3"] # We don't support dynamic fields, the default empty array is the only allowed value. dynamic = [] [project.optional-dependencies] postgres = ["psycopg>=3.2.2,<4 ; sys_platform == 'linux'"] mysql = ["pymysql>=1.1.1,<2"] databases = ["hello-world[mysql]", "hello-world[postgres]"] all = ["hello-world[databases]", "hello-world[postgres]", "hello-world[mysql]"] [project.urls] "Homepage" = "https://github.com/astral-sh/uv" "Repository" = "https://astral.sh" [project.scripts] foo = "foo.cli:__main__" [project.gui-scripts] foo-gui = "foo.gui" [project.entry-points.bar_group] foo-bar = "foo:bar" [build-system] requires = ["uv_build>=0.4.15,<0.5.0"] build-backend = "uv_build" "# }; let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); assert_snapshot!(metadata.core_metadata_format(), @r###" Metadata-Version: 2.3 Name: hello-world Version: 0.1.0 Summary: A Python package Keywords: demo,example,package Author: Ferris the crab Author-email: Ferris the crab License: 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. Classifier: Development Status :: 6 - Mature Classifier: License :: OSI Approved :: MIT License Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Requires-Dist: flask>=3,<4 Requires-Dist: sqlalchemy[asyncio]>=2.0.35,<3 Requires-Dist: hello-world[databases] ; extra == 'all' Requires-Dist: hello-world[postgres] ; extra == 'all' Requires-Dist: hello-world[mysql] ; extra == 'all' Requires-Dist: hello-world[mysql] ; extra == 'databases' Requires-Dist: hello-world[postgres] ; extra == 'databases' Requires-Dist: pymysql>=1.1.1,<2 ; extra == 'mysql' Requires-Dist: psycopg>=3.2.2,<4 ; sys_platform == 'linux' and extra == 'postgres' Maintainer: Konsti Maintainer-email: Konsti Project-URL: Homepage, https://github.com/astral-sh/uv Project-URL: Repository, https://astral.sh Provides-Extra: all Provides-Extra: databases Provides-Extra: mysql Provides-Extra: postgres Description-Content-Type: text/markdown # Foo This is the foo library. "###); assert_snapshot!(pyproject_toml.to_entry_points().unwrap().unwrap(), @r###" [console_scripts] foo = foo.cli:__main__ [gui_scripts] foo-gui = foo.gui [bar_group] foo-bar = foo:bar "###); } #[test] fn build_system_valid() { let contents = extend_project(""); let pyproject_toml: PyProjectToml = toml::from_str(&contents).unwrap(); assert_snapshot!( pyproject_toml.check_build_system("0.4.15+test").join("\n"), @"" ); } #[test] fn build_system_no_bound() { let contents = indoc! {r#" [project] name = "hello-world" version = "0.1.0" [build-system] requires = ["uv_build"] build-backend = "uv_build" "#}; let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); assert_snapshot!( pyproject_toml.check_build_system("0.4.15+test").join("\n"), @r###"`build_system.requires = ["uv_build"]` is missing an upper bound on the `uv_build` version such as `<0.5`. Without bounding the `uv_build` version, the source distribution will break when a future, breaking version of `uv_build` is released."### ); } #[test] fn build_system_multiple_packages() { let contents = indoc! {r#" [project] name = "hello-world" version = "0.1.0" [build-system] requires = ["uv_build>=0.4.15,<0.5.0", "wheel"] build-backend = "uv_build" "#}; let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); assert_snapshot!( pyproject_toml.check_build_system("0.4.15+test").join("\n"), @"Expected a single uv requirement in `build-system.requires`, found ``" ); } #[test] fn build_system_no_requires_uv() { let contents = indoc! {r#" [project] name = "hello-world" version = "0.1.0" [build-system] requires = ["setuptools"] build-backend = "uv_build" "#}; let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); assert_snapshot!( pyproject_toml.check_build_system("0.4.15+test").join("\n"), @"Expected a single uv requirement in `build-system.requires`, found ``" ); } #[test] fn build_system_not_uv() { let contents = indoc! {r#" [project] name = "hello-world" version = "0.1.0" [build-system] requires = ["uv_build>=0.4.15,<0.5.0"] build-backend = "setuptools" "#}; let pyproject_toml: PyProjectToml = toml::from_str(contents).unwrap(); assert_snapshot!( pyproject_toml.check_build_system("0.4.15+test").join("\n"), @r###"The value for `build_system.build-backend` should be `"uv_build"`, not `"setuptools"`"### ); } #[test] fn minimal() { let contents = extend_project(""); let metadata = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap(); assert_snapshot!(metadata.core_metadata_format(), @r###" Metadata-Version: 2.3 Name: hello-world Version: 0.1.0 "###); } #[test] fn invalid_readme_spec() { let contents = extend_project(indoc! {r#" readme = { path = "Readme.md" } "# }); let err = toml::from_str::(&contents).unwrap_err(); assert_snapshot!(format_err(err), @r#" TOML parse error at line 4, column 10 | 4 | readme = { path = "Readme.md" } | ^^^^^^^^^^^^^^^^^^^^^^ data did not match any variant of untagged enum Readme "#); } #[test] fn missing_readme() { let contents = extend_project(indoc! {r#" readme = "Readme.md" "# }); let err = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap_err(); // Strip away OS specific part. let err = err .to_string() .replace('\\', "/") .split_once(':') .unwrap() .0 .to_string(); assert_snapshot!(err, @"failed to open file `/do/not/read/Readme.md`"); } #[test] fn multiline_description() { let contents = extend_project(indoc! {r#" description = "Hi :)\nThis is my project" "# }); let err = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap_err(); assert_snapshot!(format_err(err), @r" Invalid project metadata Caused by: `project.description` must be a single line "); } #[test] fn mixed_licenses() { let contents = extend_project(indoc! {r#" license-files = ["licenses/*"] license = { text = "MIT" } "# }); let err = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap_err(); assert_snapshot!(format_err(err), @r" Invalid project metadata Caused by: When `project.license-files` is defined, `project.license` must be an SPDX expression string "); } #[test] fn valid_license() { let contents = extend_project(indoc! {r#" license = "MIT OR Apache-2.0" "# }); let metadata = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap(); assert_snapshot!(metadata.core_metadata_format(), @r###" Metadata-Version: 2.4 Name: hello-world Version: 0.1.0 License-Expression: MIT OR Apache-2.0 "###); } #[test] fn invalid_license() { let contents = extend_project(indoc! {r#" license = "MIT XOR Apache-2" "# }); let err = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap_err(); // TODO(konsti): We mess up the indentation in the error. assert_snapshot!(format_err(err), @r" Invalid project metadata Caused by: `project.license` is not a valid SPDX expression: MIT XOR Apache-2 Caused by: MIT XOR Apache-2 ^^^ unknown term "); } #[test] fn dynamic() { let contents = extend_project(indoc! {r#" dynamic = ["dependencies"] "# }); let err = toml::from_str::(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) .unwrap_err(); assert_snapshot!(format_err(err), @r" Invalid project metadata Caused by: Dynamic metadata is not supported "); } fn script_error(contents: &str) -> String { let err = toml::from_str::(contents) .unwrap() .to_entry_points() .unwrap_err(); format_err(err) } #[test] fn invalid_entry_point_group() { let contents = extend_project(indoc! {r#" [project.entry-points."a@b"] foo = "bar" "# }); assert_snapshot!(script_error(&contents), @"Entrypoint groups must consist of letters and numbers separated by dots, invalid group: a@b"); } #[test] fn invalid_entry_point_conflict_scripts() { let contents = extend_project(indoc! {r#" [project.entry-points.console_scripts] foo = "bar" "# }); assert_snapshot!(script_error(&contents), @"Use `project.scripts` instead of `project.entry-points.console_scripts`"); } #[test] fn invalid_entry_point_conflict_gui_scripts() { let contents = extend_project(indoc! {r#" [project.entry-points.gui_scripts] foo = "bar" "# }); assert_snapshot!(script_error(&contents), @"Use `project.gui-scripts` instead of `project.entry-points.gui_scripts`"); } } uv-0.9.17+ds1/crates/uv-build-backend/src/serde_verbatim.rs000066400000000000000000000024711520155276700234700ustar00rootroot00000000000000use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt::{Display, Formatter}; use std::ops::Deref; use std::str::FromStr; /// Preserves the verbatim string representation when deserializing `T`. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) struct SerdeVerbatim { verbatim: String, inner: T, } impl SerdeVerbatim { pub(crate) fn verbatim(&self) -> &str { &self.verbatim } } impl Deref for SerdeVerbatim { type Target = T; fn deref(&self) -> &Self::Target { &self.inner } } impl Display for SerdeVerbatim { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.inner.fmt(f) } } impl<'de, T: FromStr> Deserialize<'de> for SerdeVerbatim where ::Err: Display, { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let verbatim = String::deserialize(deserializer)?; let inner = T::from_str(&verbatim).map_err(serde::de::Error::custom)?; Ok(Self { verbatim, inner }) } } impl Serialize for SerdeVerbatim { fn serialize(&self, serializer: S) -> Result where S: Serializer, { self.inner.serialize(serializer) } } uv-0.9.17+ds1/crates/uv-build-backend/src/settings.rs000066400000000000000000000214731520155276700223400ustar00rootroot00000000000000use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use uv_macros::OptionsMetadata; /// Settings for the uv build backend (`uv_build`). /// /// Note that those settings only apply when using the `uv_build` backend, other build backends /// (such as hatchling) have their own configuration. /// /// All options that accept globs use the portable glob patterns from /// [PEP 639](https://packaging.python.org/en/latest/specifications/glob-patterns/). #[derive(Deserialize, Serialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)] #[serde(default, rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct BuildBackendSettings { /// The directory that contains the module directory. /// /// Common values are `src` (src layout, the default) or an empty path (flat layout). #[option( default = r#""src""#, value_type = "str", example = r#"module-root = """# )] pub module_root: PathBuf, /// The name of the module directory inside `module-root`. /// /// The default module name is the package name with dots and dashes replaced by underscores. /// /// Package names need to be valid Python identifiers, and the directory needs to contain a /// `__init__.py`. An exception are stubs packages, whose name ends with `-stubs`, with the stem /// being the module name, and which contain a `__init__.pyi` file. /// /// For namespace packages with a single module, the path can be dotted, e.g., `foo.bar` or /// `foo-stubs.bar`. /// /// For namespace packages with multiple modules, the path can be a list, e.g., /// `["foo", "bar"]`. We recommend using a single module per package, splitting multiple /// packages into a workspace. /// /// Note that using this option runs the risk of creating two packages with different names but /// the same module names. Installing such packages together leads to unspecified behavior, /// often with corrupted files or directory trees. #[option( default = r#"None"#, value_type = "str | list[str]", example = r#"module-name = "sklearn""# )] pub module_name: Option, /// Glob expressions which files and directories to additionally include in the source /// distribution. /// /// `pyproject.toml` and the contents of the module directory are always included. #[option( default = r#"[]"#, value_type = "list[str]", example = r#"source-include = ["tests/**"]"# )] pub source_include: Vec, /// If set to `false`, the default excludes aren't applied. /// /// Default excludes: `__pycache__`, `*.pyc`, and `*.pyo`. #[option( default = r#"true"#, value_type = "bool", example = r#"default-excludes = false"# )] pub default_excludes: bool, /// Glob expressions which files and directories to exclude from the source distribution. /// /// These exclusions are also applied to wheels to ensure that a wheel built from a source tree /// is consistent with a wheel built from a source distribution. #[option( default = r#"[]"#, value_type = "list[str]", example = r#"source-exclude = ["*.bin"]"# )] pub source_exclude: Vec, /// Glob expressions which files and directories to exclude from the wheel. #[option( default = r#"[]"#, value_type = "list[str]", example = r#"wheel-exclude = ["*.bin"]"# )] pub wheel_exclude: Vec, /// Build a namespace package. /// /// Build a PEP 420 implicit namespace package, allowing more than one root `__init__.py`. /// /// Use this option when the namespace package contains multiple root `__init__.py`, for /// namespace packages with a single root `__init__.py` use a dotted `module-name` instead. /// /// To compare dotted `module-name` and `namespace = true`, the first example below can be /// expressed with `module-name = "cloud.database"`: There is one root `__init__.py` `database`. /// In the second example, we have three roots (`cloud.database`, `cloud.database_pro`, /// `billing.modules.database_pro`), so `namespace = true` is required. /// /// ```text /// src /// └── cloud /// └── database /// ├── __init__.py /// ├── query_builder /// │ └── __init__.py /// └── sql /// ├── parser.py /// └── __init__.py /// ``` /// /// ```text /// src /// ├── cloud /// │ ├── database /// │ │ ├── __init__.py /// │ │ ├── query_builder /// │ │ │ └── __init__.py /// │ │ └── sql /// │ │ ├── __init__.py /// │ │ └── parser.py /// │ └── database_pro /// │ ├── __init__.py /// │ └── query_builder.py /// └── billing /// └── modules /// └── database_pro /// ├── __init__.py /// └── sql.py /// ``` #[option( default = r#"false"#, value_type = "bool", example = r#"namespace = true"# )] pub namespace: bool, /// Data includes for wheels. /// /// Each entry is a directory, whose contents are copied to the matching directory in the wheel /// in `-.data/(purelib|platlib|headers|scripts|data)`. Upon installation, this /// data is moved to its target location, as defined by /// . Usually, small /// data files are included by placing them in the Python module instead of using data includes. /// /// - `scripts`: Installed to the directory for executables, `/bin` on Unix or /// `\Scripts` on Windows. This directory is added to `PATH` when the virtual /// environment is activated or when using `uv run`, so this data type can be used to install /// additional binaries. Consider using `project.scripts` instead for Python entrypoints. /// - `data`: Installed over the virtualenv environment root. /// /// Warning: This may override existing files! /// /// - `headers`: Installed to the include directory. Compilers building Python packages /// with this package as build requirement use the include directory to find additional header /// files. /// - `purelib` and `platlib`: Installed to the `site-packages` directory. It is not recommended /// to use these two options. // TODO(konsti): We should show a flat example instead. // ```toml // [tool.uv.build-backend.data] // headers = "include/headers", // scripts = "bin" // ``` #[option( default = r#"{}"#, value_type = "dict[str, str]", example = r#"data = { headers = "include/headers", scripts = "bin" }"# )] pub data: WheelDataIncludes, } impl Default for BuildBackendSettings { fn default() -> Self { Self { module_root: PathBuf::from("src"), module_name: None, source_include: Vec::new(), default_excludes: true, source_exclude: Vec::new(), wheel_exclude: Vec::new(), namespace: false, data: WheelDataIncludes::default(), } } } /// Whether to include a single module or multiple modules. #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] pub enum ModuleName { /// A single module name. Name(String), /// Multiple module names, which are all included. Names(Vec), } /// Data includes for wheels. /// /// See `BuildBackendSettings::data`. #[derive(Default, Deserialize, Serialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)] // `deny_unknown_fields` to catch typos such as `header` vs `headers`. #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct WheelDataIncludes { purelib: Option, platlib: Option, headers: Option, scripts: Option, data: Option, } impl WheelDataIncludes { /// Yield all data directories name and corresponding paths. pub fn iter(&self) -> impl Iterator { [ ("purelib", self.purelib.as_deref()), ("platlib", self.platlib.as_deref()), ("headers", self.headers.as_deref()), ("scripts", self.scripts.as_deref()), ("data", self.data.as_deref()), ] .into_iter() .filter_map(|(name, value)| Some((name, value?))) } } uv-0.9.17+ds1/crates/uv-build-backend/src/source_dist.rs000066400000000000000000000313631520155276700230220ustar00rootroot00000000000000use crate::metadata::DEFAULT_EXCLUDES; use crate::wheel::build_exclude_matcher; use crate::{ BuildBackendSettings, DirectoryWriter, Error, FileList, ListWriter, PyProjectToml, error_on_venv, find_roots, }; use flate2::Compression; use flate2::write::GzEncoder; use fs_err::File; use globset::{Glob, GlobSet}; use std::io; use std::io::{BufReader, Cursor}; use std::path::{Component, Path, PathBuf}; use tar::{EntryType, Header}; use tracing::{debug, trace}; use uv_distribution_filename::{SourceDistExtension, SourceDistFilename}; use uv_fs::Simplified; use uv_globfilter::{GlobDirFilter, PortableGlobParser}; use uv_warnings::warn_user_once; use walkdir::WalkDir; /// Build a source distribution from the source tree and place it in the output directory. pub fn build_source_dist( source_tree: &Path, source_dist_directory: &Path, uv_version: &str, show_warnings: bool, ) -> Result { let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; let filename = SourceDistFilename { name: pyproject_toml.name().clone(), version: pyproject_toml.version().clone(), extension: SourceDistExtension::TarGz, }; let source_dist_path = source_dist_directory.join(filename.to_string()); let writer = TarGzWriter::new(&source_dist_path)?; write_source_dist(source_tree, writer, uv_version, show_warnings)?; Ok(filename) } /// List the files that would be included in a source distribution and their origin. pub fn list_source_dist( source_tree: &Path, uv_version: &str, show_warnings: bool, ) -> Result<(SourceDistFilename, FileList), Error> { let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; let filename = SourceDistFilename { name: pyproject_toml.name().clone(), version: pyproject_toml.version().clone(), extension: SourceDistExtension::TarGz, }; let mut files = FileList::new(); let writer = ListWriter::new(&mut files); write_source_dist(source_tree, writer, uv_version, show_warnings)?; Ok((filename, files)) } /// Build includes and excludes for source tree walking for source dists. fn source_dist_matcher( source_tree: &Path, pyproject_toml: &PyProjectToml, settings: BuildBackendSettings, show_warnings: bool, ) -> Result<(GlobDirFilter, GlobSet), Error> { // File and directories to include in the source directory let mut include_globs = Vec::new(); let mut includes: Vec = settings.source_include; // pyproject.toml is always included. includes.push(globset::escape("pyproject.toml")); // Check that the source tree contains a module. let (src_root, modules_relative) = find_roots( source_tree, pyproject_toml, &settings.module_root, settings.module_name.as_ref(), settings.namespace, show_warnings, )?; for module_relative in modules_relative { // The wheel must not include any files included by the source distribution (at least until we // have files generated in the source dist -> wheel build step). let import_path = uv_fs::normalize_path( &uv_fs::relative_to(src_root.join(module_relative), source_tree) .expect("module root is inside source tree"), ) .portable_display() .to_string(); includes.push(format!("{}/**", globset::escape(&import_path))); } for include in includes { let glob = PortableGlobParser::Uv .parse(&include) .map_err(|err| Error::PortableGlob { field: "tool.uv.build-backend.source-include".to_string(), source: err, })?; include_globs.push(glob); } // Include the Readme if let Some(readme) = pyproject_toml .readme() .as_ref() .and_then(|readme| readme.path()) { let readme = uv_fs::normalize_path(readme); trace!("Including readme at: {}", readme.user_display()); let readme = readme.portable_display().to_string(); let glob = Glob::new(&globset::escape(&readme)).expect("escaped globset is parseable"); include_globs.push(glob); } // Include the license files for license_files in pyproject_toml.license_files_source_dist() { trace!("Including license files at: {license_files}`"); let glob = PortableGlobParser::Pep639 .parse(license_files) .map_err(|err| Error::PortableGlob { field: "project.license-files".to_string(), source: err, })?; include_globs.push(glob); } // Include the data files for (name, directory) in settings.data.iter() { let directory = uv_fs::normalize_path(directory); trace!("Including data ({}) at: {}", name, directory.user_display()); if directory .components() .next() .is_some_and(|component| !matches!(component, Component::CurDir | Component::Normal(_))) { return Err(Error::InvalidDataRoot { name: name.to_string(), path: directory.to_path_buf(), }); } let directory = directory.portable_display().to_string(); let glob = PortableGlobParser::Uv .parse(&format!("{}/**", globset::escape(&directory))) .map_err(|err| Error::PortableGlob { field: format!("tool.uv.build-backend.data.{name}"), source: err, })?; include_globs.push(glob); } debug!( "Source distribution includes: {:?}", include_globs .iter() .map(ToString::to_string) .collect::>() ); let include_matcher = GlobDirFilter::from_globs(&include_globs).map_err(|err| Error::GlobSetTooLarge { field: "tool.uv.build-backend.source-include".to_string(), source: err, })?; let mut excludes: Vec = Vec::new(); if settings.default_excludes { excludes.extend(DEFAULT_EXCLUDES.iter().map(ToString::to_string)); } for exclude in settings.source_exclude { // Avoid duplicate entries. if !excludes.contains(&exclude) { excludes.push(exclude); } } debug!("Source dist excludes: {:?}", excludes); let exclude_matcher = build_exclude_matcher(excludes)?; if exclude_matcher.is_match("pyproject.toml") { return Err(Error::PyprojectTomlExcluded); } Ok((include_matcher, exclude_matcher)) } /// Shared implementation for building and listing a source distribution. fn write_source_dist( source_tree: &Path, mut writer: impl DirectoryWriter, uv_version: &str, show_warnings: bool, ) -> Result { let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { warn_user_once!("{warning}"); } let settings = pyproject_toml .settings() .cloned() .unwrap_or_else(BuildBackendSettings::default); let filename = SourceDistFilename { name: pyproject_toml.name().clone(), version: pyproject_toml.version().clone(), extension: SourceDistExtension::TarGz, }; let top_level = format!( "{}-{}", pyproject_toml.name().as_dist_info_name(), pyproject_toml.version() ); let metadata = pyproject_toml.to_metadata(source_tree)?; let metadata_email = metadata.core_metadata_format(); debug!("Adding content files to source distribution"); writer.write_bytes( &Path::new(&top_level) .join("PKG-INFO") .portable_display() .to_string(), metadata_email.as_bytes(), )?; let (include_matcher, exclude_matcher) = source_dist_matcher(source_tree, &pyproject_toml, settings, show_warnings)?; let mut files_visited = 0; for entry in WalkDir::new(source_tree) .sort_by_file_name() .into_iter() .filter_entry(|entry| { // TODO(konsti): This should be prettier. let relative = entry .path() .strip_prefix(source_tree) .expect("walkdir starts with root"); // Fast path: Don't descend into a directory that can't be included. This is the most // important performance optimization, it avoids descending into directories such as // `.venv`. While walkdir is generally cheap, we still avoid traversing large data // directories that often exist on the top level of a project. This is especially noticeable // on network file systems with high latencies per operation (while contiguous reading may // still be fast). include_matcher.match_directory(relative) && !exclude_matcher.is_match(relative) }) { let entry = entry.map_err(|err| Error::WalkDir { root: source_tree.to_path_buf(), err, })?; files_visited += 1; if files_visited > 10000 { warn_user_once!( "Visited more than 10,000 files for source distribution build. \ Consider using more constrained includes or more excludes." ); } // TODO(konsti): This should be prettier. let relative = entry .path() .strip_prefix(source_tree) .expect("walkdir starts with root"); if !include_matcher.match_path(relative) || exclude_matcher.is_match(relative) { trace!("Excluding from sdist: {}", relative.user_display()); continue; } error_on_venv(entry.file_name(), entry.path())?; let entry_path = Path::new(&top_level) .join(relative) .portable_display() .to_string(); debug!("Adding to sdist: {}", relative.user_display()); writer.write_dir_entry(&entry, &entry_path)?; } debug!("Visited {files_visited} files for source dist build"); writer.close(&top_level)?; Ok(filename) } struct TarGzWriter { path: PathBuf, tar: tar::Builder>, } impl TarGzWriter { fn new(path: impl Into) -> Result { let path = path.into(); let file = File::create(&path)?; let enc = GzEncoder::new(file, Compression::default()); let tar = tar::Builder::new(enc); Ok(Self { path, tar }) } } impl DirectoryWriter for TarGzWriter { fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> { let mut header = Header::new_gnu(); header.set_size(bytes.len() as u64); // Reasonable default to avoid 0o000 permissions, the user's umask will be applied on // unpacking. header.set_mode(0o644); self.tar .append_data(&mut header, path, Cursor::new(bytes)) .map_err(|err| Error::TarWrite(self.path.clone(), err))?; Ok(()) } fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> { let metadata = fs_err::metadata(file)?; let mut header = Header::new_gnu(); // Preserve the executable bit, especially for scripts #[cfg(unix)] let executable_bit = { use std::os::unix::fs::PermissionsExt; file.metadata()?.permissions().mode() & 0o111 != 0 }; // Windows has no executable bit #[cfg(not(unix))] let executable_bit = false; // Set reasonable defaults to avoid 0o000 permissions, while avoiding adding the exact // filesystem permissions to the archive for reproducibility. Where applicable, the // operating system filters the stored permission by the user's umask when unpacking. if executable_bit { header.set_mode(0o755); } else { header.set_mode(0o644); } header.set_size(metadata.len()); let reader = BufReader::new(File::open(file)?); self.tar .append_data(&mut header, path, reader) .map_err(|err| Error::TarWrite(self.path.clone(), err))?; Ok(()) } fn write_directory(&mut self, directory: &str) -> Result<(), Error> { let mut header = Header::new_gnu(); // Directories are always executable, which means they can be listed. header.set_mode(0o755); header.set_entry_type(EntryType::Directory); header.set_size(0); self.tar .append_data(&mut header, directory, io::empty()) .map_err(|err| Error::TarWrite(self.path.clone(), err))?; Ok(()) } fn close(mut self, _dist_info_dir: &str) -> Result<(), Error> { self.tar .finish() .map_err(|err| Error::TarWrite(self.path.clone(), err))?; Ok(()) } } uv-0.9.17+ds1/crates/uv-build-backend/src/wheel.rs000066400000000000000000000712201520155276700215770ustar00rootroot00000000000000use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD as base64}; use fs_err::File; use globset::{GlobSet, GlobSetBuilder}; use itertools::Itertools; use rustc_hash::FxHashSet; use sha2::{Digest, Sha256}; use std::io::{BufReader, Read, Write}; use std::path::{Component, Path, PathBuf}; use std::{io, mem}; use tracing::{debug, trace}; use walkdir::WalkDir; use zip::{CompressionMethod, ZipWriter}; use uv_distribution_filename::WheelFilename; use uv_fs::Simplified; use uv_globfilter::{GlobDirFilter, PortableGlobParser}; use uv_platform_tags::{AbiTag, LanguageTag, PlatformTag}; use uv_warnings::warn_user_once; use crate::metadata::DEFAULT_EXCLUDES; use crate::{ BuildBackendSettings, DirectoryWriter, Error, FileList, ListWriter, PyProjectToml, error_on_venv, find_roots, }; /// Build a wheel from the source tree and place it in the output directory. pub fn build_wheel( source_tree: &Path, wheel_dir: &Path, metadata_directory: Option<&Path>, uv_version: &str, show_warnings: bool, ) -> Result { let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { warn_user_once!("{warning}"); } crate::check_metadata_directory(source_tree, metadata_directory, &pyproject_toml)?; let filename = WheelFilename::new( pyproject_toml.name().clone(), pyproject_toml.version().clone(), LanguageTag::Python { major: 3, minor: None, }, AbiTag::None, PlatformTag::Any, ); let wheel_path = wheel_dir.join(filename.to_string()); debug!("Writing wheel at {}", wheel_path.user_display()); let wheel_writer = ZipDirectoryWriter::new_wheel(File::create(&wheel_path)?); write_wheel( source_tree, &pyproject_toml, &filename, uv_version, wheel_writer, show_warnings, )?; Ok(filename) } /// List the files that would be included in a source distribution and their origin. pub fn list_wheel( source_tree: &Path, uv_version: &str, show_warnings: bool, ) -> Result<(WheelFilename, FileList), Error> { let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { warn_user_once!("{warning}"); } let filename = WheelFilename::new( pyproject_toml.name().clone(), pyproject_toml.version().clone(), LanguageTag::Python { major: 3, minor: None, }, AbiTag::None, PlatformTag::Any, ); let mut files = FileList::new(); let writer = ListWriter::new(&mut files); write_wheel( source_tree, &pyproject_toml, &filename, uv_version, writer, show_warnings, )?; Ok((filename, files)) } fn write_wheel( source_tree: &Path, pyproject_toml: &PyProjectToml, filename: &WheelFilename, uv_version: &str, mut wheel_writer: impl DirectoryWriter, show_warnings: bool, ) -> Result<(), Error> { let settings = pyproject_toml .settings() .cloned() .unwrap_or_else(BuildBackendSettings::default); // Wheel excludes let mut excludes: Vec = Vec::new(); if settings.default_excludes { excludes.extend(DEFAULT_EXCLUDES.iter().map(ToString::to_string)); } for exclude in settings.wheel_exclude { // Avoid duplicate entries. if !excludes.contains(&exclude) { excludes.push(exclude); } } // The wheel must not include any files excluded by the source distribution (at least until we // have files generated in the source dist -> wheel build step). for exclude in &settings.source_exclude { // Avoid duplicate entries. if !excludes.contains(exclude) { excludes.push(exclude.clone()); } } debug!("Wheel excludes: {:?}", excludes); let exclude_matcher = build_exclude_matcher(excludes)?; debug!("Adding content files to wheel"); let (src_root, module_relative) = find_roots( source_tree, pyproject_toml, &settings.module_root, settings.module_name.as_ref(), settings.namespace, show_warnings, )?; let mut files_visited = 0; let mut prefix_directories = FxHashSet::default(); for module_relative in module_relative { // For convenience, have directories for the whole tree in the wheel for ancestor in module_relative.ancestors().skip(1) { if ancestor == Path::new("") { continue; } // Avoid duplicate directories in the zip. if prefix_directories.insert(ancestor.to_path_buf()) { wheel_writer.write_directory(&ancestor.portable_display().to_string())?; } } for entry in WalkDir::new(src_root.join(module_relative)) .sort_by_file_name() .into_iter() .filter_entry(|entry| !exclude_matcher.is_match(entry.path())) { let entry = entry.map_err(|err| Error::WalkDir { root: source_tree.to_path_buf(), err, })?; files_visited += 1; if files_visited > 10000 { warn_user_once!( "Visited more than 10,000 files for wheel build. \ Consider using more constrained includes or more excludes." ); } // We only want to take the module root, but since excludes start at the source tree root, // we strip higher than we iterate. let match_path = entry .path() .strip_prefix(source_tree) .expect("walkdir starts with root"); let entry_path = entry .path() .strip_prefix(&src_root) .expect("walkdir starts with root"); if exclude_matcher.is_match(match_path) { trace!("Excluding from module: {}", match_path.user_display()); continue; } error_on_venv(entry.file_name(), entry.path())?; let entry_path = entry_path.portable_display().to_string(); debug!("Adding to wheel: {entry_path}"); wheel_writer.write_dir_entry(&entry, &entry_path)?; } } debug!("Visited {files_visited} files for wheel build"); // Add the license files if pyproject_toml.license_files_wheel().next().is_some() { debug!("Adding license files"); let license_dir = format!( "{}-{}.dist-info/licenses/", pyproject_toml.name().as_dist_info_name(), pyproject_toml.version() ); wheel_subdir_from_globs( source_tree, &license_dir, pyproject_toml.license_files_wheel(), &mut wheel_writer, "project.license-files", )?; } // Add the data files for (name, directory) in settings.data.iter() { debug!( "Adding {name} data files from: {}", directory.user_display() ); if directory .components() .next() .is_some_and(|component| !matches!(component, Component::CurDir | Component::Normal(_))) { return Err(Error::InvalidDataRoot { name: name.to_string(), path: directory.to_path_buf(), }); } let data_dir = format!( "{}-{}.data/{}/", pyproject_toml.name().as_dist_info_name(), pyproject_toml.version(), name ); wheel_subdir_from_globs( &source_tree.join(directory), &data_dir, &["**".to_string()], &mut wheel_writer, &format!("tool.uv.build-backend.data.{name}"), )?; } debug!("Adding metadata files to wheel"); let dist_info_dir = write_dist_info( &mut wheel_writer, pyproject_toml, filename, source_tree, uv_version, )?; wheel_writer.close(&dist_info_dir)?; Ok(()) } /// Build a wheel from the source tree and place it in the output directory. pub fn build_editable( source_tree: &Path, wheel_dir: &Path, metadata_directory: Option<&Path>, uv_version: &str, show_warnings: bool, ) -> Result { let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { warn_user_once!("{warning}"); } let settings = pyproject_toml .settings() .cloned() .unwrap_or_else(BuildBackendSettings::default); crate::check_metadata_directory(source_tree, metadata_directory, &pyproject_toml)?; let filename = WheelFilename::new( pyproject_toml.name().clone(), pyproject_toml.version().clone(), LanguageTag::Python { major: 3, minor: None, }, AbiTag::None, PlatformTag::Any, ); let wheel_path = wheel_dir.join(filename.to_string()); debug!("Writing wheel at {}", wheel_path.user_display()); let mut wheel_writer = ZipDirectoryWriter::new_wheel(File::create(&wheel_path)?); debug!("Adding pth file to {}", wheel_path.user_display()); // Check that a module root exists in the directory we're linking from the `.pth` file let (src_root, _module_relative) = find_roots( source_tree, &pyproject_toml, &settings.module_root, settings.module_name.as_ref(), settings.namespace, show_warnings, )?; wheel_writer.write_bytes( &format!("{}.pth", pyproject_toml.name().as_dist_info_name()), src_root.as_os_str().as_encoded_bytes(), )?; debug!("Adding metadata files to: {}", wheel_path.user_display()); let dist_info_dir = write_dist_info( &mut wheel_writer, &pyproject_toml, &filename, source_tree, uv_version, )?; wheel_writer.close(&dist_info_dir)?; Ok(filename) } /// Write the dist-info directory to the output directory without building the wheel. pub fn metadata( source_tree: &Path, metadata_directory: &Path, uv_version: &str, ) -> Result { let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { warn_user_once!("{warning}"); } let filename = WheelFilename::new( pyproject_toml.name().clone(), pyproject_toml.version().clone(), LanguageTag::Python { major: 3, minor: None, }, AbiTag::None, PlatformTag::Any, ); debug!( "Writing metadata files to {}", metadata_directory.user_display() ); let mut wheel_writer = FilesystemWriter::new(metadata_directory); let dist_info_dir = write_dist_info( &mut wheel_writer, &pyproject_toml, &filename, source_tree, uv_version, )?; wheel_writer.close(&dist_info_dir)?; Ok(dist_info_dir) } /// An entry in the `RECORD` file. /// /// struct RecordEntry { /// The path to the file relative to the package root. /// /// While the spec would allow backslashes, we always use portable paths with forward slashes. path: String, /// The urlsafe-base64-nopad encoded SHA256 of the files. hash: String, /// The size of the file in bytes. size: usize, } /// Read the input file and write it both to the hasher and the target file. /// /// We're implementing this tee-ing manually since there is no sync `InspectReader` or std tee /// function. fn write_hashed( path: &str, reader: &mut dyn Read, writer: &mut dyn Write, ) -> Result { let mut hasher = Sha256::new(); let mut size = 0; // 8KB is the default defined in `std::sys_common::io`. let mut buffer = vec![0; 8 * 1024]; loop { let read = match reader.read(&mut buffer) { Ok(read) => read, Err(err) if err.kind() == io::ErrorKind::Interrupted => continue, Err(err) => return Err(err), }; if read == 0 { // End of file break; } hasher.update(&buffer[..read]); writer.write_all(&buffer[..read])?; size += read; } Ok(RecordEntry { path: path.to_string(), hash: base64.encode(hasher.finalize()), size, }) } /// Write the `RECORD` file. /// /// fn write_record( writer: &mut dyn Write, dist_info_dir: &str, record: Vec, ) -> Result<(), Error> { let mut record_writer = csv::Writer::from_writer(writer); for entry in record { record_writer.write_record(&[ entry.path, format!("sha256={}", entry.hash), entry.size.to_string(), ])?; } // We can't compute the hash or size for RECORD without modifying it at the same time. record_writer.write_record(&[ format!("{dist_info_dir}/RECORD"), String::new(), String::new(), ])?; record_writer.flush()?; Ok(()) } /// Build a globset matcher for excludes. pub(crate) fn build_exclude_matcher( excludes: impl IntoIterator>, ) -> Result { let mut exclude_builder = GlobSetBuilder::new(); for exclude in excludes { let exclude = exclude.as_ref(); // Excludes are unanchored let exclude = if let Some(exclude) = exclude.strip_prefix("/") { exclude.to_string() } else { format!("**/{exclude}").to_string() }; let glob = PortableGlobParser::Uv .parse(&exclude) .map_err(|err| Error::PortableGlob { field: "tool.uv.build-backend.*-exclude".to_string(), source: err, })?; exclude_builder.add(glob); } let exclude_matcher = exclude_builder .build() .map_err(|err| Error::GlobSetTooLarge { field: "tool.uv.build-backend.*-exclude".to_string(), source: err, })?; Ok(exclude_matcher) } /// Add the files and directories matching from the source tree matching any of the globs in the /// wheel subdirectory. fn wheel_subdir_from_globs( src: &Path, target: &str, globs: impl IntoIterator>, wheel_writer: &mut impl DirectoryWriter, // For error messages globs_field: &str, ) -> Result<(), Error> { let license_files_globs: Vec<_> = globs .into_iter() .map(|license_files| { let license_files = license_files.as_ref(); trace!( "Including {} at `{}` with `{}`", globs_field, src.user_display(), license_files ); PortableGlobParser::Pep639.parse(license_files) }) .collect::>() .map_err(|err| Error::PortableGlob { field: globs_field.to_string(), source: err, })?; let matcher = GlobDirFilter::from_globs(&license_files_globs).map_err(|err| Error::GlobSetTooLarge { field: globs_field.to_string(), source: err, })?; wheel_writer.write_directory(target)?; for entry in WalkDir::new(src) .sort_by_file_name() .into_iter() .filter_entry(|entry| { // TODO(konsti): This should be prettier. let relative = entry .path() .strip_prefix(src) .expect("walkdir starts with root"); // Fast path: Don't descend into a directory that can't be included. matcher.match_directory(relative) }) { let entry = entry.map_err(|err| Error::WalkDir { root: src.to_path_buf(), err, })?; // Skip the root path, which is already included as `target` prior to the loop. // (If `entry.path() == src`, then `relative` is empty, and `relative_licenses` is // `target`.) if entry.path() == src { continue; } // TODO(konsti): This should be prettier. let relative = entry .path() .strip_prefix(src) .expect("walkdir starts with root"); if !matcher.match_path(relative) { trace!("Excluding {}: {}", globs_field, relative.user_display()); continue; } error_on_venv(entry.file_name(), entry.path())?; let license_path = Path::new(target) .join(relative) .portable_display() .to_string(); debug!("Adding for {}: {}", globs_field, relative.user_display()); wheel_writer.write_dir_entry(&entry, &license_path)?; } Ok(()) } /// Add `METADATA` and `entry_points.txt` to the dist-info directory. /// /// Returns the name of the dist-info directory. fn write_dist_info( writer: &mut dyn DirectoryWriter, pyproject_toml: &PyProjectToml, filename: &WheelFilename, root: &Path, uv_version: &str, ) -> Result { let dist_info_dir = format!( "{}-{}.dist-info", pyproject_toml.name().as_dist_info_name(), pyproject_toml.version() ); writer.write_directory(&dist_info_dir)?; // Add `WHEEL`. let wheel_info = wheel_info(filename, uv_version); writer.write_bytes(&format!("{dist_info_dir}/WHEEL"), wheel_info.as_bytes())?; // Add `entry_points.txt`. if let Some(entrypoint) = pyproject_toml.to_entry_points()? { writer.write_bytes( &format!("{dist_info_dir}/entry_points.txt"), entrypoint.as_bytes(), )?; } // Add `METADATA`. let metadata = pyproject_toml.to_metadata(root)?.core_metadata_format(); writer.write_bytes(&format!("{dist_info_dir}/METADATA"), metadata.as_bytes())?; // `RECORD` is added on closing. Ok(dist_info_dir) } /// Returns the `WHEEL` file contents. fn wheel_info(filename: &WheelFilename, uv_version: &str) -> String { // https://packaging.python.org/en/latest/specifications/binary-distribution-format/#file-contents let mut wheel_info = vec![ ("Wheel-Version", "1.0".to_string()), ("Generator", format!("uv {uv_version}")), ("Root-Is-Purelib", "true".to_string()), ]; for python_tag in filename.python_tags() { for abi_tag in filename.abi_tags() { for platform_tag in filename.platform_tags() { wheel_info.push(("Tag", format!("{python_tag}-{abi_tag}-{platform_tag}"))); } } } wheel_info .into_iter() .map(|(key, value)| format!("{key}: {value}")) .join("\n") } /// Zip archive (wheel) writer. struct ZipDirectoryWriter { writer: ZipWriter, compression: CompressionMethod, /// The entries in the `RECORD` file. record: Vec, } impl ZipDirectoryWriter { /// A wheel writer with deflate compression. fn new_wheel(file: File) -> Self { Self { writer: ZipWriter::new(file), compression: CompressionMethod::Deflated, record: Vec::new(), } } /// A wheel writer with no (stored) compression. /// /// Since editables are temporary, we save time be skipping compression and decompression. #[expect(dead_code)] fn new_editable(file: File) -> Self { Self { writer: ZipWriter::new(file), compression: CompressionMethod::Stored, record: Vec::new(), } } /// Add a file with the given name and return a writer for it. fn new_writer<'slf>( &'slf mut self, path: &str, executable_bit: bool, ) -> Result, Error> { // Set file permissions: 644 (rw-r--r--) for regular files, 755 (rwxr-xr-x) for executables let permissions = if executable_bit { 0o755 } else { 0o644 }; let options = zip::write::SimpleFileOptions::default() .unix_permissions(permissions) .compression_method(self.compression); self.writer.start_file(path, options)?; Ok(Box::new(&mut self.writer)) } } impl DirectoryWriter for ZipDirectoryWriter { fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> { trace!("Adding {}", path); // Set appropriate permissions for metadata files (644 = rw-r--r--) let options = zip::write::SimpleFileOptions::default() .unix_permissions(0o644) .compression_method(self.compression); self.writer.start_file(path, options)?; self.writer.write_all(bytes)?; let hash = base64.encode(Sha256::new().chain_update(bytes).finalize()); self.record.push(RecordEntry { path: path.to_string(), hash, size: bytes.len(), }); Ok(()) } fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> { trace!("Adding {} from {}", path, file.user_display()); let mut reader = BufReader::new(File::open(file)?); // Preserve the executable bit, especially for scripts #[cfg(unix)] let executable_bit = { use std::os::unix::fs::PermissionsExt; file.metadata()?.permissions().mode() & 0o111 != 0 }; // Windows has no executable bit #[cfg(not(unix))] let executable_bit = false; let mut writer = self.new_writer(path, executable_bit)?; let record = write_hashed(path, &mut reader, &mut writer)?; drop(writer); self.record.push(record); Ok(()) } fn write_directory(&mut self, directory: &str) -> Result<(), Error> { trace!("Adding directory {}", directory); let options = zip::write::SimpleFileOptions::default().compression_method(self.compression); Ok(self.writer.add_directory(directory, options)?) } /// Write the `RECORD` file and the central directory. fn close(mut self, dist_info_dir: &str) -> Result<(), Error> { let record_path = format!("{dist_info_dir}/RECORD"); trace!("Adding {record_path}"); let record = mem::take(&mut self.record); write_record( &mut self.new_writer(&record_path, false)?, dist_info_dir, record, )?; trace!("Adding central directory"); self.writer.finish()?; Ok(()) } } struct FilesystemWriter { /// The virtualenv or metadata directory that add file paths are relative to. root: PathBuf, /// The entries in the `RECORD` file. record: Vec, } impl FilesystemWriter { fn new(root: &Path) -> Self { Self { root: root.to_owned(), record: Vec::new(), } } /// Add a file with the given name and return a writer for it. fn new_writer<'slf>(&'slf mut self, path: &str) -> Result, Error> { trace!("Adding {}", path); Ok(Box::new(File::create(self.root.join(path))?)) } } /// File system writer. impl DirectoryWriter for FilesystemWriter { fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> { trace!("Adding {}", path); let hash = base64.encode(Sha256::new().chain_update(bytes).finalize()); self.record.push(RecordEntry { path: path.to_string(), hash, size: bytes.len(), }); Ok(fs_err::write(self.root.join(path), bytes)?) } fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> { trace!("Adding {} from {}", path, file.user_display()); let mut reader = BufReader::new(File::open(file)?); let mut writer = self.new_writer(path)?; let record = write_hashed(path, &mut reader, &mut writer)?; drop(writer); self.record.push(record); Ok(()) } fn write_directory(&mut self, directory: &str) -> Result<(), Error> { trace!("Adding directory {}", directory); Ok(fs_err::create_dir(self.root.join(directory))?) } /// Write the `RECORD` file. fn close(mut self, dist_info_dir: &str) -> Result<(), Error> { let record = mem::take(&mut self.record); write_record( &mut self.new_writer(&format!("{dist_info_dir}/RECORD"))?, dist_info_dir, record, )?; Ok(()) } } #[cfg(test)] mod test { use super::*; use insta::assert_snapshot; use std::path::Path; use std::str::FromStr; use tempfile::TempDir; use uv_distribution_filename::WheelFilename; use uv_fs::Simplified; use uv_normalize::PackageName; use uv_pep440::Version; use uv_platform_tags::{AbiTag, PlatformTag}; use walkdir::WalkDir; #[test] fn test_wheel() { let filename = WheelFilename::new( PackageName::from_str("foo").unwrap(), Version::from_str("1.2.3").unwrap(), LanguageTag::Python { major: 3, minor: None, }, AbiTag::None, PlatformTag::Any, ); assert_snapshot!(wheel_info(&filename, "1.0.0+test"), @r" Wheel-Version: 1.0 Generator: uv 1.0.0+test Root-Is-Purelib: true Tag: py3-none-any "); } #[test] fn test_record() { let record = vec![RecordEntry { path: "built_by_uv/__init__.py".to_string(), hash: "ifhp5To6AGGlLAIz5kQtTXLegKii00BtnqC_05fteGU".to_string(), size: 37, }]; let mut writer = Vec::new(); write_record(&mut writer, "built_by_uv-0.1.0", record).unwrap(); assert_snapshot!(String::from_utf8(writer).unwrap(), @r" built_by_uv/__init__.py,sha256=ifhp5To6AGGlLAIz5kQtTXLegKii00BtnqC_05fteGU,37 built_by_uv-0.1.0/RECORD,, "); } /// Snapshot all files from the prepare metadata hook. #[test] fn test_prepare_metadata() { let metadata_dir = TempDir::new().unwrap(); let built_by_uv = Path::new("../../test/packages/built-by-uv"); metadata(built_by_uv, metadata_dir.path(), "1.0.0+test").unwrap(); let mut files: Vec<_> = WalkDir::new(metadata_dir.path()) .sort_by_file_name() .into_iter() .map(|entry| { entry .unwrap() .path() .strip_prefix(metadata_dir.path()) .expect("walkdir starts with root") .portable_display() .to_string() }) .filter(|path| !path.is_empty()) .collect(); files.sort(); assert_snapshot!(files.join("\n"), @r###" built_by_uv-0.1.0.dist-info built_by_uv-0.1.0.dist-info/METADATA built_by_uv-0.1.0.dist-info/RECORD built_by_uv-0.1.0.dist-info/WHEEL built_by_uv-0.1.0.dist-info/entry_points.txt "###); let metadata_file = metadata_dir .path() .join("built_by_uv-0.1.0.dist-info/METADATA"); assert_snapshot!(fs_err::read_to_string(metadata_file).unwrap(), @r###" Metadata-Version: 2.4 Name: built-by-uv Version: 0.1.0 Summary: A package to be built with the uv build backend that uses all features exposed by the build backend License-File: LICENSE-APACHE License-File: LICENSE-MIT License-File: third-party-licenses/PEP-401.txt Requires-Dist: anyio>=4,<5 Requires-Python: >=3.12 Description-Content-Type: text/markdown # built_by_uv A package to be built with the uv build backend that uses all features exposed by the build backend. "###); let record_file = metadata_dir .path() .join("built_by_uv-0.1.0.dist-info/RECORD"); assert_snapshot!(fs_err::read_to_string(record_file).unwrap(), @r###" built_by_uv-0.1.0.dist-info/WHEEL,sha256=PaG_oOj9G2zCRqoLK0SjWBVZbGAMtIXDmm-MEGw9Wo0,83 built_by_uv-0.1.0.dist-info/entry_points.txt,sha256=-IO6yaq6x6HSl-zWH96rZmgYvfyHlH00L5WQoCpz-YI,50 built_by_uv-0.1.0.dist-info/METADATA,sha256=m6EkVvKrGmqx43b_VR45LHD37IZxPYC0NI6Qx9_UXLE,474 built_by_uv-0.1.0.dist-info/RECORD,, "###); let wheel_file = metadata_dir .path() .join("built_by_uv-0.1.0.dist-info/WHEEL"); assert_snapshot!(fs_err::read_to_string(wheel_file).unwrap(), @r###" Wheel-Version: 1.0 Generator: uv 1.0.0+test Root-Is-Purelib: true Tag: py3-none-any "###); } } uv-0.9.17+ds1/crates/uv-build-frontend/000077500000000000000000000000001520155276700175645ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-build-frontend/.gitignore000066400000000000000000000000211520155276700215450ustar00rootroot00000000000000downloads wheels uv-0.9.17+ds1/crates/uv-build-frontend/Cargo.toml000066400000000000000000000026701520155276700215210ustar00rootroot00000000000000[package] name = "uv-build-frontend" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-auth = { workspace = true } uv-cache-key = { workspace = true } uv-configuration = { workspace = true } uv-distribution = { workspace = true } uv-distribution-types = { workspace = true } uv-fs = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-static = { workspace = true } uv-types = { workspace = true } uv-virtualenv = { workspace = true } uv-warnings = { workspace = true } uv-workspace = { workspace = true } anstream = { workspace = true } fs-err = { workspace = true } indoc = { workspace = true } itertools = { workspace = true } owo-colors = { workspace = true } regex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } toml_edit = { workspace = true } tracing = { workspace = true } rustc-hash = { workspace = true } [dev-dependencies] insta = { workspace = true } uv-0.9.17+ds1/crates/uv-build-frontend/README.md000066400000000000000000000010451520155276700210430ustar00rootroot00000000000000 # uv-build-frontend This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-build-frontend). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-build-frontend/src/000077500000000000000000000000001520155276700203535ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-build-frontend/src/error.rs000066400000000000000000000652211520155276700220600ustar00rootroot00000000000000use std::env; use std::fmt::{Display, Formatter}; use std::io; use std::path::PathBuf; use std::process::ExitStatus; use std::sync::LazyLock; use crate::PythonRunnerOutput; use owo_colors::OwoColorize; use regex::Regex; use thiserror::Error; use tracing::error; use uv_configuration::BuildOutput; use uv_distribution_types::IsBuildBackendError; use uv_fs::Simplified; use uv_normalize::PackageName; use uv_pep440::Version; use uv_types::AnyErrorBuild; /// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory` static MISSING_HEADER_RE_GCC: LazyLock = LazyLock::new(|| { Regex::new( r".*\.(?:c|c..|h|h..):\d+:\d+: fatal error: (.*\.(?:h|h..)): No such file or directory", ) .unwrap() }); /// e.g. `pygraphviz/graphviz_wrap.c:3023:10: fatal error: 'graphviz/cgraph.h' file not found` static MISSING_HEADER_RE_CLANG: LazyLock = LazyLock::new(|| { Regex::new(r".*\.(?:c|c..|h|h..):\d+:\d+: fatal error: '(.*\.(?:h|h..))' file not found") .unwrap() }); /// e.g. `pygraphviz/graphviz_wrap.c(3023): fatal error C1083: Cannot open include file: 'graphviz/cgraph.h': No such file or directory` static MISSING_HEADER_RE_MSVC: LazyLock = LazyLock::new(|| { Regex::new(r".*\.(?:c|c..|h|h..)\(\d+\): fatal error C1083: Cannot open include file: '(.*\.(?:h|h..))': No such file or directory") .unwrap() }); /// e.g. `/usr/bin/ld: cannot find -lncurses: No such file or directory` static LD_NOT_FOUND_RE: LazyLock = LazyLock::new(|| { Regex::new(r"/usr/bin/ld: cannot find -l([a-zA-Z10-9]+): No such file or directory").unwrap() }); /// e.g. `error: invalid command 'bdist_wheel'` static WHEEL_NOT_FOUND_RE: LazyLock = LazyLock::new(|| Regex::new(r"error: invalid command 'bdist_wheel'").unwrap()); /// e.g. `ModuleNotFoundError` static MODULE_NOT_FOUND: LazyLock = LazyLock::new(|| { Regex::new("ModuleNotFoundError: No module named ['\"]([^'\"]+)['\"]").unwrap() }); /// e.g. `ModuleNotFoundError: No module named 'distutils'` static DISTUTILS_NOT_FOUND_RE: LazyLock = LazyLock::new(|| Regex::new(r"ModuleNotFoundError: No module named 'distutils'").unwrap()); #[derive(Error, Debug)] pub enum Error { #[error(transparent)] Io(#[from] io::Error), #[error(transparent)] Lowering(#[from] uv_distribution::MetadataError), #[error("{} does not appear to be a Python project, as neither `pyproject.toml` nor `setup.py` are present in the directory", _0.simplified_display())] InvalidSourceDist(PathBuf), #[error("Invalid `pyproject.toml`")] InvalidPyprojectTomlSyntax(#[from] toml_edit::TomlError), #[error( "`pyproject.toml` does not match the required schema. When the `[project]` table is present, `project.name` must be present and non-empty." )] InvalidPyprojectTomlSchema(#[from] toml_edit::de::Error), #[error("Failed to resolve requirements from {0}")] RequirementsResolve(&'static str, #[source] AnyErrorBuild), #[error("Failed to install requirements from {0}")] RequirementsInstall(&'static str, #[source] AnyErrorBuild), #[error("Failed to create temporary virtualenv")] Virtualenv(#[from] uv_virtualenv::Error), // Build backend errors #[error("Failed to run `{0}`")] CommandFailed(PathBuf, #[source] io::Error), #[error("The build backend returned an error")] BuildBackend(#[from] BuildBackendError), #[error("The build backend returned an error")] MissingHeader(#[from] MissingHeaderError), #[error("Failed to build PATH for build script")] BuildScriptPath(#[source] env::JoinPathsError), // For the convenience of typing `setup_build` properly. #[error("Building source distributions for `{0}` is disabled")] NoSourceDistBuild(PackageName), #[error("Building source distributions is disabled")] NoSourceDistBuilds, #[error("Cyclic build dependency detected for `{0}`")] CyclicBuildDependency(PackageName), #[error( "Extra build requirement `{0}` was declared with `match-runtime = true`, but `{1}` does not declare static metadata, making runtime-matching impossible" )] UnmatchedRuntime(PackageName, PackageName), } impl IsBuildBackendError for Error { fn is_build_backend_error(&self) -> bool { match self { Self::Io(_) | Self::Lowering(_) | Self::InvalidSourceDist(_) | Self::InvalidPyprojectTomlSyntax(_) | Self::InvalidPyprojectTomlSchema(_) | Self::RequirementsResolve(_, _) | Self::RequirementsInstall(_, _) | Self::Virtualenv(_) | Self::NoSourceDistBuild(_) | Self::NoSourceDistBuilds | Self::CyclicBuildDependency(_) | Self::UnmatchedRuntime(_, _) => false, Self::CommandFailed(_, _) | Self::BuildBackend(_) | Self::MissingHeader(_) | Self::BuildScriptPath(_) => true, } } } #[derive(Debug)] enum MissingLibrary { Header(String), Linker(String), BuildDependency(String), DeprecatedModule(String, Version), } #[derive(Debug, Error)] pub struct MissingHeaderCause { missing_library: MissingLibrary, package_name: Option, package_version: Option, version_id: Option, } /// Extract the package name from a version specifier string. /// Uses PEP 508 naming rules but more lenient for hinting purposes. fn extract_package_name(version_id: &str) -> &str { // https://peps.python.org/pep-0508/#names // ^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$ with re.IGNORECASE // Since we're only using this for a hint, we're more lenient than what we would be doing if this was used for parsing let end = version_id .char_indices() .take_while(|(_, char)| matches!(char, 'A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '-' | '_')) .last() .map_or(0, |(i, c)| i + c.len_utf8()); if end == 0 { version_id } else { &version_id[..end] } } /// Write a hint about missing build dependencies. fn hint_build_dependency( f: &mut std::fmt::Formatter<'_>, display_name: &str, package_name: &str, package: &str, ) -> std::fmt::Result { let table_key = if package_name.contains('.') { format!("\"{package_name}\"") } else { package_name.to_string() }; write!( f, "This error likely indicates that `{}` depends on `{}`, but doesn't declare it as a build dependency. \ If `{}` is a first-party package, consider adding `{}` to its `{}`. \ Otherwise, either add it to your `pyproject.toml` under:\n\ \n\ [tool.uv.extra-build-dependencies]\n\ {} = [\"{}\"]\n\ \n\ or `{}` into the environment and re-run with `{}`.", display_name.cyan(), package.cyan(), package_name.cyan(), package.cyan(), "build-system.requires".green(), table_key.cyan(), package.cyan(), format!("uv pip install {package}").green(), "--no-build-isolation".green(), ) } impl Display for MissingHeaderCause { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self.missing_library { MissingLibrary::Header(header) => { if let (Some(package_name), Some(package_version)) = (&self.package_name, &self.package_version) { write!( f, "This error likely indicates that you need to install a library that provides \"{}\" for `{}`", header.cyan(), format!("{package_name}@{package_version}").cyan(), ) } else if let Some(version_id) = &self.version_id { write!( f, "This error likely indicates that you need to install a library that provides \"{}\" for `{}`", header.cyan(), version_id.cyan(), ) } else { write!( f, "This error likely indicates that you need to install a library that provides \"{}\"", header.cyan(), ) } } MissingLibrary::Linker(library) => { if let (Some(package_name), Some(package_version)) = (&self.package_name, &self.package_version) { write!( f, "This error likely indicates that you need to install the library that provides a shared library for `{}` for `{}` (e.g., `{}`)", library.cyan(), format!("{package_name}@{package_version}").cyan(), format!("lib{library}-dev").cyan(), ) } else if let Some(version_id) = &self.version_id { write!( f, "This error likely indicates that you need to install the library that provides a shared library for `{}` for `{}` (e.g., `{}`)", library.cyan(), version_id.cyan(), format!("lib{library}-dev").cyan(), ) } else { write!( f, "This error likely indicates that you need to install the library that provides a shared library for `{}` (e.g., `{}`)", library.cyan(), format!("lib{library}-dev").cyan(), ) } } MissingLibrary::BuildDependency(package) => { if let (Some(package_name), Some(package_version)) = (&self.package_name, &self.package_version) { hint_build_dependency( f, &format!("{package_name}@{package_version}"), package_name.as_str(), package, ) } else if let Some(version_id) = &self.version_id { let package_name = extract_package_name(version_id); hint_build_dependency(f, package_name, package_name, package) } else { write!( f, "This error likely indicates that a package depends on `{}`, but doesn't declare it as a build dependency. If the package is a first-party package, consider adding `{}` to its `{}`. Otherwise, `{}` into the environment and re-run with `{}`.", package.cyan(), package.cyan(), "build-system.requires".green(), format!("uv pip install {package}").green(), "--no-build-isolation".green(), ) } } MissingLibrary::DeprecatedModule(package, version) => { if let (Some(package_name), Some(package_version)) = (&self.package_name, &self.package_version) { write!( f, "`{}` was removed from the standard library in Python {version}. Consider adding a constraint (like `{}`) to avoid building a version of `{}` that depends on `{}`.", package.cyan(), format!("{package_name} >{package_version}").green(), package_name.cyan(), package.cyan(), ) } else { write!( f, "`{}` was removed from the standard library in Python {version}. Consider adding a constraint to avoid building a package that depends on `{}`.", package.cyan(), package.cyan(), ) } } } } } #[derive(Debug, Error)] pub struct BuildBackendError { message: String, exit_code: ExitStatus, stdout: Vec, stderr: Vec, } impl Display for BuildBackendError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{} ({})", self.message, self.exit_code)?; let mut non_empty = false; if self.stdout.iter().any(|line| !line.trim().is_empty()) { write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout.join("\n"))?; non_empty = true; } if self.stderr.iter().any(|line| !line.trim().is_empty()) { write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr.join("\n"))?; non_empty = true; } if non_empty { writeln!(f)?; } write!( f, "\n{}{} This usually indicates a problem with the package or the build environment.", "hint".bold().cyan(), ":".bold() )?; Ok(()) } } #[derive(Debug, Error)] pub struct MissingHeaderError { message: String, exit_code: ExitStatus, stdout: Vec, stderr: Vec, cause: MissingHeaderCause, } impl Display for MissingHeaderError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{} ({})", self.message, self.exit_code)?; if self.stdout.iter().any(|line| !line.trim().is_empty()) { write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout.join("\n"))?; } if self.stderr.iter().any(|line| !line.trim().is_empty()) { write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr.join("\n"))?; } write!( f, "\n\n{}{} {}", "hint".bold().cyan(), ":".bold(), self.cause )?; Ok(()) } } impl Error { /// Construct an [`Error`] from the output of a failed command. pub(crate) fn from_command_output( message: String, output: &PythonRunnerOutput, level: BuildOutput, name: Option<&PackageName>, version: Option<&Version>, version_id: Option<&str>, ) -> Self { // In the cases I've seen it was the 5th and 3rd last line (see test case), 10 seems like a reasonable cutoff. let missing_library = output.stderr.iter().rev().take(10).find_map(|line| { if let Some((_, [header])) = MISSING_HEADER_RE_GCC .captures(line.trim()) .or(MISSING_HEADER_RE_CLANG.captures(line.trim())) .or(MISSING_HEADER_RE_MSVC.captures(line.trim())) .map(|c| c.extract()) { Some(MissingLibrary::Header(header.to_string())) } else if let Some((_, [library])) = LD_NOT_FOUND_RE.captures(line.trim()).map(|c| c.extract()) { Some(MissingLibrary::Linker(library.to_string())) } else if WHEEL_NOT_FOUND_RE.is_match(line.trim()) { Some(MissingLibrary::BuildDependency("wheel".to_string())) } else if DISTUTILS_NOT_FOUND_RE.is_match(line.trim()) { Some(MissingLibrary::DeprecatedModule( "distutils".to_string(), Version::new([3, 12]), )) } else if let Some(caps) = MODULE_NOT_FOUND.captures(line.trim()) { if let Some(module_match) = caps.get(1) { let module_name = module_match.as_str(); let package_name = match crate::pipreqs::MODULE_MAPPING.lookup(module_name) { Some(package) => package.to_string(), None => module_name.to_string(), }; Some(MissingLibrary::BuildDependency(package_name)) } else { None } } else { None } }); if let Some(missing_library) = missing_library { return match level { BuildOutput::Stderr | BuildOutput::Quiet => { Self::MissingHeader(MissingHeaderError { message, exit_code: output.status, stdout: vec![], stderr: vec![], cause: MissingHeaderCause { missing_library, package_name: name.cloned(), package_version: version.cloned(), version_id: version_id.map(ToString::to_string), }, }) } BuildOutput::Debug => Self::MissingHeader(MissingHeaderError { message, exit_code: output.status, stdout: output.stdout.clone(), stderr: output.stderr.clone(), cause: MissingHeaderCause { missing_library, package_name: name.cloned(), package_version: version.cloned(), version_id: version_id.map(ToString::to_string), }, }), }; } match level { BuildOutput::Stderr | BuildOutput::Quiet => Self::BuildBackend(BuildBackendError { message, exit_code: output.status, stdout: vec![], stderr: vec![], }), BuildOutput::Debug => Self::BuildBackend(BuildBackendError { message, exit_code: output.status, stdout: output.stdout.clone(), stderr: output.stderr.clone(), }), } } } #[cfg(test)] mod test { use crate::{Error, PythonRunnerOutput}; use indoc::indoc; use std::process::ExitStatus; use std::str::FromStr; use uv_configuration::BuildOutput; use uv_normalize::PackageName; use uv_pep440::Version; #[test] fn missing_header() { let output = PythonRunnerOutput { status: ExitStatus::default(), // This is wrong but `from_raw` is platform-gated. stdout: indoc!(r" running bdist_wheel running build [...] creating build/temp.linux-x86_64-cpython-39/pygraphviz gcc -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -DOPENSSL_NO_SSL3 -fPIC -DSWIG_PYTHON_STRICT_BYTE_CHAR -I/tmp/.tmpy6vVes/.venv/include -I/home/konsti/.pyenv/versions/3.9.18/include/python3.9 -c pygraphviz/graphviz_wrap.c -o build/temp.linux-x86_64-cpython-39/pygraphviz/graphviz_wrap.o " ).lines().map(ToString::to_string).collect(), stderr: indoc!(r#" warning: no files found matching '*.png' under directory 'doc' warning: no files found matching '*.txt' under directory 'doc' [...] no previously-included directories found matching 'doc/build' pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory 3020 | #include "graphviz/cgraph.h" | ^~~~~~~~~~~~~~~~~~~ compilation terminated. error: command '/usr/bin/gcc' failed with exit code 1 "# ).lines().map(ToString::to_string).collect(), }; let err = Error::from_command_output( "Failed building wheel through setup.py".to_string(), &output, BuildOutput::Debug, None, None, Some("pygraphviz-1.11"), ); assert!(matches!(err, Error::MissingHeader { .. })); // Unix uses exit status, Windows uses exit code. let formatted = std::error::Error::source(&err) .unwrap() .to_string() .replace("exit status: ", "exit code: "); let formatted = anstream::adapter::strip_str(&formatted); insta::assert_snapshot!(formatted, @r###" Failed building wheel through setup.py (exit code: 0) [stdout] running bdist_wheel running build [...] creating build/temp.linux-x86_64-cpython-39/pygraphviz gcc -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -DOPENSSL_NO_SSL3 -fPIC -DSWIG_PYTHON_STRICT_BYTE_CHAR -I/tmp/.tmpy6vVes/.venv/include -I/home/konsti/.pyenv/versions/3.9.18/include/python3.9 -c pygraphviz/graphviz_wrap.c -o build/temp.linux-x86_64-cpython-39/pygraphviz/graphviz_wrap.o [stderr] warning: no files found matching '*.png' under directory 'doc' warning: no files found matching '*.txt' under directory 'doc' [...] no previously-included directories found matching 'doc/build' pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory 3020 | #include "graphviz/cgraph.h" | ^~~~~~~~~~~~~~~~~~~ compilation terminated. error: command '/usr/bin/gcc' failed with exit code 1 hint: This error likely indicates that you need to install a library that provides "graphviz/cgraph.h" for `pygraphviz-1.11` "###); } #[test] fn missing_linker_library() { let output = PythonRunnerOutput { status: ExitStatus::default(), // This is wrong but `from_raw` is platform-gated. stdout: Vec::new(), stderr: indoc!( r" 1099 | n = strlen(p); | ^~~~~~~~~ /usr/bin/ld: cannot find -lncurses: No such file or directory collect2: error: ld returned 1 exit status error: command '/usr/bin/x86_64-linux-gnu-gcc' failed with exit code 1" ) .lines() .map(ToString::to_string) .collect(), }; let err = Error::from_command_output( "Failed building wheel through setup.py".to_string(), &output, BuildOutput::Debug, None, None, Some("pygraphviz-1.11"), ); assert!(matches!(err, Error::MissingHeader { .. })); // Unix uses exit status, Windows uses exit code. let formatted = std::error::Error::source(&err) .unwrap() .to_string() .replace("exit status: ", "exit code: "); let formatted = anstream::adapter::strip_str(&formatted); insta::assert_snapshot!(formatted, @r###" Failed building wheel through setup.py (exit code: 0) [stderr] 1099 | n = strlen(p); | ^~~~~~~~~ /usr/bin/ld: cannot find -lncurses: No such file or directory collect2: error: ld returned 1 exit status error: command '/usr/bin/x86_64-linux-gnu-gcc' failed with exit code 1 hint: This error likely indicates that you need to install the library that provides a shared library for `ncurses` for `pygraphviz-1.11` (e.g., `libncurses-dev`) "###); } #[test] fn missing_wheel_package() { let output = PythonRunnerOutput { status: ExitStatus::default(), // This is wrong but `from_raw` is platform-gated. stdout: Vec::new(), stderr: indoc!( r" usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...] or: setup.py --help [cmd1 cmd2 ...] or: setup.py --help-commands or: setup.py cmd --help error: invalid command 'bdist_wheel'" ) .lines() .map(ToString::to_string) .collect(), }; let err = Error::from_command_output( "Failed building wheel through setup.py".to_string(), &output, BuildOutput::Debug, None, None, Some("pygraphviz-1.11"), ); assert!(matches!(err, Error::MissingHeader { .. })); // Unix uses exit status, Windows uses exit code. let formatted = std::error::Error::source(&err) .unwrap() .to_string() .replace("exit status: ", "exit code: "); let formatted = anstream::adapter::strip_str(&formatted); insta::assert_snapshot!(formatted, @r#" Failed building wheel through setup.py (exit code: 0) [stderr] usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...] or: setup.py --help [cmd1 cmd2 ...] or: setup.py --help-commands or: setup.py cmd --help error: invalid command 'bdist_wheel' hint: This error likely indicates that `pygraphviz-1.11` depends on `wheel`, but doesn't declare it as a build dependency. If `pygraphviz-1.11` is a first-party package, consider adding `wheel` to its `build-system.requires`. Otherwise, either add it to your `pyproject.toml` under: [tool.uv.extra-build-dependencies] "pygraphviz-1.11" = ["wheel"] or `uv pip install wheel` into the environment and re-run with `--no-build-isolation`. "#); } #[test] fn missing_distutils() { let output = PythonRunnerOutput { status: ExitStatus::default(), // This is wrong but `from_raw` is platform-gated. stdout: Vec::new(), stderr: indoc!( r" import distutils.core ModuleNotFoundError: No module named 'distutils' " ) .lines() .map(ToString::to_string) .collect(), }; let err = Error::from_command_output( "Failed building wheel through setup.py".to_string(), &output, BuildOutput::Debug, Some(&PackageName::from_str("pygraphviz").unwrap()), Some(&Version::new([1, 11])), Some("pygraphviz-1.11"), ); assert!(matches!(err, Error::MissingHeader { .. })); // Unix uses exit status, Windows uses exit code. let formatted = std::error::Error::source(&err) .unwrap() .to_string() .replace("exit status: ", "exit code: "); let formatted = anstream::adapter::strip_str(&formatted); insta::assert_snapshot!(formatted, @r###" Failed building wheel through setup.py (exit code: 0) [stderr] import distutils.core ModuleNotFoundError: No module named 'distutils' hint: `distutils` was removed from the standard library in Python 3.12. Consider adding a constraint (like `pygraphviz >1.11`) to avoid building a version of `pygraphviz` that depends on `distutils`. "###); } } uv-0.9.17+ds1/crates/uv-build-frontend/src/lib.rs000066400000000000000000001362141520155276700214760ustar00rootroot00000000000000//! Build wheels from source distributions. //! //! mod error; mod pipreqs; use std::borrow::Cow; use std::ffi::OsString; use std::fmt::Formatter; use std::fmt::Write; use std::io; use std::path::{Path, PathBuf}; use std::process::ExitStatus; use std::rc::Rc; use std::str::FromStr; use std::sync::LazyLock; use std::{env, iter}; use fs_err as fs; use indoc::formatdoc; use itertools::Itertools; use rustc_hash::FxHashMap; use serde::de::{self, IntoDeserializer, SeqAccess, Visitor, value}; use serde::{Deserialize, Deserializer}; use tempfile::TempDir; use tokio::io::AsyncBufReadExt; use tokio::process::Command; use tokio::sync::{Mutex, Semaphore}; use tracing::{Instrument, debug, info_span, instrument, warn}; use uv_auth::CredentialsCache; use uv_cache_key::cache_digest; use uv_configuration::{BuildKind, BuildOutput, SourceStrategy}; use uv_distribution::BuildRequires; use uv_distribution_types::{ ConfigSettings, ExtraBuildRequirement, ExtraBuildRequires, IndexLocations, Requirement, Resolution, }; use uv_fs::{LockedFile, LockedFileMode}; use uv_fs::{PythonExt, Simplified}; use uv_normalize::PackageName; use uv_pep440::Version; use uv_preview::Preview; use uv_pypi_types::VerbatimParsedUrl; use uv_python::{Interpreter, PythonEnvironment}; use uv_static::EnvVars; use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, SourceBuildTrait}; use uv_warnings::warn_user_once; use uv_workspace::WorkspaceCache; pub use crate::error::{Error, MissingHeaderCause}; /// The default backend to use when PEP 517 is used without a `build-system` section. static DEFAULT_BACKEND: LazyLock = LazyLock::new(|| Pep517Backend { backend: "setuptools.build_meta:__legacy__".to_string(), backend_path: None, requirements: vec![Requirement::from( uv_pep508::Requirement::from_str("setuptools >= 40.8.0").unwrap(), )], }); /// A `pyproject.toml` as specified in PEP 517. #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] struct PyProjectToml { /// Build-related data build_system: Option, /// Project metadata project: Option, /// Tool configuration tool: Option, } /// The `[project]` section of a pyproject.toml as specified in PEP 621. /// /// This representation only includes a subset of the fields defined in PEP 621 necessary for /// informing wheel builds. #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] struct Project { /// The name of the project name: PackageName, /// The version of the project as supported by PEP 440 version: Option, /// Specifies which fields listed by PEP 621 were intentionally unspecified so another tool /// can/will provide such metadata dynamically. dynamic: Option>, } /// The `[build-system]` section of a pyproject.toml as specified in PEP 517. #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] struct BuildSystem { /// PEP 508 dependencies required to execute the build system. requires: Vec>, /// A string naming a Python object that will be used to perform the build. build_backend: Option, /// Specify that their backend code is hosted in-tree, this key contains a list of directories. backend_path: Option, } #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] struct Tool { uv: Option, } #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] struct ToolUv { workspace: Option, } impl BackendPath { /// Return an iterator over the paths in the backend path. fn iter(&self) -> impl Iterator { self.0.iter().map(String::as_str) } } #[derive(Debug, Clone, PartialEq, Eq)] struct BackendPath(Vec); impl<'de> Deserialize<'de> for BackendPath { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct StringOrVec; impl<'de> Visitor<'de> for StringOrVec { type Value = Vec; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { formatter.write_str("list of strings") } fn visit_str(self, s: &str) -> Result where E: de::Error, { // Allow exactly `backend-path = "."`, as used in `flit_core==2.3.0`. if s == "." { Ok(vec![".".to_string()]) } else { Err(de::Error::invalid_value(de::Unexpected::Str(s), &self)) } } fn visit_seq(self, seq: S) -> Result where S: SeqAccess<'de>, { Deserialize::deserialize(value::SeqAccessDeserializer::new(seq)) } } deserializer.deserialize_any(StringOrVec).map(BackendPath) } } /// `[build-backend]` from pyproject.toml #[derive(Debug, Clone, PartialEq, Eq)] struct Pep517Backend { /// The build backend string such as `setuptools.build_meta:__legacy__` or `maturin` from /// `build-backend.backend` in pyproject.toml /// /// backend: String, /// `build-backend.requirements` in pyproject.toml requirements: Vec, /// backend_path: Option, } impl Pep517Backend { fn backend_import(&self) -> String { let import = if let Some((path, object)) = self.backend.split_once(':') { format!("from {path} import {object} as backend") } else { format!("import {} as backend", self.backend) }; let backend_path_encoded = self .backend_path .iter() .flat_map(BackendPath::iter) .map(|path| { // Turn into properly escaped python string '"'.to_string() + &path.replace('\\', "\\\\").replace('"', "\\\"") + &'"'.to_string() }) .join(", "); // > Projects can specify that their backend code is hosted in-tree by including the // > backend-path key in pyproject.toml. This key contains a list of directories, which the // > frontend will add to the start of sys.path when loading the backend, and running the // > backend hooks. formatdoc! {r#" import sys if sys.path[0] == "": sys.path.pop(0) sys.path = [{backend_path}] + sys.path {import} "#, backend_path = backend_path_encoded} } fn is_setuptools(&self) -> bool { // either `setuptools.build_meta` or `setuptools.build_meta:__legacy__` self.backend.split(':').next() == Some("setuptools.build_meta") } } /// Uses an [`Rc`] internally, clone freely. #[derive(Debug, Default, Clone)] pub struct SourceBuildContext { /// An in-memory resolution of the default backend's requirements for PEP 517 builds. default_resolution: Rc>>, } /// Holds the state through a series of PEP 517 frontend to backend calls or a single `setup.py` /// invocation. /// /// This keeps both the temp dir and the result of a potential `prepare_metadata_for_build_wheel` /// call which changes how we call `build_wheel`. pub struct SourceBuild { temp_dir: TempDir, source_tree: PathBuf, config_settings: ConfigSettings, /// If performing a PEP 517 build, the backend to use. pep517_backend: Pep517Backend, /// The PEP 621 project metadata, if any. project: Option, /// The virtual environment in which to build the source distribution. venv: PythonEnvironment, /// Populated if `prepare_metadata_for_build_wheel` was called. /// /// > If the build frontend has previously called `prepare_metadata_for_build_wheel` and depends /// > on the wheel resulting from this call to have metadata matching this earlier call, then /// > it should provide the path to the created .dist-info directory as the `metadata_directory` /// > argument. If this argument is provided, then `build_wheel` MUST produce a wheel with /// > identical metadata. The directory passed in by the build frontend MUST be identical to the /// > directory created by `prepare_metadata_for_build_wheel`, including any unrecognized files /// > it created. metadata_directory: Option, /// The name of the package, if known. package_name: Option, /// The version of the package, if known. package_version: Option, /// Distribution identifier, e.g., `foo-1.2.3`. Used for error reporting if the name and /// version are unknown. version_id: Option, /// Whether we do a regular PEP 517 build or an PEP 660 editable build build_kind: BuildKind, /// Whether to send build output to `stderr` or `tracing`, etc. level: BuildOutput, /// Modified PATH that contains the `venv_bin`, `user_path` and `system_path` variables in that order modified_path: OsString, /// Environment variables to be passed in during metadata or wheel building environment_variables: FxHashMap, /// Runner for Python scripts. runner: PythonRunner, } impl SourceBuild { /// Create a virtual environment in which to build a source distribution, extracting the /// contents from an archive if necessary. /// /// `source_dist` is for error reporting only. pub async fn setup( source: &Path, subdirectory: Option<&Path>, install_path: &Path, fallback_package_name: Option<&PackageName>, fallback_package_version: Option<&Version>, interpreter: &Interpreter, build_context: &impl BuildContext, source_build_context: SourceBuildContext, version_id: Option<&str>, locations: &IndexLocations, source_strategy: SourceStrategy, workspace_cache: &WorkspaceCache, config_settings: ConfigSettings, build_isolation: BuildIsolation<'_>, extra_build_requires: &ExtraBuildRequires, build_stack: &BuildStack, build_kind: BuildKind, mut environment_variables: FxHashMap, level: BuildOutput, concurrent_builds: usize, credentials_cache: &CredentialsCache, preview: Preview, ) -> Result { let temp_dir = build_context.cache().venv_dir()?; let source_tree = if let Some(subdir) = subdirectory { source.join(subdir) } else { source.to_path_buf() }; // Check if we have a PEP 517 build backend. let (pep517_backend, project) = Self::extract_pep517_backend( &source_tree, install_path, fallback_package_name, locations, source_strategy, workspace_cache, credentials_cache, ) .await .map_err(|err| *err)?; let package_name = project .as_ref() .map(|project| &project.name) .or(fallback_package_name) .cloned(); let package_version = project .as_ref() .and_then(|project| project.version.as_ref()) .or(fallback_package_version) .cloned(); let extra_build_dependencies = package_name .as_ref() .and_then(|name| extra_build_requires.get(name).cloned()) .unwrap_or_default() .into_iter() .map(|requirement| { match requirement { ExtraBuildRequirement { requirement, match_runtime: true, } if requirement.source.is_empty() => { Err(Error::UnmatchedRuntime( requirement.name.clone(), // SAFETY: if `package_name` is `None`, the iterator is empty. package_name.clone().unwrap(), )) } requirement => Ok(requirement), } }) .map_ok(Requirement::from) .collect::, _>>()?; // Create a virtual environment, or install into the shared environment if requested. let venv = if let Some(venv) = build_isolation.shared_environment(package_name.as_ref()) { venv.clone() } else { uv_virtualenv::create_venv( temp_dir.path(), interpreter.clone(), uv_virtualenv::Prompt::None, false, uv_virtualenv::OnExisting::Remove( uv_virtualenv::RemovalReason::TemporaryEnvironment, ), false, false, false, preview, )? }; // Set up the build environment. If build isolation is disabled, we assume the build // environment is already setup. if build_isolation.is_isolated(package_name.as_ref()) { debug!("Resolving build requirements"); let dependency_sources = if extra_build_dependencies.is_empty() { "`build-system.requires`" } else { "`build-system.requires` and `extra-build-dependencies`" }; let resolved_requirements = Self::get_resolved_requirements( build_context, source_build_context, &pep517_backend, extra_build_dependencies, build_stack, ) .await?; build_context .install(&resolved_requirements, &venv, build_stack) .await .map_err(|err| Error::RequirementsInstall(dependency_sources, err.into()))?; } else { debug!("Proceeding without build isolation"); } // Figure out what the modified path should be, and remove the PATH variable from the // environment variables if it's there. let user_path = environment_variables.remove(&OsString::from(EnvVars::PATH)); // See if there is an OS PATH variable. let os_path = env::var_os(EnvVars::PATH); // Prepend the user supplied PATH to the existing OS PATH let modified_path = if let Some(user_path) = user_path { match os_path { // Prepend the user supplied PATH to the existing PATH Some(env_path) => { let user_path = PathBuf::from(user_path); let new_path = env::split_paths(&user_path).chain(env::split_paths(&env_path)); Some(env::join_paths(new_path).map_err(Error::BuildScriptPath)?) } // Use the user supplied PATH None => Some(user_path), } } else { os_path }; // Prepend the venv bin directory to the modified path let modified_path = if let Some(path) = modified_path { let venv_path = iter::once(venv.scripts().to_path_buf()).chain(env::split_paths(&path)); env::join_paths(venv_path).map_err(Error::BuildScriptPath)? } else { OsString::from(venv.scripts()) }; // Create the PEP 517 build environment. If build isolation is disabled, we assume the build // environment is already setup. let runner = PythonRunner::new(concurrent_builds, level); if build_isolation.is_isolated(package_name.as_ref()) { debug!("Creating PEP 517 build environment"); create_pep517_build_environment( &runner, &source_tree, install_path, &venv, &pep517_backend, build_context, package_name.as_ref(), package_version.as_ref(), version_id, locations, source_strategy, workspace_cache, build_stack, build_kind, level, &config_settings, &environment_variables, &modified_path, &temp_dir, credentials_cache, ) .await?; } Ok(Self { temp_dir, source_tree, pep517_backend, project, venv, build_kind, level, config_settings, metadata_directory: None, package_name, package_version, version_id: version_id.map(ToString::to_string), environment_variables, modified_path, runner, }) } /// Acquire a lock on the source tree, if necessary. async fn acquire_lock(&self) -> Result, Error> { // Depending on the command, setuptools puts `*.egg-info`, `build/`, and `dist/` in the // source tree, and concurrent invocations of setuptools using the same source dir can // stomp on each other. We need to lock something to fix that, but we don't want to dump a // `.lock` file into the source tree that the user will need to .gitignore. Take a global // proxy lock instead. let mut source_tree_lock = None; if self.pep517_backend.is_setuptools() { debug!("Locking the source tree for setuptools"); let canonical_source_path = self.source_tree.canonicalize()?; let lock_path = env::temp_dir().join(format!( "uv-setuptools-{}.lock", cache_digest(&canonical_source_path) )); source_tree_lock = LockedFile::acquire( lock_path, LockedFileMode::Exclusive, self.source_tree.to_string_lossy(), ) .await .inspect_err(|err| { warn!("Failed to acquire build lock: {err}"); }) .ok(); } Ok(source_tree_lock) } async fn get_resolved_requirements( build_context: &impl BuildContext, source_build_context: SourceBuildContext, pep517_backend: &Pep517Backend, extra_build_dependencies: Vec, build_stack: &BuildStack, ) -> Result { Ok( if pep517_backend.requirements == DEFAULT_BACKEND.requirements && extra_build_dependencies.is_empty() { let mut resolution = source_build_context.default_resolution.lock().await; if let Some(resolved_requirements) = &*resolution { resolved_requirements.clone() } else { let resolved_requirements = build_context .resolve(&DEFAULT_BACKEND.requirements, build_stack) .await .map_err(|err| { Error::RequirementsResolve("`setup.py` build", err.into()) })?; *resolution = Some(resolved_requirements.clone()); resolved_requirements } } else { let (requirements, dependency_sources) = if extra_build_dependencies.is_empty() { ( Cow::Borrowed(&pep517_backend.requirements), "`build-system.requires`", ) } else { // If there are extra build dependencies, we need to resolve them together with // the backend requirements. let mut requirements = pep517_backend.requirements.clone(); requirements.extend(extra_build_dependencies); ( Cow::Owned(requirements), "`build-system.requires` and `extra-build-dependencies`", ) }; build_context .resolve(&requirements, build_stack) .await .map_err(|err| Error::RequirementsResolve(dependency_sources, err.into()))? }, ) } /// Extract the PEP 517 backend from the `pyproject.toml` or `setup.py` file. async fn extract_pep517_backend( source_tree: &Path, install_path: &Path, package_name: Option<&PackageName>, locations: &IndexLocations, source_strategy: SourceStrategy, workspace_cache: &WorkspaceCache, credentials_cache: &CredentialsCache, ) -> Result<(Pep517Backend, Option), Box> { match fs::read_to_string(source_tree.join("pyproject.toml")) { Ok(toml) => { let pyproject_toml = toml_edit::Document::from_str(&toml) .map_err(Error::InvalidPyprojectTomlSyntax)?; let pyproject_toml = PyProjectToml::deserialize(pyproject_toml.into_deserializer()) .map_err(Error::InvalidPyprojectTomlSchema)?; let backend = if let Some(build_system) = pyproject_toml.build_system { // If necessary, lower the requirements. let requirements = match source_strategy { SourceStrategy::Enabled => { if let Some(name) = pyproject_toml .project .as_ref() .map(|project| &project.name) .or(package_name) { let build_requires = uv_pypi_types::BuildRequires { name: Some(name.clone()), requires_dist: build_system.requires, }; let build_requires = BuildRequires::from_project_maybe_workspace( build_requires, install_path, locations, source_strategy, workspace_cache, credentials_cache, ) .await .map_err(Error::Lowering)?; build_requires.requires_dist } else { build_system .requires .into_iter() .map(Requirement::from) .collect() } } SourceStrategy::Disabled => build_system .requires .into_iter() .map(Requirement::from) .collect(), }; Pep517Backend { // If `build-backend` is missing, inject the legacy setuptools backend, but // retain the `requires`, to match `pip` and `build`. Note that while PEP 517 // says that in this case we "should revert to the legacy behaviour of running // `setup.py` (either directly, or by implicitly invoking the // `setuptools.build_meta:__legacy__` backend)", we found that in practice, only // the legacy setuptools backend is allowed. See also: // https://github.com/pypa/build/blob/de5b44b0c28c598524832dff685a98d5a5148c44/src/build/__init__.py#L114-L118 backend: build_system .build_backend .unwrap_or_else(|| "setuptools.build_meta:__legacy__".to_string()), backend_path: build_system.backend_path, requirements, } } else { // If a `pyproject.toml` is present, but `[build-system]` is missing, proceed // with a PEP 517 build using the default backend (`setuptools`), to match `pip` // and `build`. // // If there is no build system defined and there is no metadata source for // `setuptools`, warn. The build will succeed, but the metadata will be // incomplete (for example, the package name will be `UNKNOWN`). if pyproject_toml.project.is_none() && !source_tree.join("setup.py").is_file() && !source_tree.join("setup.cfg").is_file() { // Give a specific hint for `uv pip install .` in a workspace root. let looks_like_workspace_root = pyproject_toml .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) .and_then(|tool| tool.workspace.as_ref()) .is_some(); if looks_like_workspace_root { warn_user_once!( "`{}` appears to be a workspace root without a Python project; \ consider using `uv sync` to install the workspace, or add a \ `[build-system]` table to `pyproject.toml`", source_tree.simplified_display().cyan(), ); } else { warn_user_once!( "`{}` does not appear to be a Python project, as the `pyproject.toml` \ does not include a `[build-system]` table, and neither `setup.py` \ nor `setup.cfg` are present in the directory", source_tree.simplified_display().cyan(), ); } } DEFAULT_BACKEND.clone() }; Ok((backend, pyproject_toml.project)) } Err(err) if err.kind() == io::ErrorKind::NotFound => { // We require either a `pyproject.toml` or a `setup.py` file at the top level. if !source_tree.join("setup.py").is_file() { return Err(Box::new(Error::InvalidSourceDist( source_tree.to_path_buf(), ))); } // If no `pyproject.toml` is present, by default, proceed with a PEP 517 build using // the default backend, to match `build`. `pip` uses `setup.py` directly in this // case, but plans to make PEP 517 builds the default in the future. // See: https://github.com/pypa/pip/issues/9175. Ok((DEFAULT_BACKEND.clone(), None)) } Err(err) => Err(Box::new(err.into())), } } /// Try calling `prepare_metadata_for_build_wheel` to get the metadata without executing the /// actual build. pub async fn get_metadata_without_build(&mut self) -> Result, Error> { // We've already called this method; return the existing result. if let Some(metadata_dir) = &self.metadata_directory { return Ok(Some(metadata_dir.clone())); } // Lock the source tree, if necessary. let _lock = self.acquire_lock().await?; // Hatch allows for highly dynamic customization of metadata via hooks. In such cases, Hatch // can't uphold the PEP 517 contract, in that the metadata Hatch would return by // `prepare_metadata_for_build_wheel` isn't guaranteed to match that of the built wheel. // // Hatch disables `prepare_metadata_for_build_wheel` entirely for pip. We'll instead disable // it on our end when metadata is defined as "dynamic" in the pyproject.toml, which should // allow us to leverage the hook in _most_ cases while still avoiding incorrect metadata for // the remaining cases. // // This heuristic will have false positives (i.e., there will be some Hatch projects for // which we could have safely called `prepare_metadata_for_build_wheel`, despite having // dynamic metadata). However, false positives are preferable to false negatives, since // this is just an optimization. // // See: https://github.com/astral-sh/uv/issues/2130 if self.pep517_backend.backend == "hatchling.build" { if self .project .as_ref() .and_then(|project| project.dynamic.as_ref()) .is_some_and(|dynamic| { dynamic .iter() .any(|field| field == "dependencies" || field == "optional-dependencies") }) { return Ok(None); } } let metadata_directory = self.temp_dir.path().join("metadata_directory"); fs::create_dir(&metadata_directory)?; // Write the hook output to a file so that we can read it back reliably. let outfile = self.temp_dir.path().join(format!( "prepare_metadata_for_build_{}.txt", self.build_kind )); debug!( "Calling `{}.prepare_metadata_for_build_{}()`", self.pep517_backend.backend, self.build_kind, ); let script = formatdoc! { r#" {} import json prepare_metadata_for_build = getattr(backend, "prepare_metadata_for_build_{}", None) if prepare_metadata_for_build: dirname = prepare_metadata_for_build("{}", {}) else: dirname = None with open("{}", "w") as fp: fp.write(dirname or "") "#, self.pep517_backend.backend_import(), self.build_kind, escape_path_for_python(&metadata_directory), self.config_settings.escape_for_python(), outfile.escape_for_python(), }; let span = info_span!( "run_python_script", script = format!("prepare_metadata_for_build_{}", self.build_kind), version_id = self.version_id, ); let output = self .runner .run_script( &self.venv, &script, &self.source_tree, &self.environment_variables, &self.modified_path, ) .instrument(span) .await?; if !output.status.success() { return Err(Error::from_command_output( format!( "Call to `{}.prepare_metadata_for_build_{}` failed", self.pep517_backend.backend, self.build_kind ), &output, self.level, self.package_name.as_ref(), self.package_version.as_ref(), self.version_id.as_deref(), )); } let dirname = fs::read_to_string(&outfile)?; if dirname.is_empty() { return Ok(None); } self.metadata_directory = Some(metadata_directory.join(dirname)); Ok(self.metadata_directory.clone()) } /// Build a distribution from an archive (`.zip` or `.tar.gz`) or source tree, and return the /// location of the built distribution. /// /// The location will be inside `temp_dir`, i.e., you must use the distribution before dropping /// the temporary directory. /// /// #[instrument(skip_all, fields(version_id = self.version_id))] pub async fn build(&self, wheel_dir: &Path) -> Result { // The build scripts run with the extracted root as cwd, so they need the absolute path. let wheel_dir = std::path::absolute(wheel_dir)?; let filename = self.pep517_build(&wheel_dir).await?; Ok(filename) } /// Perform a PEP 517 build for a wheel or source distribution (sdist). async fn pep517_build(&self, output_dir: &Path) -> Result { // Lock the source tree, if necessary. let _lock = self.acquire_lock().await?; // Write the hook output to a file so that we can read it back reliably. let outfile = self .temp_dir .path() .join(format!("build_{}.txt", self.build_kind)); // Construct the appropriate build script based on the build kind. let script = match self.build_kind { BuildKind::Sdist => { debug!( r#"Calling `{}.build_{}("{}", {})`"#, self.pep517_backend.backend, self.build_kind, output_dir.escape_for_python(), self.config_settings.escape_for_python(), ); formatdoc! { r#" {} sdist_filename = backend.build_{}("{}", {}) with open("{}", "w") as fp: fp.write(sdist_filename) "#, self.pep517_backend.backend_import(), self.build_kind, output_dir.escape_for_python(), self.config_settings.escape_for_python(), outfile.escape_for_python() } } BuildKind::Wheel | BuildKind::Editable => { let metadata_directory = self .metadata_directory .as_deref() .map_or("None".to_string(), |path| { format!(r#""{}""#, path.escape_for_python()) }); debug!( r#"Calling `{}.build_{}("{}", {}, {})`"#, self.pep517_backend.backend, self.build_kind, output_dir.escape_for_python(), self.config_settings.escape_for_python(), metadata_directory, ); formatdoc! { r#" {} wheel_filename = backend.build_{}("{}", {}, {}) with open("{}", "w") as fp: fp.write(wheel_filename) "#, self.pep517_backend.backend_import(), self.build_kind, output_dir.escape_for_python(), self.config_settings.escape_for_python(), metadata_directory, outfile.escape_for_python() } } }; let span = info_span!( "run_python_script", script = format!("build_{}", self.build_kind), version_id = self.version_id, ); let output = self .runner .run_script( &self.venv, &script, &self.source_tree, &self.environment_variables, &self.modified_path, ) .instrument(span) .await?; if !output.status.success() { return Err(Error::from_command_output( format!( "Call to `{}.build_{}` failed", self.pep517_backend.backend, self.build_kind ), &output, self.level, self.package_name.as_ref(), self.package_version.as_ref(), self.version_id.as_deref(), )); } let distribution_filename = fs::read_to_string(&outfile)?; if !output_dir.join(&distribution_filename).is_file() { return Err(Error::from_command_output( format!( "Call to `{}.build_{}` failed", self.pep517_backend.backend, self.build_kind ), &output, self.level, self.package_name.as_ref(), self.package_version.as_ref(), self.version_id.as_deref(), )); } Ok(distribution_filename) } } impl SourceBuildTrait for SourceBuild { async fn metadata(&mut self) -> Result, AnyErrorBuild> { Ok(self.get_metadata_without_build().await?) } async fn wheel<'a>(&'a self, wheel_dir: &'a Path) -> Result { Ok(self.build(wheel_dir).await?) } } fn escape_path_for_python(path: &Path) -> String { path.to_string_lossy() .replace('\\', "\\\\") .replace('"', "\\\"") } /// Not a method because we call it before the builder is completely initialized async fn create_pep517_build_environment( runner: &PythonRunner, source_tree: &Path, install_path: &Path, venv: &PythonEnvironment, pep517_backend: &Pep517Backend, build_context: &impl BuildContext, package_name: Option<&PackageName>, package_version: Option<&Version>, version_id: Option<&str>, locations: &IndexLocations, source_strategy: SourceStrategy, workspace_cache: &WorkspaceCache, build_stack: &BuildStack, build_kind: BuildKind, level: BuildOutput, config_settings: &ConfigSettings, environment_variables: &FxHashMap, modified_path: &OsString, temp_dir: &TempDir, credentials_cache: &CredentialsCache, ) -> Result<(), Error> { // Write the hook output to a file so that we can read it back reliably. let outfile = temp_dir .path() .join(format!("get_requires_for_build_{build_kind}.txt")); debug!( "Calling `{}.get_requires_for_build_{}()`", pep517_backend.backend, build_kind ); let script = formatdoc! { r#" {} import json get_requires_for_build = getattr(backend, "get_requires_for_build_{}", None) if get_requires_for_build: requires = get_requires_for_build({}) else: requires = [] with open("{}", "w") as fp: json.dump(requires, fp) "#, pep517_backend.backend_import(), build_kind, config_settings.escape_for_python(), outfile.escape_for_python() }; let span = info_span!( "run_python_script", script = format!("get_requires_for_build_{}", build_kind), version_id = version_id, ); let output = runner .run_script( venv, &script, source_tree, environment_variables, modified_path, ) .instrument(span) .await?; if !output.status.success() { return Err(Error::from_command_output( format!( "Call to `{}.build_{}` failed", pep517_backend.backend, build_kind ), &output, level, package_name, package_version, version_id, )); } // Read and deserialize the requirements from the output file. let read_requires_result = fs_err::read(&outfile) .map_err(|err| err.to_string()) .and_then(|contents| serde_json::from_slice(&contents).map_err(|err| err.to_string())); let extra_requires: Vec> = match read_requires_result { Ok(extra_requires) => extra_requires, Err(err) => { return Err(Error::from_command_output( format!( "Call to `{}.get_requires_for_build_{}` failed: {}", pep517_backend.backend, build_kind, err ), &output, level, package_name, package_version, version_id, )); } }; // If necessary, lower the requirements. let extra_requires = match source_strategy { SourceStrategy::Enabled => { let build_requires = uv_pypi_types::BuildRequires { name: package_name.cloned(), requires_dist: extra_requires, }; let build_requires = BuildRequires::from_project_maybe_workspace( build_requires, install_path, locations, source_strategy, workspace_cache, credentials_cache, ) .await .map_err(Error::Lowering)?; build_requires.requires_dist } SourceStrategy::Disabled => extra_requires.into_iter().map(Requirement::from).collect(), }; // Some packages (such as tqdm 4.66.1) list only extra requires that have already been part of // the pyproject.toml requires (in this case, `wheel`). We can skip doing the whole resolution // and installation again. // TODO(konstin): Do we still need this when we have a fast resolver? if extra_requires .iter() .any(|req| !pep517_backend.requirements.contains(req)) { debug!("Installing extra requirements for build backend"); let requirements: Vec<_> = pep517_backend .requirements .iter() .cloned() .chain(extra_requires) .collect(); let resolution = build_context .resolve(&requirements, build_stack) .await .map_err(|err| { Error::RequirementsResolve("`build-system.requires`", AnyErrorBuild::from(err)) })?; build_context .install(&resolution, venv, build_stack) .await .map_err(|err| { Error::RequirementsInstall("`build-system.requires`", AnyErrorBuild::from(err)) })?; } Ok(()) } /// A runner that manages the execution of external python processes with a /// concurrency limit. #[derive(Debug)] struct PythonRunner { control: Semaphore, level: BuildOutput, } #[derive(Debug)] struct PythonRunnerOutput { stdout: Vec, stderr: Vec, status: ExitStatus, } impl PythonRunner { /// Create a `PythonRunner` with the provided concurrency limit and output level. fn new(concurrency: usize, level: BuildOutput) -> Self { Self { control: Semaphore::new(concurrency), level, } } /// Spawn a process that runs a python script in the provided environment. /// /// If the concurrency limit has been reached this method will wait until a pending /// script completes before spawning this one. /// /// Note: It is the caller's responsibility to create an informative span. async fn run_script( &self, venv: &PythonEnvironment, script: &str, source_tree: &Path, environment_variables: &FxHashMap, modified_path: &OsString, ) -> Result { /// Read lines from a reader and store them in a buffer. async fn read_from( mut reader: tokio::io::Split>, mut printer: Printer, buffer: &mut Vec, ) -> io::Result<()> { loop { match reader.next_segment().await? { Some(line_buf) => { let line_buf = line_buf.strip_suffix(b"\r").unwrap_or(&line_buf); let line = String::from_utf8_lossy(line_buf).into(); let _ = write!(printer, "{line}"); buffer.push(line); } None => return Ok(()), } } } let _permit = self.control.acquire().await.unwrap(); let mut child = Command::new(venv.python_executable()) .args(["-c", script]) .current_dir(source_tree.simplified()) .envs(environment_variables) .env(EnvVars::PATH, modified_path) .env(EnvVars::VIRTUAL_ENV, venv.root()) // NOTE: it would be nice to get colored output from build backends, // but setting CLICOLOR_FORCE=1 changes the output of underlying // tools, which might mess with wrappers trying to parse their // output. .env(EnvVars::PYTHONIOENCODING, "utf-8:backslashreplace") // Remove potentially-sensitive environment variables. .env_remove(EnvVars::PYX_API_KEY) .env_remove(EnvVars::UV_API_KEY) .env_remove(EnvVars::PYX_AUTH_TOKEN) .env_remove(EnvVars::UV_AUTH_TOKEN) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .map_err(|err| Error::CommandFailed(venv.python_executable().to_path_buf(), err))?; // Create buffers to capture `stdout` and `stderr`. let mut stdout_buf = Vec::with_capacity(1024); let mut stderr_buf = Vec::with_capacity(1024); // Create separate readers for `stdout` and `stderr`. let stdout_reader = tokio::io::BufReader::new(child.stdout.take().unwrap()).split(b'\n'); let stderr_reader = tokio::io::BufReader::new(child.stderr.take().unwrap()).split(b'\n'); // Asynchronously read from the in-memory pipes. let printer = Printer::from(self.level); let result = tokio::join!( read_from(stdout_reader, printer, &mut stdout_buf), read_from(stderr_reader, printer, &mut stderr_buf), ); match result { (Ok(()), Ok(())) => {} (Err(err), _) | (_, Err(err)) => { return Err(Error::CommandFailed( venv.python_executable().to_path_buf(), err, )); } } // Wait for the child process to finish. let status = child .wait() .await .map_err(|err| Error::CommandFailed(venv.python_executable().to_path_buf(), err))?; Ok(PythonRunnerOutput { stdout: stdout_buf, stderr: stderr_buf, status, }) } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Printer { /// Send the build backend output to `stderr`. Stderr, /// Send the build backend output to `tracing`. Debug, /// Hide the build backend output. Quiet, } impl From for Printer { fn from(output: BuildOutput) -> Self { match output { BuildOutput::Stderr => Self::Stderr, BuildOutput::Debug => Self::Debug, BuildOutput::Quiet => Self::Quiet, } } } impl Write for Printer { fn write_str(&mut self, s: &str) -> std::fmt::Result { match self { Self::Stderr => { anstream::eprintln!("{s}"); } Self::Debug => { debug!("{s}"); } Self::Quiet => {} } Ok(()) } } uv-0.9.17+ds1/crates/uv-build-frontend/src/pipreqs.rs000066400000000000000000000021531520155276700224050ustar00rootroot00000000000000use std::str::FromStr; use std::sync::LazyLock; use rustc_hash::FxHashMap; use uv_normalize::PackageName; /// A mapping from module name to PyPI package name. pub(crate) struct ModuleMap<'a>(FxHashMap<&'a str, PackageName>); impl<'a> ModuleMap<'a> { /// Generate a [`ModuleMap`] from a string representation, encoded in `${module}:{package}` format. fn from_str(source: &'a str) -> Self { let mut mapping = FxHashMap::default(); for line in source.lines() { if let Some((module, package)) = line.split_once(':') { let module = module.trim(); let package = PackageName::from_str(package.trim()).unwrap(); mapping.insert(module, package); } } Self(mapping) } /// Look up a PyPI package name for a given module name. pub(crate) fn lookup(&self, module: &str) -> Option<&PackageName> { self.0.get(module) } } /// A mapping from module name to PyPI package name. pub(crate) static MODULE_MAPPING: LazyLock = LazyLock::new(|| ModuleMap::from_str(include_str!("pipreqs/mapping"))); uv-0.9.17+ds1/crates/uv-build-frontend/src/pipreqs/000077500000000000000000000000001520155276700220365ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-build-frontend/src/pipreqs/LICENSE000066400000000000000000000261351520155276700230520ustar00rootroot00000000000000 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. uv-0.9.17+ds1/crates/uv-build-frontend/src/pipreqs/mapping000066400000000000000000000645061520155276700234270ustar00rootroot00000000000000AFQ:pyAFQ AG_fft_tools:agpy ANSI:pexpect Adafruit:Adafruit_Libraries App:Zope2 Asterisk:py_Asterisk BB_jekyll_hook:bitbucket_jekyll_hook Banzai:Banzai_NGS BeautifulSoupTests:BeautifulSoup BioSQL:biopython BuildbotStatusShields:BuildbotEightStatusShields ComputedAttribute:ExtensionClass constraint:python-constraint Crypto:pycryptodome Cryptodome:pycryptodomex FSM:pexpect FiftyOneDegrees:51degrees_mobile_detector_v3_wrapper functional:pyfunctional GeoBaseMain:GeoBasesDev GeoBases:GeoBasesDev Globals:Zope2 HelpSys:Zope2 IPython:ipython Kittens:astro_kittens Levenshtein:python_Levenshtein Lifetime:Zope2 MethodObject:ExtensionClass MySQLdb:MySQL-python OFS:Zope2 OpenGL:PyOpenGL OpenSSL:pyOpenSSL PIL:Pillow Products:Zope2 PyWCSTools:astLib Pyxides:astro_pyxis QtCore:PySide S3:s3cmd SCons:pystick speech_recognition:SpeechRecognition Shared:Zope2 Signals:Zope2 Stemmer:PyStemmer Testing:Zope2 TopZooTools:topzootools TreeDisplay:DocumentTemplate WorkingWithDocumentConversion:aspose_pdf_java_for_python ZPublisher:Zope2 ZServer:Zope2 ZTUtils:Zope2 aadb:auto_adjust_display_brightness abakaffe:abakaffe_cli abiosgaming:abiosgaming.py abiquo:abiquo_api abl:abl.cssprocessor abl:abl.robot abl:abl.util abl:abl.vpath abo:abo_generator abris_transform:abris abstract:abstract.jwrotator abu:abu.admin ac_flask:AC_Flask_HipChat acg:anikom15 acme:acme.dchat acme:acme.hello acted:acted.projects action:ActionServer actionbar:actionbar.panel activehomed:afn activepapers:ActivePapers.Py address_book:address_book_lansry adi:adi.commons adi:adi.devgen adi:adi.fullscreen adi:adi.init adi:adi.playlist adi:adi.samplecontent adi:adi.slickstyle adi:adi.suite adi:adi.trash adict:aDict2 aditam:aditam.agent aditam:aditam.core adiumsh:adium_sh adjector:AdjectorClient adjector:AdjectorTracPlugin adkit:Banner_Ad_Toolkit admin_tools:django_admin_tools adminishcategories:adminish_categories adminsortable:django_admin_sortable adspygoogle:adspygoogle.adwords advancedcaching:agtl adytum:Adytum_PyMonitor affinitic:affinitic.docpyflakes affinitic:affinitic.recipe.fakezope2eggs affinitic:affinitic.simplecookiecuttr affinitic:affinitic.verifyinterface affinitic:affinitic.zamqp afpy:afpy.xap agatesql:agate_sql ageliaco:ageliaco.recipe.csvconfig agent_http:agent.http agora:Agora_Client agora:Agora_Fountain agora:Agora_Fragment agora:Agora_Planner agora:Agora_Service_Provider agoraplex:agoraplex.themes.sphinx agsci:agsci.blognewsletter agx:agx.core agx:agx.dev agx:agx.generator.buildout agx:agx.generator.dexterity agx:agx.generator.generator agx:agx.generator.plone agx:agx.generator.pyegg agx:agx.generator.sql agx:agx.generator.uml agx:agx.generator.zca agx:agx.transform.uml2fs agx:agx.transform.xmi2uml aimes:aimes.bundle aimes:aimes.skeleton aio:aio.app aio:aio.config aio:aio.core aio:aio.signals aiohs2:aio_hs2 aioroutes:aio_routes aios3:aio_s3 airbrake:airbrake_flask airship:airship_icloud airship:airship_steamcloud airflow:apache-airflow akamai:edgegrid_python alation:alation_api alba_client:alba_client_python alburnum:alburnum_maas_client alchemist:alchemist.audit alchemist:alchemist.security alchemist:alchemist.traversal alchemist:alchemist.ui alchemyapi:alchemyapi_python alerta:alerta_server alexandria_upload:Alexandria_Upload_Utils alibaba:alibaba_python_sdk aliyun:aliyun_python_sdk aliyuncli:alicloudcli aliyunsdkacs:aliyun_python_sdk_acs aliyunsdkbatchcompute:aliyun_python_sdk_batchcompute aliyunsdkbsn:aliyun_python_sdk_bsn aliyunsdkbss:aliyun_python_sdk_bss aliyunsdkcdn:aliyun_python_sdk_cdn aliyunsdkcms:aliyun_python_sdk_cms aliyunsdkcore:aliyun_python_sdk_core aliyunsdkcrm:aliyun_python_sdk_crm aliyunsdkcs:aliyun_python_sdk_cs aliyunsdkdrds:aliyun_python_sdk_drds aliyunsdkecs:aliyun_python_sdk_ecs aliyunsdkess:aliyun_python_sdk_ess aliyunsdkft:aliyun_python_sdk_ft aliyunsdkmts:aliyun_python_sdk_mts aliyunsdkocs:aliyun_python_sdk_ocs aliyunsdkoms:aliyun_python_sdk_oms aliyunsdkossadmin:aliyun_python_sdk_ossadmin aliyunsdkr-kvstore:aliyun_python_sdk_r_kvstore aliyunsdkram:aliyun_python_sdk_ram aliyunsdkrds:aliyun_python_sdk_rds aliyunsdkrisk:aliyun_python_sdk_risk aliyunsdkros:aliyun_python_sdk_ros aliyunsdkslb:aliyun_python_sdk_slb aliyunsdksts:aliyun_python_sdk_sts aliyunsdkubsms:aliyun_python_sdk_ubsms aliyunsdkyundun:aliyun_python_sdk_yundun allattachments:AllAttachmentsMacro allocine:allocine_wrapper allowedsites:django_allowedsites alm:alm.solrindex aloft:aloft.py alpacalib:alpaca alphabetic:alphabetic_simple alphasms:alphasms_client altered:altered.states alterootheme:alterootheme.busycity alterootheme:alterootheme.intensesimplicity alterootheme:alterootheme.lazydays alurinium:alurinium_image_processing alxlib:alx amara3:amara3_iri amara3:amara3_xml amazon:AmazonAPIWrapper amazon:python_amazon_simple_product_api ambikesh1349-1:ambikesh1349_1 ambilight:AmbilightParty amifs:amifs_core amiorganizer:ami_organizer amitu:amitu.lipy amitu:amitu_putils amitu:amitu_websocket_client amitu:amitu_zutils amltlearn:AMLT_learn amocrm:amocrm_api amqpdispatcher:amqp_dispatcher amqpstorm:AMQP_Storm analytics:analytics_python analyzedir:AnalyzeDirectory ancientsolutions:ancientsolutions_crypttools anderson_paginator:anderson.paginator android_clean_app:android_resource_remover anel_power_control:AnelPowerControl angus:angus_sdk_python annalist_root:Annalist annogesiclib:ANNOgesic ansible-role-apply:ansible_role_apply ansibledebugger:ansible_playbook_debugger ansibledocgen:ansible_docgen ansibleflow:ansible_flow ansibleinventorygrapher:ansible_inventory_grapher ansiblelint:ansible_lint ansiblerolesgraph:ansible_roles_graph ansibletools:ansible_tools anthill:anthill.exampletheme anthill:anthill.skinner anthill:anthill.tal.macrorenderer anthrax:AnthraxDojoFrontend anthrax:AnthraxHTMLInput anthrax:AnthraxImage antisphinx:antiweb antispoofing:antispoofing.evaluation antlr4:antlr4_python2_runtime antlr4:antlr4_python3_runtime antlr4:antlr4_python_alt anybox:anybox.buildbot.openerp anybox:anybox.nose.odoo anybox:anybox.paster.odoo anybox:anybox.paster.openerp anybox:anybox.recipe.sysdeps anybox:anybox.scripts.odoo apiclient:google_api_python_client apitools:google_apitools apm:arpm app_data:django_appdata appconf:django_appconf appd:AppDynamicsDownloader appd:AppDynamicsREST appdynamics_bindeps:appdynamics_bindeps_linux_x64 appdynamics_bindeps:appdynamics_bindeps_linux_x86 appdynamics_bindeps:appdynamics_bindeps_osx_x64 appdynamics_proxysupport:appdynamics_proxysupport_linux_x64 appdynamics_proxysupport:appdynamics_proxysupport_linux_x86 appdynamics_proxysupport:appdynamics_proxysupport_osx_x64 appium:Appium_Python_Client appliapps:applibase appserver:broadwick archetypes:archetypes.kss archetypes:archetypes.multilingual archetypes:archetypes.schemaextender arm:ansible_role_manager armor:armor_api armstrong:armstrong.apps.related_content armstrong:armstrong.apps.series armstrong:armstrong.cli armstrong:armstrong.core.arm_access armstrong:armstrong.core.arm_layout armstrong:armstrong.core.arm_sections armstrong:armstrong.core.arm_wells armstrong:armstrong.dev armstrong:armstrong.esi armstrong:armstrong.hatband armstrong:armstrong.templates.standard armstrong:armstrong.utils.backends armstrong:armstrong.utils.celery arstecnica:arstecnica.raccoon.autobahn arstecnica:arstecnica.sqlalchemy.async article-downloader:article_downloader artifactcli:artifact_cli arvados:arvados_python_client arvados_cwl:arvados_cwl_runner arvnodeman:arvados_node_manager asana_to_github:AsanaToGithub asciibinary:AsciiBinaryConverter asd:AdvancedSearchDiscovery askbot:askbot_tuan askbot:askbot_tuanpa asnhistory:asnhistory_redis aspen_jinja2_renderer:aspen_jinja2 aspen_tornado_engine:aspen_tornado asprise_ocr_api:asprise_ocr_sdk_python_api aspy:aspy.refactor_imports aspy:aspy.yaml asterisk:asterisk_ami asts:add_asts asymmetricbase:asymmetricbase.enum asymmetricbase:asymmetricbase.fields asymmetricbase:asymmetricbase.logging asymmetricbase:asymmetricbase.utils asyncirc:asyncio_irc asyncmongoorm:asyncmongoorm_je asyncssh:asyncssh_unofficial athletelist:athletelistyy atm:automium atmosphere:atmosphere_python_client atom:gdata atomic:AtomicWrite atomisator:atomisator.db atomisator:atomisator.enhancers atomisator:atomisator.feed atomisator:atomisator.indexer atomisator:atomisator.outputs atomisator:atomisator.parser atomisator:atomisator.readers atreal:atreal.cmfeditions.unlocker atreal:atreal.filestorage.common atreal:atreal.layouts atreal:atreal.mailservices atreal:atreal.massloader atreal:atreal.monkeyplone atreal:atreal.override.albumview atreal:atreal.richfile.preview atreal:atreal.richfile.qualifier atreal:atreal.usersinout atsim:atsim.potentials attractsdk:attract_sdk audio:audio.bitstream audio:audio.coders audio:audio.filters audio:audio.fourier audio:audio.frames audio:audio.lp audio:audio.psychoacoustics audio:audio.quantizers audio:audio.shrink audio:audio.wave aufrefer:auf_refer auslfe:auslfe.formonline.content auspost:auspost_apis auth0:auth0_python auth_server_client:AuthServerClient authorize:AuthorizeSauce authzpolicy:AuthzPolicyPlugin autobahn:autobahn_rce avatar:geonode_avatar awebview:android_webview azure:azure_common azure:azure_mgmt_common azure:azure_mgmt_compute azure:azure_mgmt_network azure:azure_mgmt_nspkg azure:azure_mgmt_resource azure:azure_mgmt_storage azure:azure_nspkg azure:azure_servicebus azure:azure_servicemanagement_legacy azure:azure_storage b2gcommands:b2g_commands b2gperf:b2gperf_v1.3 b2gperf:b2gperf_v1.4 b2gperf:b2gperf_v2.0 b2gperf:b2gperf_v2.1 b2gperf:b2gperf_v2.2 b2gpopulate:b2gpopulate_v1.3 b2gpopulate:b2gpopulate_v1.4 b2gpopulate:b2gpopulate_v2.0 b2gpopulate:b2gpopulate_v2.1 b2gpopulate:b2gpopulate_v2.2 b3j0f:b3j0f.annotation b3j0f:b3j0f.aop b3j0f:b3j0f.conf b3j0f:b3j0f.sync b3j0f:b3j0f.utils babel:Babel babelglade:BabelGladeExtractor backplane:backplane2_pyclient backport_abcoll:backport_collections backports:backports.functools_lru_cache backports:backports.inspect backports:backports.pbkdf2 backports:backports.shutil_get_terminal_size backports:backports.socketpair backports:backports.ssl backports:backports.ssl_match_hostname backports:backports.statistics badgekit:badgekit_api_client badlinks:BadLinksPlugin bael:bael.project baidu:baidupy balrog:buildtools baluhn:baluhn_redux bamboo:bamboo.pantrybell bamboo:bamboo.scaffold bamboo:bamboo.setuptools_version bamboo:bamboo_data bamboo:bamboo_server bambu:bambu_codemirror bambu:bambu_dataportability bambu:bambu_enqueue bambu:bambu_faq bambu:bambu_ffmpeg bambu:bambu_grids bambu:bambu_international bambu:bambu_jwplayer bambu:bambu_minidetect bambu:bambu_navigation bambu:bambu_notifications bambu:bambu_payments bambu:bambu_pusher bambu:bambu_saas bambu:bambu_sites banana:Bananas banana:banana.maya bang:bangtext barcode:barcode_generator bark:bark_ssg barking_owl:BarkingOwl bart:bart_py basalt:basalt_tasks base62:base_62 basemap:basemap_Jim bash:bash_toolbelt bashutils:Python_Bash_Utils basic_http:BasicHttp basil:basil_daq batchapps:azure_batch_apps bcrypt:python_bcrypt beaker:Beaker beetsplug:beets begin:begins benchit:bench_it beproud:beproud.utils bfillings:burrito_fillings bigjob:BigJob billboard:billboard.py binstar_build_client:anaconda_build binstar_client:anaconda_client biocommons:biocommons.dev birdhousebuilder:birdhousebuilder.recipe.conda birdhousebuilder:birdhousebuilder.recipe.docker birdhousebuilder:birdhousebuilder.recipe.redis birdhousebuilder:birdhousebuilder.recipe.supervisor blender26-meshio:pymeshio bootstrap:BigJob borg:borg.localrole bow:bagofwords bpdb:bpython bqapi:bisque_api braces:django_braces briefscaster:briefs_caster brisa_media_server/plugins:brisa_media_server_plugins brkt_requests:brkt_sdk broadcastlogging:broadcast_logging brocadetool:brocade_tool bronto:bronto_python brownie:Brownie browsermobproxy:browsermob_proxy brubeckmysql:brubeck_mysql brubeckoauth:brubeck_oauth brubeckservice:brubeck_service brubeckuploader:brubeck_uploader bs4:beautifulsoup4 bson:pymongo bst:bst.pygasus.core bst:bst.pygasus.datamanager bst:bst.pygasus.demo bst:bst.pygasus.i18n bst:bst.pygasus.resources bst:bst.pygasus.scaffolding bst:bst.pygasus.security bst:bst.pygasus.session bst:bst.pygasus.wsgi btable:btable_py btapi:bananatag_api btceapi:btce_api btcebot:btce_bot btsync:btsync.py buck:buck.pprint bud:bud.nospam budy:budy_api buffer:buffer_alpaca buggd:bug.gd bugle:bugle_sites bugspots:bug_spots bugzilla:python_bugzilla bugzscout:bugzscout_py buildTools:ajk_ios_buildTools buildnotifylib:BuildNotify buildout:buildout.bootstrap buildout:buildout.disablessl buildout:buildout.dumppickedversions buildout:buildout.dumppickedversions2 buildout:buildout.dumprequirements buildout:buildout.eggnest buildout:buildout.eggscleaner buildout:buildout.eggsdirectories buildout:buildout.eggtractor buildout:buildout.extensionscripts buildout:buildout.locallib buildout:buildout.packagename buildout:buildout.recipe.isolation buildout:buildout.removeaddledeggs buildout:buildout.requirements buildout:buildout.sanitycheck buildout:buildout.sendpickedversions buildout:buildout.threatlevel buildout:buildout.umask buildout:buildout.variables buildslave:buildbot_slave builtins:pies2overrides bumper:bumper_lib bumple:bumple_downloader bundesliga:bundesliga_cli bundlemaker:bundlemanager burpui:burp_ui busyflow:busyflow.pivotal buttercms-django:buttercms_django buzz:buzz_python_client bvc:buildout_versions_checker bvggrabber:bvg_grabber byond:BYONDTools bzETL:Bugzilla_ETL bzlib:bugzillatools bzrlib:bzr bzrlib:bzr_automirror bzrlib:bzr_bash_completion bzrlib:bzr_colo bzrlib:bzr_killtrailing bzrlib:bzr_pqm c2c:c2c.cssmin c2c:c2c.recipe.closurecompile c2c:c2c.recipe.cssmin c2c:c2c.recipe.jarfile c2c:c2c.recipe.msgfmt c2c:c2c.recipe.pkgversions c2c:c2c.sqlalchemy.rest c2c:c2c.versions c2c_recipe_facts:c2c.recipe.facts cabalgata:cabalgata_silla_de_montar cabalgata:cabalgata_zookeeper cache_utils:django_cache_utils captcha:django_recaptcha cartridge:Cartridge cassandra:cassandra_driver cassandralauncher:CassandraLauncher cc42:42qucc cerberus:Cerberus cfnlint:cfn-lint chameleon:Chameleon charmtools:charm_tools chef:PyChef chip8:c8d cjson:python_cjson classytags:django_classy_tags cloghandler:ConcurrentLogHandler clonevirtualenv:virtualenv_clone cloud-insight:al_cloudinsight cloud_admin:adminapi cloudservers:python_cloudservers clusterconsole:cerebrod clustersitter:cerebrod cms:django_cms colander:ba_colander colors:ansicolors compile:bf_lc3 compose:docker_compose compressor:django_compressor concurrent:futures configargparse:ConfigArgParse configparser:pies2overrides contracts:PyContracts coordination:BigJob copyreg:pies2overrides corebio:weblogo couchapp:Couchapp couchdb:CouchDB couchdbcurl:couchdb_python_curl courseradownloader:coursera_dl cow:cow_framework creole:python_creole creoleparser:Creoleparser crispy_forms:django_crispy_forms cronlog:python_crontab crontab:python_crontab ctff:tff cups:pycups curator:elasticsearch_curator curl:pycurl cv2:opencv-python daemon:python_daemon dare:DARE dateutil:python_dateutil dawg:DAWG deb822:python_debian debian:python_debian decouple:python-decouple demo:webunit demosongs:PySynth deployer:juju_deployer depot:filedepot devtools:tg.devtools dgis:2gis dhtmlparser:pyDHTMLParser digitalocean:python_digitalocean discord:discord.py distribute_setup:ez_setup distutils2:Distutils2 django:Django django_hstore:amitu_hstore djangobower:django_bower djcelery:django_celery djkombu:django_kombu djorm_pgarray:djorm_ext_pgarray dns:dnspython docgen:ansible_docgenerator docker:docker_py dogpile:dogpile.cache dogpile:dogpile.core dogshell:dogapi dot_parser:pydot dot_parser:pydot2 dot_parser:pydot3k dotenv:python-dotenv dpkt:dpkt_fix dsml:python_ldap durationfield:django_durationfield dzclient:datazilla easybuild:easybuild_framework editor:python_editor elasticluster:azure_elasticluster elasticluster:azure_elasticluster_current elftools:pyelftools elixir:Elixir em:empy emlib:empy enchant:pyenchant encutils:cssutils engineio:python_engineio enum:enum34 ephem:pyephem errorreporter:abl.errorreporter esplot:beaker_es_plot example:adrest examples:tweepy ez_setup:pycassa fabfile:Fabric fabric:Fabric faker:Faker fdpexpect:pexpect fedora:python_fedora fias:ailove_django_fias fiftyone_degrees:51degrees_mobile_detector five:five.customerize five:five.globalrequest five:five.intid five:five.localsitemanager five:five.pt flasher:android_flasher flask:Flask flask_frozen:Frozen_Flask flask_redis:Flask_And_Redis flaskext:Flask_Bcrypt flvscreen:vnc2flv followit:django_followit forge:pyforge formencode:FormEncode formtools:django_formtools fourch:4ch franz:allegrordf freetype:freetype_py frontmatter:python_frontmatter ftpcloudfs:ftp_cloudfs funtests:librabbitmq fuse:fusepy fuzzy:Fuzzy gabbi:tiddlyweb gen_3dwallet:3d_wallet_generator gendimen:android_gendimen genshi:Genshi geohash:python_geohash geonode:GeoNode geoserver:gsconfig geraldo:Geraldo getenv:django_getenv geventwebsocket:gevent_websocket gflags:python_gflags git:GitPython github:PyGithub github3:github3.py gitpy:git_py globusonline:globusonline_transfer_api_client google:protobuf googleapiclient:google_api_python_client grace-dizmo:grace_dizmo grammar:anovelmous_grammar grapheneapi:graphenelib greplin:scales gridfs:pymongo grokcore:grokcore.component gslib:gsutil hamcrest:PyHamcrest harpy:HARPy hawk:PyHawk_with_a_single_extra_commit haystack:django_haystack hgext:mercurial hggit:hg_git hglib:python_hglib ho:pisa hola:amarokHola hoover:Hoover hostlist:python_hostlist html:pies2overrides htmloutput:nosehtmloutput http:pies2overrides hvad:django_hvad hydra:hydra-core i99fix:199Fix igraph:python_igraph imdb:IMDbPY impala:impyla inmemorystorage:ambition_inmemorystorage ipaddress:backport_ipaddress jaraco:jaraco.timing jaraco:jaraco.util jinja2:Jinja2 jiracli:jira_cli johnny:johnny_cache jose:python_jose jpgrid:python_geohash jpiarea:python_geohash jpype:JPype1 jpypex:JPype1 jsonfield:django_jsonfield jstools:aino_jstools jupyterpip:jupyter_pip jwt:PyJWT kazoo:asana_kazoo kernprof:line_profiler keyczar:python_keyczar keyedcache:django_keyedcache keystoneclient:python_keystoneclient kickstarter:kickstart krbv:krbV kss:kss.core kuyruk:Kuyruk langconv:AdvancedLangConv lava:lava_utils_interface lazr:lazr.authentication lazr:lazr.restfulclient lazr:lazr.uri ldap:python_ldap ldaplib:adpasswd ldapurl:python_ldap ldif:python_ldap lib2or3:2or3 lib3to2:3to2 libaito:Aito libbe:bugs_everywhere libbucket:bucket libcloud:apache_libcloud libfuturize:future libgenerateDS:generateDS libmproxy:mitmproxy libpasteurize:future libsvm:7lk_ocr_deploy lisa:lisa_server loadingandsaving:aspose_words_java_for_python locust:locustio logbook:Logbook logentries:buildbot_status_logentries logilab:logilab_mtconverter machineconsole:cerebrod machinesitter:cerebrod magic:python_magic mako:Mako manifestparser:ManifestDestiny marionette:marionette_client markdown:Markdown marks:pytest_marks markupsafe:MarkupSafe mavnative:pymavlink memcache:python_memcached metacomm:AllPairs metaphone:Metafone metlog:metlog_py mezzanine:Mezzanine migrate:sqlalchemy_migrate mimeparse:python_mimeparse minitage:minitage.paste minitage:minitage.recipe.common missingdrawables:android_missingdrawables mixfiles:PySynth mkfreq:PySynth mkrst_themes:2lazy2rest mockredis:mockredispy modargs:python_modargs model_utils:django_model_utils models:asposebarcode models:asposestorage moksha:moksha.common moksha:moksha.hub moksha:moksha.wsgi moneyed:py_moneyed mongoalchemy:MongoAlchemy monthdelta:MonthDelta mopidy:Mopidy mopytools:MoPyTools mptt:django_mptt mpv:python-mpv mrbob:mr.bob msgpack:msgpack_python mutations:aino_mutations mws:amazon_mws mysql:mysql_connector_repackaged native_tags:django_native_tags ndg:ndg_httpsclient nereid:trytond_nereid nested:baojinhuan nester:Amauri nester:abofly nester:bssm_pythonSig novaclient:python_novaclient oauth2_provider:alauda_django_oauth oauth2client:oauth2client odf:odfpy ometa:Parsley openid:python_openid opensearchsdk:ali_opensearch oslo_i18n:oslo.i18n oslo_serialization:oslo.serialization oslo_utils:oslo.utils oss:alioss oss:aliyun_python_sdk_oss oss:aliyunoss output:cashew owslib:OWSLib packetdiag:nwdiag paho:paho_mqtt paintstore:django_paintstore parler:django_parler past:future paste:PasteScript path:forked_path path:path.py patricia:patricia-trie paver:Paver peak:ProxyTypes picasso:anderson.picasso picklefield:django-picklefield pilot:BigJob pivotal:pivotal_py play_wav:PySynth playhouse:peewee plivoxml:plivo plone:plone.alterego plone:plone.api plone:plone.app.blob plone:plone.app.collection plone:plone.app.content plone:plone.app.contentlisting plone:plone.app.contentmenu plone:plone.app.contentrules plone:plone.app.contenttypes plone:plone.app.controlpanel plone:plone.app.customerize plone:plone.app.dexterity plone:plone.app.discussion plone:plone.app.event plone:plone.app.folder plone:plone.app.i18n plone:plone.app.imaging plone:plone.app.intid plone:plone.app.layout plone:plone.app.linkintegrity plone:plone.app.locales plone:plone.app.lockingbehavior plone:plone.app.multilingual plone:plone.app.portlets plone:plone.app.querystring plone:plone.app.redirector plone:plone.app.registry plone:plone.app.relationfield plone:plone.app.textfield plone:plone.app.theming plone:plone.app.users plone:plone.app.uuid plone:plone.app.versioningbehavior plone:plone.app.viewletmanager plone:plone.app.vocabularies plone:plone.app.widgets plone:plone.app.workflow plone:plone.app.z3cform plone:plone.autoform plone:plone.batching plone:plone.behavior plone:plone.browserlayer plone:plone.caching plone:plone.contentrules plone:plone.dexterity plone:plone.event plone:plone.folder plone:plone.formwidget.namedfile plone:plone.formwidget.recurrence plone:plone.i18n plone:plone.indexer plone:plone.intelligenttext plone:plone.keyring plone:plone.locking plone:plone.memoize plone:plone.namedfile plone:plone.outputfilters plone:plone.portlet.collection plone:plone.portlet.static plone:plone.portlets plone:plone.protect plone:plone.recipe.zope2install plone:plone.registry plone:plone.resource plone:plone.resourceeditor plone:plone.rfc822 plone:plone.scale plone:plone.schema plone:plone.schemaeditor plone:plone.session plone:plone.stringinterp plone:plone.subrequest plone:plone.supermodel plone:plone.synchronize plone:plone.theme plone:plone.transformchain plone:plone.uuid plone:plone.z3cform plonetheme:plonetheme.barceloneta png:pypng polymorphic:django_polymorphic postmark:python_postmark powerprompt:bash_powerprompt prefetch:django-prefetch printList:AndrewList progressbar:progressbar2 progressbar:progressbar33 provider:django_oauth2_provider puresasl:pure_sasl pwiz:peewee pxssh:pexpect py7zlib:pylzma pyAMI:pyAMI_core pyarsespyder:arsespyder pyasdf:asdf pyaspell:aspell_python_ctypes pybb:pybbm pybloomfilter:pybloomfiltermmap pyccuracy:Pyccuracy pyck:PyCK pycrfsuite:python_crfsuite pydispatch:PyDispatcher pygeolib:pygeocoder pygments:Pygments pygraph:python_graph_core pyjon:pyjon.utils pyjsonrpc:python_jsonrpc pykka:Pykka pylogo:PyLogo pylons:adhocracy_Pylons pymagic:libmagic pymycraawler:Amalwebcrawler pynma:AbakaffeNotifier pyphen:Pyphen pyrimaa:AEI pysideuic:PySide pysqlite2:adhocracy_pysqlite pysqlite2:pysqlite pysynth_b:PySynth pysynth_beeper:PySynth pysynth_c:PySynth pysynth_d:PySynth pysynth_e:PySynth pysynth_p:PySynth pysynth_s:PySynth pysynth_samp:PySynth pythongettext:python_gettext pythonjsonlogger:python_json_logger pyutilib:PyUtilib pywintypes:pywin32 pyximport:Cython qs:qserve quadtree:python_geohash queue:future quickapi:django_quickapi quickunit:nose_quickunit rackdiag:nwdiag radical:radical.pilot radical:radical.utils reStructuredText:Zope2 readability:readability_lxml readline:gnureadline recaptcha_works:django_recaptcha_works relstorage:RelStorage reportapi:django_reportapi reprlib:pies2overrides requests:Requests requirements:requirements_parser rest_framework:djangorestframework restclient:py_restclient retrial:async_retrial reversion:django_reversion rhaptos2:rhaptos2.common robot:robotframework robots:django_robots rosdep2:rosdep rsbackends:RSFile ruamel:ruamel.base s2repoze:pysaml2 saga:saga_python saml2:pysaml2 samtranslator:aws-sam-translator sass:libsass sassc:libsass sasstests:libsass sassutils:libsass sayhi:alex_sayhi scalrtools:scalr scikits:scikits.talkbox scratch:scratchpy screen:pexpect scss:pyScss sdict:dict.sorted sdk_updater:android_sdk_updater sekizai:django_sekizai sendfile:pysendfile serial:pyserial setuputils:astor shapefile:pyshp shapely:Shapely sika:ahonya_sika singleton:pysingleton sittercommon:cerebrod skbio:scikit_bio sklearn:scikit_learn slack:slackclient slugify:unicode_slugify slugify:python-slugify smarkets:smk_python_sdk snappy:ctypes_snappy socketio:python-socketio socketserver:pies2overrides sockjs:sockjs_tornado socks:SocksiPy_branch solr:solrpy solution:Solution sorl:sorl_thumbnail south:South sphinx:Sphinx sphinx_pypi_upload:ATD_document sphinxcontrib:sphinxcontrib_programoutput sqlalchemy:SQLAlchemy src:atlas src:auto_mix_prep stats_toolkit:bw_stats_toolkit statsd:dogstatsd_python stdnum:python_stdnum stoneagehtml:StoneageHTML storages:django_storages stubout:mox suds:suds_jurko swiftclient:python_swiftclient sx:pisa tabix:pytabix taggit:django_taggit tasksitter:cerebrod tastypie:django_tastypie teamcity:teamcity_messages telebot:pyTelegramBotAPI telegram:python-telegram-bot tempita:Tempita tenjin:Tenjin termstyle:python_termstyle test:pytabix thclient:treeherder_client threaded_multihost:django_threaded_multihost threecolor:3color_Press tidylib:pytidylib tkinter:future tlw:3lwg toredis:toredis_fork tornadoredis:tornado_redis tower_cli:ansible_tower_cli trac:Trac tracopt:Trac translation_helper:android_localization_helper treebeard:django_treebeard trytond:trytond_stock tsuru:tsuru_circus tvrage:python_tvrage tw2:tw2.core tw2:tw2.d3 tw2:tw2.dynforms tw2:tw2.excanvas tw2:tw2.forms tw2:tw2.jit tw2:tw2.jqplugins.flot tw2:tw2.jqplugins.gritter tw2:tw2.jqplugins.ui tw2:tw2.jquery tw2:tw2.sqla twisted:Twisted twitter:python_twitter txclib:transifex_client u115:115wangpan unidecode:Unidecode universe:ansible_universe usb:pyusb useless:useless.pipes userpass:auth_userpass utilities:automakesetup.py utkik:aino_utkik uwsgidecorators:uWSGI valentine:ab validate:configobj version:chartio virtualenvapi:ar_virtualenv_api vyatta:brocade_plugins webdav:Zope2 weblogolib:weblogo webob:WebOb websocket:websocket_client webtest:WebTest werkzeug:Werkzeug wheezy:wheezy.caching wheezy:wheezy.core wheezy:wheezy.http wikklytext:tiddlywebwiki winreg:future winrm:pywinrm workflow:Alfred_Workflow wsmeext:WSME wtforms:WTForms wtfpeewee:wtf_peewee xdg:pyxdg xdist:pytest_xdist xmldsig:pysaml2 xmlenc:pysaml2 xmlrpc:pies2overrides xmpp:xmpppy xstatic:XStatic_Font_Awesome xstatic:XStatic_jQuery xstatic:XStatic_jquery_ui yaml:PyYAML z3c:z3c.autoinclude z3c:z3c.caching z3c:z3c.form z3c:z3c.formwidget.query z3c:z3c.objpath z3c:z3c.pt z3c:z3c.relationfield z3c:z3c.traverser z3c:z3c.zcmlhook zmq:pyzmq zopyx:zopyx.textindexng3 uv-0.9.17+ds1/crates/uv-build/000077500000000000000000000000001520155276700157475ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-build/Cargo.toml000066400000000000000000000010631520155276700176770ustar00rootroot00000000000000[package] name = "uv-build" version = "0.9.17" description = "A Python build backend" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [dependencies] uv-build-backend = { workspace = true } uv-logging = { workspace = true } uv-version = { workspace = true } anstream = { workspace = true } anyhow = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } [lints] workspace = true uv-0.9.17+ds1/crates/uv-build/LICENSE-APACHE000066400000000000000000000261351520155276700177020ustar00rootroot00000000000000 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. uv-0.9.17+ds1/crates/uv-build/LICENSE-MIT000066400000000000000000000020651520155276700174060ustar00rootroot00000000000000MIT License Copyright (c) 2025 Astral Software Inc. 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. uv-0.9.17+ds1/crates/uv-build/README.md000066400000000000000000000003301520155276700172220ustar00rootroot00000000000000# Build backend for uv This package is a slimmed down version of uv containing only the build backend. See https://pypi.org/project/uv/ and https://docs.astral.sh/uv/ for the main project package and documentation. uv-0.9.17+ds1/crates/uv-build/deny.toml000066400000000000000000000006261520155276700176070ustar00rootroot00000000000000[bans] multiple-versions = "allow" deny = [ { crate = "rustls", reason = "The build backend does not need network access" }, { crate = "openssl", reason = "The build backend does not need network access" }, { crate = "reqwest", reason = "The build backend does not need network access" }, { crate = "schemars", reason = "JSON Schema generation is a development feature, not a runtime feature" }, ] uv-0.9.17+ds1/crates/uv-build/pyproject.toml000066400000000000000000000031361520155276700206660ustar00rootroot00000000000000[project] name = "uv-build" version = "0.9.17" description = "The uv build backend" authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" keywords = [ "uv", "requirements", "packaging" ] license = "MIT OR Apache-2.0" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Operating System :: OS Independent", "License :: OSI Approved :: MIT License", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Quality Assurance", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries", ] readme = "README.md" [project.urls] Repository = "https://github.com/astral-sh/uv" Documentation = "https://docs.astral.sh/uv" Changelog = "https://github.com/astral-sh/uv/blob/main/CHANGELOG.md" Releases = "https://github.com/astral-sh/uv/releases" Discord = "https://discord.gg/astral-sh" [build-system] requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [tool.maturin] bindings = "bin" module-name = "uv_build" python-source = "python" strip = true include = [ { path = "LICENSE-APACHE", format = "sdist" }, { path = "LICENSE-MIT", format = "sdist" }, ] [tool.uv] managed = false uv-0.9.17+ds1/crates/uv-build/python/000077500000000000000000000000001520155276700172705ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-build/python/uv_build/000077500000000000000000000000001520155276700211015ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-build/python/uv_build/__init__.py000066400000000000000000000113031520155276700232100ustar00rootroot00000000000000""" Python shims for the PEP 517 and PEP 660 build backend. Major imports in this module are required to be lazy: ``` $ hyperfine \ "/usr/bin/python3 -c \"print('hi')\"" \ "/usr/bin/python3 -c \"from subprocess import check_call; print('hi')\"" Base: Time (mean ± σ): 11.0 ms ± 1.7 ms [User: 8.5 ms, System: 2.5 ms] With import: Time (mean ± σ): 15.2 ms ± 2.0 ms [User: 12.3 ms, System: 2.9 ms] Base 1.38 ± 0.28 times faster than with import ``` The same thing goes for the typing module, so we use Python 3.10 type annotations that don't require importing typing but then quote them so earlier Python version ignore them while IDEs and type checker can see through the quotes. """ TYPE_CHECKING = False if TYPE_CHECKING: from collections.abc import Mapping, Sequence # noqa:I001 from typing import Any # noqa:I001 # Use the `uv build-backend` command rather than `uv-build`. This option is provided # for downstream distributions who provide `uv` and wish to avoid building a partially # overlapping `uv-build` executable. USE_UV_EXECUTABLE = False def warn_config_settings(config_settings: "Mapping[Any, Any] | None" = None) -> None: import sys if config_settings: print("Warning: Config settings are not supported", file=sys.stderr) def call( args: "Sequence[str]", config_settings: "Mapping[Any, Any] | None" = None ) -> str: """Invoke a uv subprocess and return the filename from stdout.""" import shutil import subprocess import sys warn_config_settings(config_settings) uv_bin_name = "uv" if USE_UV_EXECUTABLE else "uv-build" # Unlike `find_uv_bin`, this mechanism must work according to PEP 517 uv_bin = shutil.which(uv_bin_name) if uv_bin is None: raise RuntimeError(f"{uv_bin_name} was not properly installed") build_backend_args = ["build-backend"] if USE_UV_EXECUTABLE else [] # Forward stderr, capture stdout for the filename result = subprocess.run( [uv_bin, *build_backend_args, *args], stdout=subprocess.PIPE ) if result.returncode != 0: sys.exit(result.returncode) # If there was extra stdout, forward it (there should not be extra stdout) stdout = result.stdout.decode("utf-8").strip().splitlines(keepends=True) sys.stdout.writelines(stdout[:-1]) # Fail explicitly instead of an irrelevant stacktrace if not stdout: print( f"{uv_bin_name} subprocess did not return a filename on stdout", file=sys.stderr, ) sys.exit(1) return stdout[-1].strip() def build_sdist( sdist_directory: str, config_settings: "Mapping[Any, Any] | None" = None ) -> str: """PEP 517 hook `build_sdist`.""" args = ["build-sdist", sdist_directory] return call(args, config_settings) def build_wheel( wheel_directory: str, config_settings: "Mapping[Any, Any] | None" = None, metadata_directory: "str | None" = None, ) -> str: """PEP 517 hook `build_wheel`.""" args = ["build-wheel", wheel_directory] if metadata_directory: args.extend([metadata_directory]) return call(args, config_settings) def get_requires_for_build_sdist( config_settings: "Mapping[Any, Any] | None" = None, ) -> "Sequence[str]": """PEP 517 hook `get_requires_for_build_sdist`.""" warn_config_settings(config_settings) return [] def get_requires_for_build_wheel( config_settings: "Mapping[Any, Any] | None" = None, ) -> "Sequence[str]": """PEP 517 hook `get_requires_for_build_wheel`.""" warn_config_settings(config_settings) return [] def prepare_metadata_for_build_wheel( metadata_directory: str, config_settings: "Mapping[Any, Any] | None" = None ) -> str: """PEP 517 hook `prepare_metadata_for_build_wheel`.""" args = ["prepare-metadata-for-build-wheel", metadata_directory] return call(args, config_settings) def build_editable( wheel_directory: str, config_settings: "Mapping[Any, Any] | None" = None, metadata_directory: "str | None" = None, ) -> str: """PEP 660 hook `build_editable`.""" args = ["build-editable", wheel_directory] if metadata_directory: args.extend([metadata_directory]) return call(args, config_settings) def get_requires_for_build_editable( config_settings: "Mapping[Any, Any] | None" = None, ) -> "Sequence[str]": """PEP 660 hook `get_requires_for_build_editable`.""" warn_config_settings(config_settings) return [] def prepare_metadata_for_build_editable( metadata_directory: str, config_settings: "Mapping[Any, Any] | None" = None ) -> str: """PEP 660 hook `prepare_metadata_for_build_editable`.""" args = ["prepare-metadata-for-build-editable", metadata_directory] return call(args, config_settings) uv-0.9.17+ds1/crates/uv-build/python/uv_build/__main__.py000066400000000000000000000007011520155276700231710ustar00rootroot00000000000000def main(): import sys # This works both as redirect to use the proper uv package and as smoke test. print( "uv_build contains only the PEP 517 build backend for uv and can't be used on the CLI. " "Use `uv build` or another build frontend instead.", file=sys.stderr, ) if "--help" in sys.argv or "-h" in sys.argv: sys.exit(0) else: sys.exit(1) if __name__ == "__main__": main() uv-0.9.17+ds1/crates/uv-build/python/uv_build/py.typed000066400000000000000000000000001520155276700225660ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-build/ruff.toml000066400000000000000000000001461520155276700176070ustar00rootroot00000000000000# It is important retain compatibility with old versions in the build backend target-version = "py37" uv-0.9.17+ds1/crates/uv-build/src/000077500000000000000000000000001520155276700165365ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-build/src/main.rs000066400000000000000000000113141520155276700200300ustar00rootroot00000000000000use std::env; use std::io::Write; use std::path::PathBuf; use anyhow::{Context, Result, bail}; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{EnvFilter, Layer}; use uv_logging::UvFormat; /// Entrypoint for the `uv-build` Python package. fn main() -> Result<()> { // Support configuring the log level with `RUST_LOG` (shows only the error level by default) and // color. // // This configuration is a simplified version of the uv logging configuration. When using // uv_build through uv proper, the uv logging configuration applies. let filter = EnvFilter::builder() .with_default_directive(LevelFilter::OFF.into()) .from_env() .context("Invalid RUST_LOG directives")?; let stderr_layer = tracing_subscriber::fmt::layer() .event_format(UvFormat::default()) .with_writer(std::sync::Mutex::new(anstream::stderr())) .with_filter(filter); tracing_subscriber::registry().with(stderr_layer).init(); // Handrolled to avoid the large clap dependency let mut args = env::args_os(); // Skip the name of the binary args.next(); let command = args .next() .context("Missing command")? .to_str() .context("Invalid non-UTF8 command")? .to_string(); match command.as_str() { "build-sdist" => { let sdist_directory = PathBuf::from(args.next().context("Missing sdist directory")?); let filename = uv_build_backend::build_source_dist( &env::current_dir()?, &sdist_directory, uv_version::version(), false, )?; // Tell the build frontend about the name of the artifact we built writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; } "build-wheel" => { let wheel_directory = PathBuf::from(args.next().context("Missing wheel directory")?); let metadata_directory = args.next().map(PathBuf::from); let filename = uv_build_backend::build_wheel( &env::current_dir()?, &wheel_directory, metadata_directory.as_deref(), uv_version::version(), false, )?; // Tell the build frontend about the name of the artifact we built writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; } "build-editable" => { let wheel_directory = PathBuf::from(args.next().context("Missing wheel directory")?); let metadata_directory = args.next().map(PathBuf::from); let filename = uv_build_backend::build_editable( &env::current_dir()?, &wheel_directory, metadata_directory.as_deref(), uv_version::version(), false, )?; // Tell the build frontend about the name of the artifact we built writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; } "prepare-metadata-for-build-wheel" => { let wheel_directory = PathBuf::from(args.next().context("Missing wheel directory")?); let filename = uv_build_backend::metadata( &env::current_dir()?, &wheel_directory, uv_version::version(), )?; // Tell the build frontend about the name of the artifact we built writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; } "prepare-metadata-for-build-editable" => { let wheel_directory = PathBuf::from(args.next().context("Missing wheel directory")?); let filename = uv_build_backend::metadata( &env::current_dir()?, &wheel_directory, uv_version::version(), )?; // Tell the build frontend about the name of the artifact we built writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; } "--help" => { // This works both as redirect to use the proper uv package and as smoke test. writeln!( &mut std::io::stderr(), "uv_build contains only the PEP 517 build backend for uv and can't be used on the CLI. \ Use `uv build` or another build frontend instead." ).context("stdout is closed")?; } unknown => { bail!( "Unknown subcommand: {} (cli: {:?})", unknown, env::args_os() ); } } Ok(()) } uv-0.9.17+ds1/crates/uv-cache-info/000077500000000000000000000000001520155276700166445ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-cache-info/Cargo.toml000066400000000000000000000013771520155276700206040ustar00rootroot00000000000000[package] name = "uv-cache-info" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-fs = { workspace = true } fs-err = { workspace = true } globwalk = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } toml = { workspace = true } tracing = { workspace = true } walkdir = { workspace = true } [dev-dependencies] anyhow = { workspace = true } tempfile = { workspace = true } uv-0.9.17+ds1/crates/uv-cache-info/README.md000066400000000000000000000010351520155276700201220ustar00rootroot00000000000000 # uv-cache-info This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-cache-info). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-cache-info/src/000077500000000000000000000000001520155276700174335ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-cache-info/src/cache_info.rs000066400000000000000000000405301520155276700220610ustar00rootroot00000000000000use std::borrow::Cow; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use serde::Deserialize; use tracing::{debug, warn}; use uv_fs::Simplified; use crate::git_info::{Commit, Tags}; use crate::glob::cluster_globs; use crate::timestamp::Timestamp; #[derive(Debug, thiserror::Error)] pub enum CacheInfoError { #[error("Failed to parse glob patterns for `cache-keys`: {0}")] Glob(#[from] globwalk::GlobError), #[error(transparent)] Io(#[from] std::io::Error), } /// The information used to determine whether a built distribution is up-to-date, based on the /// timestamps of relevant files, the current commit of a repository, etc. #[derive(Default, Debug, Clone, Hash, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "kebab-case")] pub struct CacheInfo { /// The timestamp of the most recent `ctime` of any relevant files, at the time of the build. /// The timestamp will typically be the maximum of the `ctime` values of the `pyproject.toml`, /// `setup.py`, and `setup.cfg` files, if they exist; however, users can provide additional /// files to timestamp via the `cache-keys` field. timestamp: Option, /// The commit at which the distribution was built. commit: Option, /// The Git tags present at the time of the build. tags: Option, /// Environment variables to include in the cache key. #[serde(default)] env: BTreeMap>, /// The timestamp or inode of any directories that should be considered in the cache key. #[serde(default)] directories: BTreeMap, Option>, } impl CacheInfo { /// Return the [`CacheInfo`] for a given timestamp. pub fn from_timestamp(timestamp: Timestamp) -> Self { Self { timestamp: Some(timestamp), ..Self::default() } } /// Compute the cache info for a given path, which may be a file or a directory. pub fn from_path(path: &Path) -> Result { let metadata = fs_err::metadata(path)?; if metadata.is_file() { Ok(Self::from_file(path)?) } else { Self::from_directory(path) } } /// Compute the cache info for a given directory. pub fn from_directory(directory: &Path) -> Result { let mut commit = None; let mut tags = None; let mut last_changed: Option<(PathBuf, Timestamp)> = None; let mut directories = BTreeMap::new(); let mut env = BTreeMap::new(); // Read the cache keys. let cache_keys = if let Ok(contents) = fs_err::read_to_string(directory.join("pyproject.toml")) { if let Ok(pyproject_toml) = toml::from_str::(&contents) { pyproject_toml .tool .and_then(|tool| tool.uv) .and_then(|tool_uv| tool_uv.cache_keys) } else { None } } else { None }; // If no cache keys were defined, use the defaults. let cache_keys = cache_keys.unwrap_or_else(|| { vec![ CacheKey::Path(Cow::Borrowed("pyproject.toml")), CacheKey::Path(Cow::Borrowed("setup.py")), CacheKey::Path(Cow::Borrowed("setup.cfg")), CacheKey::Directory { dir: Cow::Borrowed("src"), }, ] }); // Incorporate timestamps from any direct filepaths. let mut globs = vec![]; for cache_key in cache_keys { match cache_key { CacheKey::Path(file) | CacheKey::File { file } => { if file .as_ref() .chars() .any(|c| matches!(c, '*' | '?' | '[' | '{')) { // Defer globs to a separate pass. globs.push(file); continue; } // Treat the path as a file. let path = directory.join(file.as_ref()); let metadata = match path.metadata() { Ok(metadata) => metadata, Err(err) if err.kind() == std::io::ErrorKind::NotFound => { continue; } Err(err) => { warn!("Failed to read metadata for file: {err}"); continue; } }; if !metadata.is_file() { warn!( "Expected file for cache key, but found directory: `{}`", path.display() ); continue; } let timestamp = Timestamp::from_metadata(&metadata); if last_changed.as_ref().is_none_or(|(_, prev_timestamp)| { *prev_timestamp < Timestamp::from_metadata(&metadata) }) { last_changed = Some((path, timestamp)); } } CacheKey::Directory { dir } => { // Treat the path as a directory. let path = directory.join(dir.as_ref()); let metadata = match path.metadata() { Ok(metadata) => metadata, Err(err) if err.kind() == std::io::ErrorKind::NotFound => { directories.insert(dir, None); continue; } Err(err) => { warn!("Failed to read metadata for directory: {err}"); continue; } }; if !metadata.is_dir() { warn!( "Expected directory for cache key, but found file: `{}`", path.display() ); continue; } if let Ok(created) = metadata.created() { // Prefer the creation time. directories.insert( dir, Some(DirectoryTimestamp::Timestamp(Timestamp::from(created))), ); } else { // Fall back to the inode. #[cfg(unix)] { use std::os::unix::fs::MetadataExt; directories .insert(dir, Some(DirectoryTimestamp::Inode(metadata.ino()))); } #[cfg(not(unix))] { warn!( "Failed to read creation time for directory: `{}`", path.display() ); } } } CacheKey::Git { git: GitPattern::Bool(true), } => match Commit::from_repository(directory) { Ok(commit_info) => commit = Some(commit_info), Err(err) => { debug!("Failed to read the current commit: {err}"); } }, CacheKey::Git { git: GitPattern::Set(set), } => { if set.commit.unwrap_or(false) { match Commit::from_repository(directory) { Ok(commit_info) => commit = Some(commit_info), Err(err) => { debug!("Failed to read the current commit: {err}"); } } } if set.tags.unwrap_or(false) { match Tags::from_repository(directory) { Ok(tags_info) => tags = Some(tags_info), Err(err) => { debug!("Failed to read the current tags: {err}"); } } } } CacheKey::Git { git: GitPattern::Bool(false), } => {} CacheKey::Environment { env: var } => { let value = std::env::var(&var).ok(); env.insert(var, value); } } } // If we have any globs, first cluster them using LCP and then do a single pass on each group. if !globs.is_empty() { for (glob_base, glob_patterns) in cluster_globs(&globs) { let walker = globwalk::GlobWalkerBuilder::from_patterns( directory.join(glob_base), &glob_patterns, ) .file_type(globwalk::FileType::FILE | globwalk::FileType::SYMLINK) .build()?; for entry in walker { let entry = match entry { Ok(entry) => entry, Err(err) => { warn!("Failed to read glob entry: {err}"); continue; } }; let metadata = if entry.path_is_symlink() { // resolve symlinks for leaf entries without following symlinks while globbing match fs_err::metadata(entry.path()) { Ok(metadata) => metadata, Err(err) => { warn!("Failed to resolve symlink for glob entry: {err}"); continue; } } } else { match entry.metadata() { Ok(metadata) => metadata, Err(err) => { warn!("Failed to read metadata for glob entry: {err}"); continue; } } }; if !metadata.is_file() { if !entry.path_is_symlink() { // don't warn if it was a symlink - it may legitimately resolve to a directory warn!( "Expected file for cache key, but found directory: `{}`", entry.path().display() ); } continue; } let timestamp = Timestamp::from_metadata(&metadata); if last_changed.as_ref().is_none_or(|(_, prev_timestamp)| { *prev_timestamp < Timestamp::from_metadata(&metadata) }) { last_changed = Some((entry.into_path(), timestamp)); } } } } let timestamp = if let Some((path, timestamp)) = last_changed { debug!( "Computed cache info: {timestamp:?}, {commit:?}, {tags:?}, {env:?}, {directories:?}. Most recently modified: {}", path.user_display() ); Some(timestamp) } else { None }; Ok(Self { timestamp, commit, tags, env, directories, }) } /// Compute the cache info for a given file, assumed to be a binary or source distribution /// represented as (e.g.) a `.whl` or `.tar.gz` archive. pub fn from_file(path: impl AsRef) -> std::io::Result { let metadata = fs_err::metadata(path.as_ref())?; let timestamp = Timestamp::from_metadata(&metadata); Ok(Self { timestamp: Some(timestamp), ..Self::default() }) } /// Returns `true` if the cache info is empty. pub fn is_empty(&self) -> bool { self.timestamp.is_none() && self.commit.is_none() && self.tags.is_none() && self.env.is_empty() && self.directories.is_empty() } } /// A `pyproject.toml` with an (optional) `[tool.uv]` section. #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] struct PyProjectToml { tool: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] struct Tool { uv: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] struct ToolUv { cache_keys: Option>, } #[derive(Debug, Clone, serde::Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)] pub enum CacheKey { /// Ex) `"Cargo.lock"` or `"**/*.toml"` Path(Cow<'static, str>), /// Ex) `{ file = "Cargo.lock" }` or `{ file = "**/*.toml" }` File { file: Cow<'static, str> }, /// Ex) `{ dir = "src" }` Directory { dir: Cow<'static, str> }, /// Ex) `{ git = true }` or `{ git = { commit = true, tags = false } }` Git { git: GitPattern }, /// Ex) `{ env = "UV_CACHE_INFO" }` Environment { env: String }, } #[derive(Debug, Clone, serde::Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)] pub enum GitPattern { Bool(bool), Set(GitSet), } #[derive(Debug, Clone, serde::Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct GitSet { commit: Option, tags: Option, } pub enum FilePattern { Glob(String), Path(PathBuf), } /// A timestamp used to measure changes to a directory. #[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)] enum DirectoryTimestamp { Timestamp(Timestamp), Inode(u64), } #[cfg(all(test, unix))] mod tests_unix { use anyhow::Result; use super::{CacheInfo, Timestamp}; #[test] fn test_cache_info_symlink_resolve() -> Result<()> { let dir = tempfile::tempdir()?; let dir = dir.path().join("dir"); fs_err::create_dir_all(&dir)?; let write_manifest = |cache_key: &str| { fs_err::write( dir.join("pyproject.toml"), format!( r#" [tool.uv] cache-keys = [ "{cache_key}" ] "# ), ) }; let touch = |path: &str| -> Result<_> { let path = dir.join(path); fs_err::create_dir_all(path.parent().unwrap())?; fs_err::write(&path, "")?; Ok(Timestamp::from_metadata(&path.metadata()?)) }; let cache_timestamp = || -> Result<_> { Ok(CacheInfo::from_directory(&dir)?.timestamp) }; write_manifest("x/**")?; assert_eq!(cache_timestamp()?, None); let y = touch("x/y")?; assert_eq!(cache_timestamp()?, Some(y)); let z = touch("x/z")?; assert_eq!(cache_timestamp()?, Some(z)); // leaf entry symlink should be resolved let a = touch("../a")?; fs_err::os::unix::fs::symlink(dir.join("../a"), dir.join("x/a"))?; assert_eq!(cache_timestamp()?, Some(a)); // symlink directories should not be followed while globbing let c = touch("../b/c")?; fs_err::os::unix::fs::symlink(dir.join("../b"), dir.join("x/b"))?; assert_eq!(cache_timestamp()?, Some(a)); // no globs, should work as expected write_manifest("x/y")?; assert_eq!(cache_timestamp()?, Some(y)); write_manifest("x/a")?; assert_eq!(cache_timestamp()?, Some(a)); write_manifest("x/b/c")?; assert_eq!(cache_timestamp()?, Some(c)); // symlink pointing to a directory write_manifest("x/*b*")?; assert_eq!(cache_timestamp()?, None); Ok(()) } } uv-0.9.17+ds1/crates/uv-cache-info/src/git_info.rs000066400000000000000000000155041520155276700216040ustar00rootroot00000000000000use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use tracing::warn; use walkdir::WalkDir; #[derive(Debug, thiserror::Error)] pub(crate) enum GitInfoError { #[error("The repository at {0} is missing a `.git` directory")] MissingGitDir(PathBuf), #[error("The repository at {0} is missing a `HEAD` file")] MissingHead(PathBuf), #[error("The repository at {0} is missing a `refs` directory")] MissingRefs(PathBuf), #[error("The repository at {0} has an invalid reference: `{1}`")] InvalidRef(PathBuf, String), #[error("The discovered commit has an invalid length (expected 40 characters): `{0}`")] WrongLength(String), #[error("The discovered commit has an invalid character (expected hexadecimal): `{0}`")] WrongDigit(String), #[error(transparent)] Io(#[from] std::io::Error), } /// The current commit for a repository (i.e., a 40-character hexadecimal string). #[derive(Default, Debug, Clone, Hash, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub(crate) struct Commit(String); impl Commit { /// Return the [`Commit`] for the repository at the given path. pub(crate) fn from_repository(path: &Path) -> Result { // Find the `.git` directory, searching through parent directories if necessary. let git_dir = path .ancestors() .map(|ancestor| ancestor.join(".git")) .find(|git_dir| git_dir.exists()) .ok_or_else(|| GitInfoError::MissingGitDir(path.to_path_buf()))?; let git_head_path = git_head(&git_dir).ok_or_else(|| GitInfoError::MissingHead(git_dir.clone()))?; let git_head_contents = fs_err::read_to_string(git_head_path)?; // The contents are either a commit or a reference in the following formats // - "" when the head is detached // - "ref " when working on a branch // If a commit, checking if the HEAD file has changed is sufficient // If a ref, we need to add the head file for that ref to rebuild on commit let mut git_ref_parts = git_head_contents.split_whitespace(); let commit_or_ref = git_ref_parts .next() .ok_or_else(|| GitInfoError::InvalidRef(git_dir.clone(), git_head_contents.clone()))?; let commit = if let Some(git_ref) = git_ref_parts.next() { let git_ref_path = git_dir.join(git_ref); let commit = fs_err::read_to_string(git_ref_path)?; commit.trim().to_string() } else { commit_or_ref.to_string() }; // The commit should be 40 hexadecimal characters. if commit.len() != 40 { return Err(GitInfoError::WrongLength(commit)); } if commit.chars().any(|c| !c.is_ascii_hexdigit()) { return Err(GitInfoError::WrongDigit(commit)); } Ok(Self(commit)) } } /// The set of tags visible in a repository. #[derive(Default, Debug, Clone, Hash, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub(crate) struct Tags(BTreeMap); impl Tags { /// Return the [`Tags`] for the repository at the given path. pub(crate) fn from_repository(path: &Path) -> Result { // Find the `.git` directory, searching through parent directories if necessary. let git_dir = path .ancestors() .map(|ancestor| ancestor.join(".git")) .find(|git_dir| git_dir.exists()) .ok_or_else(|| GitInfoError::MissingGitDir(path.to_path_buf()))?; let git_tags_path = git_refs(&git_dir) .ok_or_else(|| GitInfoError::MissingRefs(git_dir.clone()))? .join("tags"); let mut tags = BTreeMap::new(); // Map each tag to its commit. for entry in WalkDir::new(&git_tags_path).contents_first(true) { let entry = match entry { Ok(entry) => entry, Err(err) => { warn!("Failed to read Git tags: {err}"); continue; } }; let path = entry.path(); if !entry.file_type().is_file() { continue; } if let Ok(Some(tag)) = path.strip_prefix(&git_tags_path).map(|name| name.to_str()) { let commit = fs_err::read_to_string(path)?.trim().to_string(); // The commit should be 40 hexadecimal characters. if commit.len() != 40 { return Err(GitInfoError::WrongLength(commit)); } if commit.chars().any(|c| !c.is_ascii_hexdigit()) { return Err(GitInfoError::WrongDigit(commit)); } tags.insert(tag.to_string(), commit); } } Ok(Self(tags)) } } /// Return the path to the `HEAD` file of a Git repository, taking worktrees into account. fn git_head(git_dir: &Path) -> Option { // The typical case is a standard git repository. let git_head_path = git_dir.join("HEAD"); if git_head_path.exists() { return Some(git_head_path); } if !git_dir.is_file() { return None; } // If `.git/HEAD` doesn't exist and `.git` is actually a file, // then let's try to attempt to read it as a worktree. If it's // a worktree, then its contents will look like this, e.g.: // // gitdir: /home/andrew/astral/uv/main/.git/worktrees/pr2 // // And the HEAD file we want to watch will be at: // // /home/andrew/astral/uv/main/.git/worktrees/pr2/HEAD let contents = fs_err::read_to_string(git_dir).ok()?; let (label, worktree_path) = contents.split_once(':')?; if label != "gitdir" { return None; } let worktree_path = worktree_path.trim(); Some(PathBuf::from(worktree_path)) } /// Return the path to the `refs` directory of a Git repository, taking worktrees into account. fn git_refs(git_dir: &Path) -> Option { // The typical case is a standard git repository. let git_head_path = git_dir.join("refs"); if git_head_path.exists() { return Some(git_head_path); } if !git_dir.is_file() { return None; } // If `.git/refs` doesn't exist and `.git` is actually a file, // then let's try to attempt to read it as a worktree. If it's // a worktree, then its contents will look like this, e.g.: // // gitdir: /home/andrew/astral/uv/main/.git/worktrees/pr2 // // And the HEAD refs we want to watch will be at: // // /home/andrew/astral/uv/main/.git/refs let contents = fs_err::read_to_string(git_dir).ok()?; let (label, worktree_path) = contents.split_once(':')?; if label != "gitdir" { return None; } let worktree_path = PathBuf::from(worktree_path.trim()); let refs_path = worktree_path.parent()?.parent()?.join("refs"); Some(refs_path) } uv-0.9.17+ds1/crates/uv-cache-info/src/glob.rs000066400000000000000000000252001520155276700207230ustar00rootroot00000000000000use std::{ collections::BTreeMap, path::{Component, Components, Path, PathBuf}, }; /// Check if a component of the path looks like it may be a glob pattern. /// /// Note: this function is being used when splitting a glob pattern into a long possible /// base and the glob remainder (scanning through components until we hit the first component /// for which this function returns true). It is acceptable for this function to return /// false positives (e.g. patterns like 'foo[bar' or 'foo{bar') in which case correctness /// will not be affected but efficiency might be (because we'll traverse more than we should), /// however it should not return false negatives. fn is_glob_like(part: Component) -> bool { matches!(part, Component::Normal(_)) && part.as_os_str().to_str().is_some_and(|part| { ["*", "{", "}", "?", "[", "]"] .into_iter() .any(|c| part.contains(c)) }) } #[derive(Debug, Default, Clone, PartialEq, Eq)] struct GlobParts { base: PathBuf, pattern: PathBuf, } /// Split a glob into longest possible base + shortest possible glob pattern. fn split_glob(pattern: impl AsRef) -> GlobParts { let pattern: &Path = pattern.as_ref().as_ref(); let mut glob = GlobParts::default(); let mut globbing = false; let mut last = None; for part in pattern.components() { if let Some(last) = last { if last != Component::CurDir { if globbing { glob.pattern.push(last); } else { glob.base.push(last); } } } if !globbing { globbing = is_glob_like(part); } // we don't know if this part is the last one, defer handling it by one iteration last = Some(part); } if let Some(last) = last { // defer handling the last component to prevent draining entire pattern into base if globbing || matches!(last, Component::Normal(_)) { glob.pattern.push(last); } else { glob.base.push(last); } } glob } /// Classic trie with edges being path components and values being glob patterns. #[derive(Default)] struct Trie<'a> { children: BTreeMap, Trie<'a>>, patterns: Vec<&'a Path>, } impl<'a> Trie<'a> { fn insert(&mut self, mut components: Components<'a>, pattern: &'a Path) { if let Some(part) = components.next() { self.children .entry(part) .or_default() .insert(components, pattern); } else { self.patterns.push(pattern); } } #[allow(clippy::needless_pass_by_value)] fn collect_patterns( &self, pattern_prefix: PathBuf, group_prefix: PathBuf, patterns: &mut Vec, groups: &mut Vec<(PathBuf, Vec)>, ) { // collect all patterns beneath and including this node for pattern in &self.patterns { patterns.push(pattern_prefix.join(pattern)); } for (part, child) in &self.children { if let Component::Normal(_) = part { // for normal components, collect all descendant patterns ('normal' edges only) child.collect_patterns( pattern_prefix.join(part), group_prefix.join(part), patterns, groups, ); } else { // for non-normal component edges, kick off separate group collection at this node child.collect_groups(group_prefix.join(part), groups); } } } #[allow(clippy::needless_pass_by_value)] fn collect_groups(&self, prefix: PathBuf, groups: &mut Vec<(PathBuf, Vec)>) { // LCP-style grouping of patterns if self.patterns.is_empty() { // no patterns in this node; child nodes can form independent groups for (part, child) in &self.children { child.collect_groups(prefix.join(part), groups); } } else { // pivot point, we've hit a pattern node; we have to stop here and form a group let mut group = Vec::new(); self.collect_patterns(PathBuf::new(), prefix.clone(), &mut group, groups); groups.push((prefix, group)); } } } /// Given a collection of globs, cluster them into (base, globs) groups so that: /// - base doesn't contain any glob symbols /// - each directory would only be walked at most once /// - base of each group is the longest common prefix of globs in the group pub(crate) fn cluster_globs(patterns: &[impl AsRef]) -> Vec<(PathBuf, Vec)> { // split all globs into base/pattern let globs: Vec<_> = patterns.iter().map(split_glob).collect(); // construct a path trie out of all split globs let mut trie = Trie::default(); for glob in &globs { trie.insert(glob.base.components(), &glob.pattern); } // run LCP-style aggregation of patterns in the trie into groups let mut groups = Vec::new(); trie.collect_groups(PathBuf::new(), &mut groups); // finally, convert resulting patterns to strings groups .into_iter() .map(|(base, patterns)| { ( base, patterns .iter() // NOTE: this unwrap is ok because input patterns are valid utf-8 .map(|p| p.to_str().unwrap().to_owned()) .collect(), ) }) .collect() } #[cfg(test)] mod tests { use super::{GlobParts, cluster_globs, split_glob}; fn windowsify(path: &str) -> String { if cfg!(windows) { path.replace('/', "\\") } else { path.to_owned() } } #[test] fn test_split_glob() { #[track_caller] fn check(input: &str, base: &str, pattern: &str) { let result = split_glob(input); let expected = GlobParts { base: base.into(), pattern: pattern.into(), }; assert_eq!(result, expected, "{input:?} != {base:?} + {pattern:?}"); } check("", "", ""); check("a", "", "a"); check("a/b", "a", "b"); check("a/b/", "a", "b"); check("a/.//b/", "a", "b"); check("./a/b/c", "a/b", "c"); check("c/d/*", "c/d", "*"); check("c/d/*/../*", "c/d", "*/../*"); check("a/?b/c", "a", "?b/c"); check("/a/b/*", "/a/b", "*"); check("../x/*", "../x", "*"); check("a/{b,c}/d", "a", "{b,c}/d"); check("a/[bc]/d", "a", "[bc]/d"); check("*", "", "*"); check("*/*", "", "*/*"); check("..", "..", ""); check("/", "/", ""); } #[test] fn test_cluster_globs() { #[track_caller] fn check(input: &[&str], expected: &[(&str, &[&str])]) { let input = input.iter().map(|s| windowsify(s)).collect::>(); let mut result_sorted = cluster_globs(&input); for (_, patterns) in &mut result_sorted { patterns.sort_unstable(); } result_sorted.sort_unstable(); let mut expected_sorted = Vec::new(); for (base, patterns) in expected { let mut patterns_sorted = Vec::new(); for pattern in *patterns { patterns_sorted.push(windowsify(pattern)); } patterns_sorted.sort_unstable(); expected_sorted.push((windowsify(base).into(), patterns_sorted)); } expected_sorted.sort_unstable(); assert_eq!( result_sorted, expected_sorted, "{input:?} != {expected_sorted:?} (got: {result_sorted:?})" ); } check(&["a/b/*", "a/c/*"], &[("a/b", &["*"]), ("a/c", &["*"])]); check(&["./a/b/*", "a/c/*"], &[("a/b", &["*"]), ("a/c", &["*"])]); check(&["/a/b/*", "/a/c/*"], &[("/a/b", &["*"]), ("/a/c", &["*"])]); check( &["../a/b/*", "../a/c/*"], &[("../a/b", &["*"]), ("../a/c", &["*"])], ); check(&["x/*", "y/*"], &[("x", &["*"]), ("y", &["*"])]); check(&[], &[]); check( &["./*", "a/*", "../foo/*.png"], &[("", &["*", "a/*"]), ("../foo", &["*.png"])], ); check( &[ "?", "/foo/?", "/foo/bar/*", "../bar/*.png", "../bar/../baz/*.jpg", ], &[ ("", &["?"]), ("/foo", &["?", "bar/*"]), ("../bar", &["*.png"]), ("../bar/../baz", &["*.jpg"]), ], ); check(&["/abs/path/*"], &[("/abs/path", &["*"])]); check(&["/abs/*", "rel/*"], &[("/abs", &["*"]), ("rel", &["*"])]); check(&["a/{b,c}/*", "a/d?/*"], &[("a", &["{b,c}/*", "d?/*"])]); check( &[ "../shared/a/[abc].png", "../shared/a/b/*", "../shared/b/c/?x/d", "docs/important/*.{doc,xls}", "docs/important/very/*", ], &[ ("../shared/a", &["[abc].png", "b/*"]), ("../shared/b/c", &["?x/d"]), ("docs/important", &["*.{doc,xls}", "very/*"]), ], ); check(&["file.txt"], &[("", &["file.txt"])]); check(&["/"], &[("/", &[""])]); check(&[".."], &[("..", &[""])]); check( &["file1.txt", "file2.txt"], &[("", &["file1.txt", "file2.txt"])], ); check( &["a/file1.txt", "a/file2.txt"], &[("a", &["file1.txt", "file2.txt"])], ); check( &["*", "a/b/*", "a/../c/*.jpg", "a/../c/*.png", "/a/*", "/b/*"], &[ ("", &["*", "a/b/*"]), ("a/../c", &["*.jpg", "*.png"]), ("/a", &["*"]), ("/b", &["*"]), ], ); if cfg!(windows) { check( &[ r"\\foo\bar\shared/a/[abc].png", r"\\foo\bar\shared/a/b/*", r"\\foo\bar/shared/b/c/?x/d", r"D:\docs\important/*.{doc,xls}", r"D:\docs/important/very/*", ], &[ (r"\\foo\bar\shared\a", &["[abc].png", r"b\*"]), (r"\\foo\bar\shared\b\c", &[r"?x\d"]), (r"D:\docs\important", &["*.{doc,xls}", r"very\*"]), ], ); } } } uv-0.9.17+ds1/crates/uv-cache-info/src/lib.rs000066400000000000000000000001631520155276700205470ustar00rootroot00000000000000pub use crate::cache_info::*; pub use crate::timestamp::*; mod cache_info; mod git_info; mod glob; mod timestamp; uv-0.9.17+ds1/crates/uv-cache-info/src/timestamp.rs000066400000000000000000000034321520155276700220060ustar00rootroot00000000000000use serde::{Deserialize, Serialize}; use std::path::Path; /// A timestamp used to measure changes to a file. /// /// On Unix, this uses `ctime` as a conservative approach. `ctime` should detect all /// modifications, including some that we don't care about, like hardlink modifications. /// On other platforms, it uses `mtime`. /// /// See: /// See: #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] pub struct Timestamp(std::time::SystemTime); impl Timestamp { /// Return the [`Timestamp`] for the given path. pub fn from_path(path: impl AsRef) -> std::io::Result { let metadata = fs_err::metadata(path.as_ref())?; Ok(Self::from_metadata(&metadata)) } /// Return the [`Timestamp`] for the given metadata. pub fn from_metadata(metadata: &std::fs::Metadata) -> Self { #[cfg(unix)] { use std::os::unix::fs::MetadataExt; let ctime = u64::try_from(metadata.ctime()).expect("ctime to be representable as u64"); let ctime_nsec = u32::try_from(metadata.ctime_nsec()) .expect("ctime_nsec to be representable as u32"); let duration = std::time::Duration::new(ctime, ctime_nsec); Self(std::time::UNIX_EPOCH + duration) } #[cfg(not(unix))] { let modified = metadata.modified().expect("modified time to be available"); Self(modified) } } /// Return the current [`Timestamp`]. pub fn now() -> Self { Self(std::time::SystemTime::now()) } } impl From for Timestamp { fn from(system_time: std::time::SystemTime) -> Self { Self(system_time) } } uv-0.9.17+ds1/crates/uv-cache-key/000077500000000000000000000000001520155276700165015ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-cache-key/Cargo.toml000066400000000000000000000010571520155276700204340ustar00rootroot00000000000000[package] name = "uv-cache-key" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-redacted = { workspace = true } hex = { workspace = true } memchr = { workspace = true } percent-encoding = { workspace = true } seahash = { workspace = true } url = { workspace = true } uv-0.9.17+ds1/crates/uv-cache-key/README.md000066400000000000000000000010331520155276700177550ustar00rootroot00000000000000 # uv-cache-key This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-cache-key). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-cache-key/src/000077500000000000000000000000001520155276700172705ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-cache-key/src/cache_key.rs000066400000000000000000000202171520155276700215530ustar00rootroot00000000000000use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; use std::hash::{Hash, Hasher}; use std::num::{ NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, }; use std::path::{Path, PathBuf}; use seahash::SeaHasher; use url::Url; /// A trait for types that can be hashed in a stable way across versions and platforms. Equivalent /// to Ruff's [`CacheKey`] trait. pub trait CacheKey { fn cache_key(&self, state: &mut CacheKeyHasher); fn cache_key_slice(data: &[Self], state: &mut CacheKeyHasher) where Self: Sized, { for piece in data { piece.cache_key(state); } } } impl CacheKey for bool { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_u8(u8::from(*self)); } } impl CacheKey for char { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_u32(*self as u32); } } impl CacheKey for usize { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_usize(*self); } } impl CacheKey for u128 { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_u128(*self); } } impl CacheKey for u64 { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_u64(*self); } } impl CacheKey for u32 { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_u32(*self); } } impl CacheKey for u16 { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_u16(*self); } } impl CacheKey for u8 { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_u8(*self); } } impl CacheKey for isize { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_isize(*self); } } impl CacheKey for i128 { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_i128(*self); } } impl CacheKey for i64 { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_i64(*self); } } impl CacheKey for i32 { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_i32(*self); } } impl CacheKey for i16 { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_i16(*self); } } impl CacheKey for i8 { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_i8(*self); } } macro_rules! impl_cache_key_non_zero { ($name:ident) => { impl CacheKey for $name { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { self.get().cache_key(state) } } }; } impl_cache_key_non_zero!(NonZeroU8); impl_cache_key_non_zero!(NonZeroU16); impl_cache_key_non_zero!(NonZeroU32); impl_cache_key_non_zero!(NonZeroU64); impl_cache_key_non_zero!(NonZeroU128); impl_cache_key_non_zero!(NonZeroI8); impl_cache_key_non_zero!(NonZeroI16); impl_cache_key_non_zero!(NonZeroI32); impl_cache_key_non_zero!(NonZeroI64); impl_cache_key_non_zero!(NonZeroI128); macro_rules! impl_cache_key_tuple { () => ( impl CacheKey for () { #[inline] fn cache_key(&self, _state: &mut CacheKeyHasher) {} } ); ( $($name:ident)+) => ( impl<$($name: CacheKey),+> CacheKey for ($($name,)+) where last_type!($($name,)+): ?Sized { #[allow(non_snake_case)] #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { let ($(ref $name,)+) = *self; $($name.cache_key(state);)+ } } ); } macro_rules! last_type { ($a:ident,) => { $a }; ($a:ident, $($rest_a:ident,)+) => { last_type!($($rest_a,)+) }; } impl_cache_key_tuple! {} impl_cache_key_tuple! { T } impl_cache_key_tuple! { T B } impl_cache_key_tuple! { T B C } impl_cache_key_tuple! { T B C D } impl_cache_key_tuple! { T B C D E } impl_cache_key_tuple! { T B C D E F } impl_cache_key_tuple! { T B C D E F G } impl_cache_key_tuple! { T B C D E F G H } impl_cache_key_tuple! { T B C D E F G H I } impl_cache_key_tuple! { T B C D E F G H I J } impl_cache_key_tuple! { T B C D E F G H I J K } impl_cache_key_tuple! { T B C D E F G H I J K L } impl CacheKey for str { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { self.hash(&mut *state); } } impl CacheKey for String { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { self.hash(&mut *state); } } impl CacheKey for Path { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { self.hash(&mut *state); } } impl CacheKey for PathBuf { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { self.as_path().cache_key(state); } } impl CacheKey for Url { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { self.as_str().cache_key(state); } } impl CacheKey for Option { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { match self { None => state.write_usize(0), Some(value) => { state.write_usize(1); value.cache_key(state); } } } } impl CacheKey for [T] { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_usize(self.len()); CacheKey::cache_key_slice(self, state); } } impl CacheKey for &T { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { (**self).cache_key(state); } } impl CacheKey for &mut T { #[inline] fn cache_key(&self, state: &mut CacheKeyHasher) { (**self).cache_key(state); } } impl CacheKey for Vec where T: CacheKey, { fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_usize(self.len()); CacheKey::cache_key_slice(self, state); } } impl CacheKey for BTreeSet { fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_usize(self.len()); for item in self { item.cache_key(state); } } } impl CacheKey for BTreeMap { fn cache_key(&self, state: &mut CacheKeyHasher) { state.write_usize(self.len()); for (key, value) in self { key.cache_key(state); value.cache_key(state); } } } impl CacheKey for Cow<'_, V> where V: CacheKey + ToOwned, { fn cache_key(&self, state: &mut CacheKeyHasher) { (**self).cache_key(state); } } #[derive(Clone, Default)] pub struct CacheKeyHasher { inner: SeaHasher, } impl CacheKeyHasher { pub fn new() -> Self { Self { inner: SeaHasher::new(), } } } impl Hasher for CacheKeyHasher { #[inline] fn finish(&self) -> u64 { self.inner.finish() } #[inline] fn write(&mut self, bytes: &[u8]) { self.inner.write(bytes); } #[inline] fn write_u8(&mut self, i: u8) { self.inner.write_u8(i); } #[inline] fn write_u16(&mut self, i: u16) { self.inner.write_u16(i); } #[inline] fn write_u32(&mut self, i: u32) { self.inner.write_u32(i); } #[inline] fn write_u64(&mut self, i: u64) { self.inner.write_u64(i); } #[inline] fn write_u128(&mut self, i: u128) { self.inner.write_u128(i); } #[inline] fn write_usize(&mut self, i: usize) { self.inner.write_usize(i); } #[inline] fn write_i8(&mut self, i: i8) { self.inner.write_i8(i); } #[inline] fn write_i16(&mut self, i: i16) { self.inner.write_i16(i); } #[inline] fn write_i32(&mut self, i: i32) { self.inner.write_i32(i); } #[inline] fn write_i64(&mut self, i: i64) { self.inner.write_i64(i); } #[inline] fn write_i128(&mut self, i: i128) { self.inner.write_i128(i); } #[inline] fn write_isize(&mut self, i: isize) { self.inner.write_isize(i); } } uv-0.9.17+ds1/crates/uv-cache-key/src/canonical_url.rs000066400000000000000000000442701520155276700224560ustar00rootroot00000000000000use std::borrow::Cow; use std::fmt::{Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::ops::Deref; use url::Url; use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError}; use crate::cache_key::{CacheKey, CacheKeyHasher}; /// A wrapper around `Url` which represents a "canonical" version of an original URL. /// /// A "canonical" url is only intended for internal comparison purposes. It's to help paper over /// mistakes such as depending on `github.com/foo/bar` vs. `github.com/foo/bar.git`. /// /// This is **only** for internal purposes and provides no means to actually read the underlying /// string value of the `Url` it contains. This is intentional, because all fetching should still /// happen within the context of the original URL. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct CanonicalUrl(DisplaySafeUrl); impl CanonicalUrl { pub fn new(url: &DisplaySafeUrl) -> Self { let mut url = url.clone(); // If the URL cannot be a base, then it's not a valid URL anyway. if url.cannot_be_a_base() { return Self(url); } // Strip credentials. let _ = url.set_password(None); let _ = url.set_username(""); // Strip a trailing slash. if url.path().ends_with('/') { url.path_segments_mut().unwrap().pop_if_empty(); } // For GitHub URLs specifically, just lower-case everything. GitHub // treats both the same, but they hash differently, and we're gonna be // hashing them. This wants a more general solution, and also we're // almost certainly not using the same case conversion rules that GitHub // does. (See issue #84) if url.host_str() == Some("github.com") { let scheme = url.scheme().to_lowercase(); url.set_scheme(&scheme).unwrap(); let path = url.path().to_lowercase(); url.set_path(&path); } // Repos can generally be accessed with or without `.git` extension. if let Some((prefix, suffix)) = url.path().rsplit_once('@') { // Ex) `git+https://github.com/pypa/sample-namespace-packages.git@2.0.0` let needs_chopping = std::path::Path::new(prefix) .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("git")); if needs_chopping { let prefix = &prefix[..prefix.len() - 4]; let path = format!("{prefix}@{suffix}"); url.set_path(&path); } } else { // Ex) `git+https://github.com/pypa/sample-namespace-packages.git` let needs_chopping = std::path::Path::new(url.path()) .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("git")); if needs_chopping { let last = { // Unwrap safety: We checked `url.cannot_be_a_base()`, and `url.path()` having // an extension implies at least one segment. let last = url.path_segments().unwrap().next_back().unwrap(); last[..last.len() - 4].to_owned() }; url.path_segments_mut().unwrap().pop().push(&last); } } // Decode any percent-encoded characters in the path. if memchr::memchr(b'%', url.path().as_bytes()).is_some() { // Unwrap safety: We checked `url.cannot_be_a_base()`. let decoded = url .path_segments() .unwrap() .map(|segment| { percent_encoding::percent_decode_str(segment) .decode_utf8() .unwrap_or(Cow::Borrowed(segment)) .into_owned() }) .collect::>(); let mut path_segments = url.path_segments_mut().unwrap(); path_segments.clear(); path_segments.extend(decoded); } Self(url) } pub fn parse(url: &str) -> Result { Ok(Self::new(&DisplaySafeUrl::parse(url)?)) } } impl CacheKey for CanonicalUrl { fn cache_key(&self, state: &mut CacheKeyHasher) { // `as_str` gives the serialisation of a url (which has a spec) and so insulates against // possible changes in how the URL crate does hashing. self.0.as_str().cache_key(state); } } impl Hash for CanonicalUrl { fn hash(&self, state: &mut H) { // `as_str` gives the serialisation of a url (which has a spec) and so insulates against // possible changes in how the URL crate does hashing. self.0.as_str().hash(state); } } impl From for DisplaySafeUrl { fn from(value: CanonicalUrl) -> Self { value.0 } } impl std::fmt::Display for CanonicalUrl { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(&self.0, f) } } /// Like [`CanonicalUrl`], but attempts to represent an underlying source repository, abstracting /// away details like the specific commit or branch, or the subdirectory to build within the /// repository. /// /// For example, `https://github.com/pypa/package.git#subdirectory=pkg_a` and /// `https://github.com/pypa/package.git#subdirectory=pkg_b` would map to different /// [`CanonicalUrl`] values, but the same [`RepositoryUrl`], since they map to the same /// resource. /// /// The additional information it holds should only be used to discriminate between /// sources that hold the exact same commit in their canonical representation, /// but may differ in the contents such as when Git LFS is enabled. /// /// A different cache key will be computed when Git LFS is enabled. /// When Git LFS is `false` or `None`, the cache key remains unchanged. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct RepositoryUrl { repo_url: DisplaySafeUrl, with_lfs: Option, } impl RepositoryUrl { pub fn new(url: &DisplaySafeUrl) -> Self { let mut url = CanonicalUrl::new(url).0; // If a Git URL ends in a reference (like a branch, tag, or commit), remove it. if url.scheme().starts_with("git+") { if let Some(prefix) = url .path() .rsplit_once('@') .map(|(prefix, _suffix)| prefix.to_string()) { url.set_path(&prefix); } } // Drop any fragments and query parameters. url.set_fragment(None); url.set_query(None); Self { repo_url: url, with_lfs: None, } } pub fn parse(url: &str) -> Result { Ok(Self::new(&DisplaySafeUrl::parse(url)?)) } #[must_use] pub fn with_lfs(mut self, lfs: Option) -> Self { self.with_lfs = lfs; self } } impl CacheKey for RepositoryUrl { fn cache_key(&self, state: &mut CacheKeyHasher) { // `as_str` gives the serialisation of a url (which has a spec) and so insulates against // possible changes in how the URL crate does hashing. self.repo_url.as_str().cache_key(state); if let Some(true) = self.with_lfs { 1u8.cache_key(state); } } } impl Hash for RepositoryUrl { fn hash(&self, state: &mut H) { // `as_str` gives the serialisation of a url (which has a spec) and so insulates against // possible changes in how the URL crate does hashing. self.repo_url.as_str().hash(state); if let Some(true) = self.with_lfs { 1u8.hash(state); } } } impl Deref for RepositoryUrl { type Target = Url; fn deref(&self) -> &Self::Target { &self.repo_url } } impl std::fmt::Display for RepositoryUrl { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(&self.repo_url, f) } } #[cfg(test)] mod tests { use super::*; #[test] fn user_credential_does_not_affect_cache_key() -> Result<(), DisplaySafeUrlError> { let mut hasher = CacheKeyHasher::new(); CanonicalUrl::parse("https://example.com/pypa/sample-namespace-packages.git@2.0.0")? .cache_key(&mut hasher); let hash_without_creds = hasher.finish(); let mut hasher = CacheKeyHasher::new(); CanonicalUrl::parse( "https://user:foo@example.com/pypa/sample-namespace-packages.git@2.0.0", )? .cache_key(&mut hasher); let hash_with_creds = hasher.finish(); assert_eq!( hash_without_creds, hash_with_creds, "URLs with no user credentials should hash the same as URLs with different user credentials", ); let mut hasher = CacheKeyHasher::new(); CanonicalUrl::parse( "https://user:bar@example.com/pypa/sample-namespace-packages.git@2.0.0", )? .cache_key(&mut hasher); let hash_with_creds = hasher.finish(); assert_eq!( hash_without_creds, hash_with_creds, "URLs with different user credentials should hash the same", ); let mut hasher = CacheKeyHasher::new(); CanonicalUrl::parse("https://:bar@example.com/pypa/sample-namespace-packages.git@2.0.0")? .cache_key(&mut hasher); let hash_with_creds = hasher.finish(); assert_eq!( hash_without_creds, hash_with_creds, "URLs with no username, though with a password, should hash the same as URLs with different user credentials", ); let mut hasher = CacheKeyHasher::new(); CanonicalUrl::parse("https://user:@example.com/pypa/sample-namespace-packages.git@2.0.0")? .cache_key(&mut hasher); let hash_with_creds = hasher.finish(); assert_eq!( hash_without_creds, hash_with_creds, "URLs with no password, though with a username, should hash the same as URLs with different user credentials", ); Ok(()) } #[test] fn canonical_url() -> Result<(), DisplaySafeUrlError> { // Two URLs should be considered equal regardless of the `.git` suffix. assert_eq!( CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages")?, ); // Two URLs should be considered equal regardless of the `.git` suffix. assert_eq!( CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git@2.0.0")?, CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages@2.0.0")?, ); // Two URLs should be _not_ considered equal if they point to different repositories. assert_ne!( CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, CanonicalUrl::parse("git+https://github.com/pypa/sample-packages.git")?, ); // Two URLs should _not_ be considered equal if they request different subdirectories. assert_ne!( CanonicalUrl::parse( "git+https://github.com/pypa/sample-namespace-packages.git#subdirectory=pkg_resources/pkg_a" )?, CanonicalUrl::parse( "git+https://github.com/pypa/sample-namespace-packages.git#subdirectory=pkg_resources/pkg_b" )?, ); // Two URLs should _not_ be considered equal if they differ in Git LFS enablement. assert_ne!( CanonicalUrl::parse( "git+https://github.com/pypa/sample-namespace-packages.git#lfs=true" )?, CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, ); // Two URLs should _not_ be considered equal if they request different commit tags. assert_ne!( CanonicalUrl::parse( "git+https://github.com/pypa/sample-namespace-packages.git@v1.0.0" )?, CanonicalUrl::parse( "git+https://github.com/pypa/sample-namespace-packages.git@v2.0.0" )?, ); // Two URLs that cannot be a base should be considered equal. assert_eq!( CanonicalUrl::parse("git+https:://github.com/pypa/sample-namespace-packages.git")?, CanonicalUrl::parse("git+https:://github.com/pypa/sample-namespace-packages.git")?, ); // Two URLs should _not_ be considered equal based on percent-decoding slashes. assert_ne!( CanonicalUrl::parse("https://github.com/pypa/sample%2Fnamespace%2Fpackages")?, CanonicalUrl::parse("https://github.com/pypa/sample/namespace/packages")?, ); // Two URLs should be considered equal regardless of percent-encoding. assert_eq!( CanonicalUrl::parse("https://github.com/pypa/sample%2Bnamespace%2Bpackages")?, CanonicalUrl::parse("https://github.com/pypa/sample+namespace+packages")?, ); // Two URLs should _not_ be considered equal based on percent-decoding slashes. assert_ne!( CanonicalUrl::parse( "file:///home/ferris/my_project%2Fmy_project-0.1.0-py3-none-any.whl" )?, CanonicalUrl::parse( "file:///home/ferris/my_project/my_project-0.1.0-py3-none-any.whl" )?, ); // Two URLs should be considered equal regardless of percent-encoding. assert_eq!( CanonicalUrl::parse( "file:///home/ferris/my_project/my_project-0.1.0+foo-py3-none-any.whl" )?, CanonicalUrl::parse( "file:///home/ferris/my_project/my_project-0.1.0%2Bfoo-py3-none-any.whl" )?, ); Ok(()) } #[test] fn repository_url() -> Result<(), DisplaySafeUrlError> { // Two URLs should be considered equal regardless of the `.git` suffix. assert_eq!( RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages")?, ); // Two URLs should be considered equal regardless of the `.git` suffix. assert_eq!( RepositoryUrl::parse( "git+https://github.com/pypa/sample-namespace-packages.git@2.0.0" )?, RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages@2.0.0")?, ); // Two URLs should be _not_ considered equal if they point to different repositories. assert_ne!( RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, RepositoryUrl::parse("git+https://github.com/pypa/sample-packages.git")?, ); // Two URLs should be considered equal if they map to the same repository, even if they // request different subdirectories. assert_eq!( RepositoryUrl::parse( "git+https://github.com/pypa/sample-namespace-packages.git#subdirectory=pkg_resources/pkg_a" )?, RepositoryUrl::parse( "git+https://github.com/pypa/sample-namespace-packages.git#subdirectory=pkg_resources/pkg_b" )?, ); // Two URLs should be considered equal if they map to the same repository, even if they // request different commit tags. assert_eq!( RepositoryUrl::parse( "git+https://github.com/pypa/sample-namespace-packages.git@v1.0.0" )?, RepositoryUrl::parse( "git+https://github.com/pypa/sample-namespace-packages.git@v2.0.0" )?, ); // Two URLs should be considered equal if they map to the same repository, even if they // differ in Git LFS enablement. assert_eq!( RepositoryUrl::parse( "git+https://github.com/pypa/sample-namespace-packages.git#lfs=true" )?, RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, ); Ok(()) } #[test] fn repository_url_with_lfs() -> Result<(), DisplaySafeUrlError> { let mut hasher = CacheKeyHasher::new(); RepositoryUrl::parse("https://example.com/pypa/sample-namespace-packages.git@2.0.0")? .cache_key(&mut hasher); let repo_url_basic = hasher.finish(); let mut hasher = CacheKeyHasher::new(); RepositoryUrl::parse( "https://user:foo@example.com/pypa/sample-namespace-packages.git@2.0.0#foo=bar", )? .cache_key(&mut hasher); let repo_url_with_fragments = hasher.finish(); assert_eq!( repo_url_basic, repo_url_with_fragments, "repository urls should have the exact cache keys as fragments are removed", ); let mut hasher = CacheKeyHasher::new(); RepositoryUrl::parse( "https://user:foo@example.com/pypa/sample-namespace-packages.git@2.0.0#foo=bar", )? .with_lfs(None) .cache_key(&mut hasher); let git_url_with_fragments = hasher.finish(); assert_eq!( repo_url_with_fragments, git_url_with_fragments, "both structs should have the exact cache keys as fragments are still removed", ); let mut hasher = CacheKeyHasher::new(); RepositoryUrl::parse( "https://user:foo@example.com/pypa/sample-namespace-packages.git@2.0.0#foo=bar", )? .with_lfs(Some(false)) .cache_key(&mut hasher); let git_url_with_fragments_and_lfs_false = hasher.finish(); assert_eq!( git_url_with_fragments, git_url_with_fragments_and_lfs_false, "both structs should have the exact cache keys as lfs false should not influence them", ); let mut hasher = CacheKeyHasher::new(); RepositoryUrl::parse( "https://user:foo@example.com/pypa/sample-namespace-packages.git@2.0.0#foo=bar", )? .with_lfs(Some(true)) .cache_key(&mut hasher); let git_url_with_fragments_and_lfs_true = hasher.finish(); assert_ne!( git_url_with_fragments, git_url_with_fragments_and_lfs_true, "both structs should have different cache keys as one has Git LFS enabled", ); Ok(()) } } uv-0.9.17+ds1/crates/uv-cache-key/src/digest.rs000066400000000000000000000020141520155276700211120ustar00rootroot00000000000000use std::hash::{Hash, Hasher}; use seahash::SeaHasher; use crate::cache_key::{CacheKey, CacheKeyHasher}; /// Compute a hex string hash of a `CacheKey` object. /// /// The value returned by [`cache_digest`] should be stable across releases and platforms. pub fn cache_digest(hashable: &H) -> String { /// Compute a u64 hash of a [`CacheKey`] object. fn cache_key_u64(hashable: &H) -> u64 { let mut hasher = CacheKeyHasher::new(); hashable.cache_key(&mut hasher); hasher.finish() } to_hex(cache_key_u64(hashable)) } /// Compute a hex string hash of a hashable object. pub fn hash_digest(hashable: &H) -> String { /// Compute a u64 hash of a hashable object. fn hash_u64(hashable: &H) -> u64 { let mut hasher = SeaHasher::new(); hashable.hash(&mut hasher); hasher.finish() } to_hex(hash_u64(hashable)) } /// Convert a u64 to a hex string. fn to_hex(num: u64) -> String { hex::encode(num.to_le_bytes()) } uv-0.9.17+ds1/crates/uv-cache-key/src/lib.rs000066400000000000000000000003011520155276700203760ustar00rootroot00000000000000pub use cache_key::{CacheKey, CacheKeyHasher}; pub use canonical_url::{CanonicalUrl, RepositoryUrl}; pub use digest::{cache_digest, hash_digest}; mod cache_key; mod canonical_url; mod digest; uv-0.9.17+ds1/crates/uv-cache/000077500000000000000000000000001520155276700157135ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-cache/Cargo.toml000066400000000000000000000021251520155276700176430ustar00rootroot00000000000000[package] name = "uv-cache" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-cache-info = { workspace = true } uv-cache-key = { workspace = true } uv-dirs = { workspace = true } uv-distribution-types = { workspace = true } uv-fs = { workspace = true, features = ["tokio"] } uv-normalize = { workspace = true } uv-pypi-types = { workspace = true } uv-redacted = { workspace = true } uv-static = { workspace = true } clap = { workspace = true, features = ["derive", "env"], optional = true } fs-err = { workspace = true, features = ["tokio"] } nanoid = { workspace = true } rmp-serde = { workspace = true } rustc-hash = { workspace = true } same-file = { workspace = true } serde = { workspace = true, features = ["derive"] } tempfile = { workspace = true } tracing = { workspace = true } walkdir = { workspace = true } uv-0.9.17+ds1/crates/uv-cache/README.md000066400000000000000000000010231520155276700171660ustar00rootroot00000000000000 # uv-cache This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-cache). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-cache/src/000077500000000000000000000000001520155276700165025ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-cache/src/archive.rs000066400000000000000000000015361520155276700204760ustar00rootroot00000000000000use std::path::Path; use std::str::FromStr; /// A unique identifier for an archive (unzipped wheel) in the cache. #[derive(Debug, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] pub struct ArchiveId(String); impl Default for ArchiveId { fn default() -> Self { Self::new() } } impl ArchiveId { /// Generate a new unique identifier for an archive. pub fn new() -> Self { Self(nanoid::nanoid!()) } } impl AsRef for ArchiveId { fn as_ref(&self) -> &Path { self.0.as_ref() } } impl std::fmt::Display for ArchiveId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } impl FromStr for ArchiveId { type Err = ::Err; fn from_str(s: &str) -> Result { Ok(Self(s.to_string())) } } uv-0.9.17+ds1/crates/uv-cache/src/by_timestamp.rs000066400000000000000000000002771520155276700215530ustar00rootroot00000000000000use serde::{Deserialize, Serialize}; use uv_cache_info::Timestamp; #[derive(Deserialize, Serialize)] pub struct CachedByTimestamp { pub timestamp: Timestamp, pub data: Data, } uv-0.9.17+ds1/crates/uv-cache/src/cli.rs000066400000000000000000000113641520155276700176240ustar00rootroot00000000000000use std::io; use std::path::{Path, PathBuf}; use uv_static::EnvVars; use crate::Cache; use clap::Parser; use tracing::{debug, warn}; #[derive(Parser, Debug, Clone)] #[command(next_help_heading = "Cache options")] pub struct CacheArgs { /// Avoid reading from or writing to the cache, instead using a temporary directory for the /// duration of the operation. #[arg( global = true, long, short, alias = "no-cache-dir", env = EnvVars::UV_NO_CACHE, value_parser = clap::builder::BoolishValueParser::new(), )] pub no_cache: bool, /// Path to the cache directory. /// /// Defaults to `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on macOS and Linux, and /// `%LOCALAPPDATA%\uv\cache` on Windows. /// /// To view the location of the cache directory, run `uv cache dir`. #[arg(global = true, long, env = EnvVars::UV_CACHE_DIR)] pub cache_dir: Option, } impl Cache { /// Prefer, in order: /// /// 1. A temporary cache directory, if the user requested `--no-cache`. /// 2. The specific cache directory specified by the user via `--cache-dir` or `UV_CACHE_DIR`. /// 3. The system-appropriate cache directory. /// 4. A `.uv_cache` directory in the current working directory. /// /// Returns an absolute cache dir. pub fn from_settings(no_cache: bool, cache_dir: Option) -> Result { if no_cache { Self::temp() } else if let Some(cache_dir) = cache_dir { Ok(Self::from_path(cache_dir)) } else if let Some(cache_dir) = uv_dirs::legacy_user_cache_dir().filter(|dir| dir.exists()) { // If the user has an existing directory at (e.g.) `/Users/user/Library/Caches/uv`, // respect it for backwards compatibility. Otherwise, prefer the XDG strategy, even on // macOS. Ok(Self::from_path(cache_dir)) } else if let Some(cache_dir) = uv_dirs::user_cache_dir() { if cfg!(windows) { // On Windows, we append `cache` to the LocalAppData directory, i.e., prefer // `C:\Users\User\AppData\Local\uv\cache` over `C:\Users\User\AppData\Local\uv`. // // Unfortunately, v0.3.0 and v0.3.1 used the latter, so we need to migrate the cache // for those users. let destination = cache_dir.join("cache"); let source = cache_dir; if let Err(err) = migrate_windows_cache(&source, &destination) { warn!( "Failed to migrate cache from `{}` to `{}`: {err}", source.display(), destination.display() ); } Ok(Self::from_path(destination)) } else { Ok(Self::from_path(cache_dir)) } } else { Ok(Self::from_path(".uv_cache")) } } } impl TryFrom for Cache { type Error = io::Error; fn try_from(value: CacheArgs) -> Result { Self::from_settings(value.no_cache, value.cache_dir) } } /// Migrate the Windows cache from `C:\Users\User\AppData\Local\uv` to `C:\Users\User\AppData\Local\uv\cache`. fn migrate_windows_cache(source: &Path, destination: &Path) -> Result<(), io::Error> { // The list of expected cache buckets in v0.3.0. for directory in [ "built-wheels-v3", "flat-index-v0", "git-v0", "interpreter-v2", "simple-v12", "wheels-v1", "archive-v0", "builds-v0", "environments-v1", ] { let source = source.join(directory); let destination = destination.join(directory); // Migrate the cache bucket. if source.exists() { debug!( "Migrating cache bucket from {} to {}", source.display(), destination.display() ); if let Some(parent) = destination.parent() { fs_err::create_dir_all(parent)?; } fs_err::rename(&source, &destination)?; } } // The list of expected cache files in v0.3.0. for file in [".gitignore", "CACHEDIR.TAG"] { let source = source.join(file); let destination = destination.join(file); // Migrate the cache file. if source.exists() { debug!( "Migrating cache file from {} to {}", source.display(), destination.display() ); if let Some(parent) = destination.parent() { fs_err::create_dir_all(parent)?; } fs_err::rename(&source, &destination)?; } } Ok(()) } uv-0.9.17+ds1/crates/uv-cache/src/lib.rs000066400000000000000000001512611520155276700176240ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use std::io; use std::io::Write; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; use rustc_hash::FxHashMap; use tracing::{debug, trace, warn}; use uv_cache_info::Timestamp; use uv_fs::{LockedFile, LockedFileError, LockedFileMode, Simplified, cachedir, directories}; use uv_normalize::PackageName; use uv_pypi_types::ResolutionMetadata; pub use crate::by_timestamp::CachedByTimestamp; #[cfg(feature = "clap")] pub use crate::cli::CacheArgs; use crate::removal::Remover; pub use crate::removal::{Removal, rm_rf}; pub use crate::wheel::WheelCache; use crate::wheel::WheelCacheKind; pub use archive::ArchiveId; mod archive; mod by_timestamp; #[cfg(feature = "clap")] mod cli; mod removal; mod wheel; /// The version of the archive bucket. /// /// Must be kept in-sync with the version in [`CacheBucket::to_str`]. pub const ARCHIVE_VERSION: u8 = 0; /// A [`CacheEntry`] which may or may not exist yet. #[derive(Debug, Clone)] pub struct CacheEntry(PathBuf); impl CacheEntry { /// Create a new [`CacheEntry`] from a directory and a file name. pub fn new(dir: impl Into, file: impl AsRef) -> Self { Self(dir.into().join(file)) } /// Create a new [`CacheEntry`] from a path. pub fn from_path(path: impl Into) -> Self { Self(path.into()) } /// Return the cache entry's parent directory. pub fn shard(&self) -> CacheShard { CacheShard(self.dir().to_path_buf()) } /// Convert the [`CacheEntry`] into a [`PathBuf`]. #[inline] pub fn into_path_buf(self) -> PathBuf { self.0 } /// Return the path to the [`CacheEntry`]. #[inline] pub fn path(&self) -> &Path { &self.0 } /// Return the cache entry's parent directory. #[inline] pub fn dir(&self) -> &Path { self.0.parent().expect("Cache entry has no parent") } /// Create a new [`CacheEntry`] with the given file name. #[must_use] pub fn with_file(&self, file: impl AsRef) -> Self { Self(self.dir().join(file)) } /// Acquire the [`CacheEntry`] as an exclusive lock. pub async fn lock(&self) -> Result { fs_err::create_dir_all(self.dir())?; LockedFile::acquire( self.path(), LockedFileMode::Exclusive, self.path().display(), ) .await } } impl AsRef for CacheEntry { fn as_ref(&self) -> &Path { &self.0 } } /// A subdirectory within the cache. #[derive(Debug, Clone)] pub struct CacheShard(PathBuf); impl CacheShard { /// Return a [`CacheEntry`] within this shard. pub fn entry(&self, file: impl AsRef) -> CacheEntry { CacheEntry::new(&self.0, file) } /// Return a [`CacheShard`] within this shard. #[must_use] pub fn shard(&self, dir: impl AsRef) -> Self { Self(self.0.join(dir.as_ref())) } /// Acquire the cache entry as an exclusive lock. pub async fn lock(&self) -> Result { fs_err::create_dir_all(self.as_ref())?; LockedFile::acquire( self.join(".lock"), LockedFileMode::Exclusive, self.display(), ) .await } /// Return the [`CacheShard`] as a [`PathBuf`]. pub fn into_path_buf(self) -> PathBuf { self.0 } } impl AsRef for CacheShard { fn as_ref(&self) -> &Path { &self.0 } } impl Deref for CacheShard { type Target = Path; fn deref(&self) -> &Self::Target { &self.0 } } /// The main cache abstraction. /// /// While the cache is active, it holds a read (shared) lock that prevents cache cleaning #[derive(Debug, Clone)] pub struct Cache { /// The cache directory. root: PathBuf, /// The refresh strategy to use when reading from the cache. refresh: Refresh, /// A temporary cache directory, if the user requested `--no-cache`. /// /// Included to ensure that the temporary directory exists for the length of the operation, but /// is dropped at the end as appropriate. temp_dir: Option>, /// Ensure that `uv cache` operations don't remove items from the cache that are used by another /// uv process. lock_file: Option>, } impl Cache { /// A persistent cache directory at `root`. pub fn from_path(root: impl Into) -> Self { Self { root: root.into(), refresh: Refresh::None(Timestamp::now()), temp_dir: None, lock_file: None, } } /// Create a temporary cache directory. pub fn temp() -> Result { let temp_dir = tempfile::tempdir()?; Ok(Self { root: temp_dir.path().to_path_buf(), refresh: Refresh::None(Timestamp::now()), temp_dir: Some(Arc::new(temp_dir)), lock_file: None, }) } /// Set the [`Refresh`] policy for the cache. #[must_use] pub fn with_refresh(self, refresh: Refresh) -> Self { Self { refresh, ..self } } /// Acquire a lock that allows removing entries from the cache. pub async fn with_exclusive_lock(self) -> Result { let Self { root, refresh, temp_dir, lock_file, } = self; // Release the existing lock, avoid deadlocks from a cloned cache. if let Some(lock_file) = lock_file { drop( Arc::try_unwrap(lock_file).expect( "cloning the cache before acquiring an exclusive lock causes a deadlock", ), ); } let lock_file = LockedFile::acquire( root.join(".lock"), LockedFileMode::Exclusive, root.simplified_display(), ) .await?; Ok(Self { root, refresh, temp_dir, lock_file: Some(Arc::new(lock_file)), }) } /// Acquire a lock that allows removing entries from the cache, if available. /// /// If the lock is not immediately available, returns [`Err`] with self. pub fn with_exclusive_lock_no_wait(self) -> Result { let Self { root, refresh, temp_dir, lock_file, } = self; match LockedFile::acquire_no_wait( root.join(".lock"), LockedFileMode::Exclusive, root.simplified_display(), ) { Some(lock_file) => Ok(Self { root, refresh, temp_dir, lock_file: Some(Arc::new(lock_file)), }), None => Err(Self { root, refresh, temp_dir, lock_file, }), } } /// Return the root of the cache. pub fn root(&self) -> &Path { &self.root } /// Return the [`Refresh`] policy for the cache. pub fn refresh(&self) -> &Refresh { &self.refresh } /// The folder for a specific cache bucket pub fn bucket(&self, cache_bucket: CacheBucket) -> PathBuf { self.root.join(cache_bucket.to_str()) } /// Compute an entry in the cache. pub fn shard(&self, cache_bucket: CacheBucket, dir: impl AsRef) -> CacheShard { CacheShard(self.bucket(cache_bucket).join(dir.as_ref())) } /// Compute an entry in the cache. pub fn entry( &self, cache_bucket: CacheBucket, dir: impl AsRef, file: impl AsRef, ) -> CacheEntry { CacheEntry::new(self.bucket(cache_bucket).join(dir), file) } /// Return the path to an archive in the cache. pub fn archive(&self, id: &ArchiveId) -> PathBuf { self.bucket(CacheBucket::Archive).join(id) } /// Create a temporary directory to be used as a Python virtual environment. pub fn venv_dir(&self) -> io::Result { fs_err::create_dir_all(self.bucket(CacheBucket::Builds))?; tempfile::tempdir_in(self.bucket(CacheBucket::Builds)) } /// Create a temporary directory to be used for executing PEP 517 source distribution builds. pub fn build_dir(&self) -> io::Result { fs_err::create_dir_all(self.bucket(CacheBucket::Builds))?; tempfile::tempdir_in(self.bucket(CacheBucket::Builds)) } /// Returns `true` if a cache entry must be revalidated given the [`Refresh`] policy. pub fn must_revalidate_package(&self, package: &PackageName) -> bool { match &self.refresh { Refresh::None(_) => false, Refresh::All(_) => true, Refresh::Packages(packages, _, _) => packages.contains(package), } } /// Returns `true` if a cache entry must be revalidated given the [`Refresh`] policy. pub fn must_revalidate_path(&self, path: &Path) -> bool { match &self.refresh { Refresh::None(_) => false, Refresh::All(_) => true, Refresh::Packages(_, paths, _) => paths .iter() .any(|target| same_file::is_same_file(path, target).unwrap_or(false)), } } /// Returns the [`Freshness`] for a cache entry, validating it against the [`Refresh`] policy. /// /// A cache entry is considered fresh if it was created after the cache itself was /// initialized, or if the [`Refresh`] policy does not require revalidation. pub fn freshness( &self, entry: &CacheEntry, package: Option<&PackageName>, path: Option<&Path>, ) -> io::Result { // Grab the cutoff timestamp, if it's relevant. let timestamp = match &self.refresh { Refresh::None(_) => return Ok(Freshness::Fresh), Refresh::All(timestamp) => timestamp, Refresh::Packages(packages, paths, timestamp) => { if package.is_none_or(|package| packages.contains(package)) || path.is_some_and(|path| { paths .iter() .any(|target| same_file::is_same_file(path, target).unwrap_or(false)) }) { timestamp } else { return Ok(Freshness::Fresh); } } }; match fs_err::metadata(entry.path()) { Ok(metadata) => { if Timestamp::from_metadata(&metadata) >= *timestamp { Ok(Freshness::Fresh) } else { Ok(Freshness::Stale) } } Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(Freshness::Missing), Err(err) => Err(err), } } /// Persist a temporary directory to the artifact store, returning its unique ID. pub async fn persist( &self, temp_dir: impl AsRef, path: impl AsRef, ) -> io::Result { // Create a unique ID for the artifact. // TODO(charlie): Support content-addressed persistence via SHAs. let id = ArchiveId::new(); // Move the temporary directory into the directory store. let archive_entry = self.entry(CacheBucket::Archive, "", &id); fs_err::create_dir_all(archive_entry.dir())?; uv_fs::rename_with_retry(temp_dir.as_ref(), archive_entry.path()).await?; // Create a symlink to the directory store. fs_err::create_dir_all(path.as_ref().parent().expect("Cache entry to have parent"))?; self.create_link(&id, path.as_ref())?; Ok(id) } /// Returns `true` if the [`Cache`] is temporary. pub fn is_temporary(&self) -> bool { self.temp_dir.is_some() } /// Populate the cache scaffold. fn create_base_files(root: &PathBuf) -> Result<(), io::Error> { // Create the cache directory, if it doesn't exist. fs_err::create_dir_all(root)?; // Add the CACHEDIR.TAG. cachedir::ensure_tag(root)?; // Add the .gitignore. match fs_err::OpenOptions::new() .write(true) .create_new(true) .open(root.join(".gitignore")) { Ok(mut file) => file.write_all(b"*")?, Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (), Err(err) => return Err(err), } // Add an empty .gitignore to the build bucket, to ensure that the cache's own .gitignore // doesn't interfere with source distribution builds. Build backends (like hatchling) will // traverse upwards to look for .gitignore files. fs_err::create_dir_all(root.join(CacheBucket::SourceDistributions.to_str()))?; match fs_err::OpenOptions::new() .write(true) .create_new(true) .open( root.join(CacheBucket::SourceDistributions.to_str()) .join(".gitignore"), ) { Ok(_) => {} Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (), Err(err) => return Err(err), } // Add a phony .git, if it doesn't exist, to ensure that the cache isn't considered to be // part of a Git repository. (Some packages will include Git metadata (like a hash) in the // built version if they're in a Git repository, but the cache should be viewed as an // isolated store.). // We have to put this below the gitignore. Otherwise, if the build backend uses the rust // ignore crate it will walk up to the top level .gitignore and ignore its python source // files. fs_err::OpenOptions::new().create(true).write(true).open( root.join(CacheBucket::SourceDistributions.to_str()) .join(".git"), )?; Ok(()) } /// Initialize the [`Cache`]. pub async fn init(self) -> Result { let root = &self.root; Self::create_base_files(root)?; // Block cache removal operations from interfering. let lock_file = match LockedFile::acquire( root.join(".lock"), LockedFileMode::Shared, root.simplified_display(), ) .await { Ok(lock_file) => Some(Arc::new(lock_file)), Err(err) if err .as_io_error() .is_some_and(|err| err.kind() == io::ErrorKind::Unsupported) => { warn!( "Shared locking is not supported by the current platform or filesystem, \ reduced parallel process safety with `uv cache clean` and `uv cache prune`." ); None } Err(err) => return Err(err), }; Ok(Self { root: std::path::absolute(root)?, lock_file, ..self }) } /// Initialize the [`Cache`], assuming that there are no other uv processes running. pub fn init_no_wait(self) -> Result, io::Error> { let root = &self.root; Self::create_base_files(root)?; // Block cache removal operations from interfering. let Some(lock_file) = LockedFile::acquire_no_wait( root.join(".lock"), LockedFileMode::Shared, root.simplified_display(), ) else { return Ok(None); }; Ok(Some(Self { root: std::path::absolute(root)?, lock_file: Some(Arc::new(lock_file)), ..self })) } /// Clear the cache, removing all entries. pub fn clear(self, reporter: Box) -> Result { // Remove everything but `.lock`, Windows does not allow removal of a locked file let mut removal = Remover::new(reporter).rm_rf(&self.root, true)?; let Self { root, lock_file, .. } = self; // Remove the `.lock` file, unlocking it first if let Some(lock) = lock_file { drop(lock); fs_err::remove_file(root.join(".lock"))?; } removal.num_files += 1; // Remove the root directory match fs_err::remove_dir(root) { Ok(()) => { removal.num_dirs += 1; } // On Windows, when `--force` is used, the `.lock` file can exist and be unremovable, // so we make this non-fatal Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => { trace!("Failed to remove root cache directory: not empty"); } Err(err) => return Err(err), } Ok(removal) } /// Remove a package from the cache. /// /// Returns the number of entries removed from the cache. pub fn remove(&self, name: &PackageName) -> Result { // Collect the set of referenced archives. let references = self.find_archive_references()?; // Remove any entries for the package from the cache. let mut summary = Removal::default(); for bucket in CacheBucket::iter() { summary += bucket.remove(self, name)?; } // Remove any archives that are no longer referenced. for (target, references) in references { if references.iter().all(|path| !path.exists()) { debug!("Removing dangling cache entry: {}", target.display()); summary += rm_rf(target)?; } } Ok(summary) } /// Run the garbage collector on the cache, removing any dangling entries. pub fn prune(&self, ci: bool) -> Result { let mut summary = Removal::default(); // First, remove any top-level directories that are unused. These typically represent // outdated cache buckets (e.g., `wheels-v0`, when latest is `wheels-v1`). for entry in fs_err::read_dir(&self.root)? { let entry = entry?; let metadata = entry.metadata()?; if entry.file_name() == "CACHEDIR.TAG" || entry.file_name() == ".gitignore" || entry.file_name() == ".git" || entry.file_name() == ".lock" { continue; } if metadata.is_dir() { // If the directory is not a cache bucket, remove it. if CacheBucket::iter().all(|bucket| entry.file_name() != bucket.to_str()) { let path = entry.path(); debug!("Removing dangling cache bucket: {}", path.display()); summary += rm_rf(path)?; } } else { // If the file is not a marker file, remove it. let path = entry.path(); debug!("Removing dangling cache bucket: {}", path.display()); summary += rm_rf(path)?; } } // Second, remove any cached environments. These are never referenced by symlinks, so we can // remove them directly. match fs_err::read_dir(self.bucket(CacheBucket::Environments)) { Ok(entries) => { for entry in entries { let entry = entry?; let path = fs_err::canonicalize(entry.path())?; debug!("Removing dangling cache environment: {}", path.display()); summary += rm_rf(path)?; } } Err(err) if err.kind() == io::ErrorKind::NotFound => (), Err(err) => return Err(err), } // Third, if enabled, remove all unzipped wheels, leaving only the wheel archives. if ci { // Remove the entire pre-built wheel cache, since every entry is an unzipped wheel. match fs_err::read_dir(self.bucket(CacheBucket::Wheels)) { Ok(entries) => { for entry in entries { let entry = entry?; let path = fs_err::canonicalize(entry.path())?; if path.is_dir() { debug!("Removing unzipped wheel entry: {}", path.display()); summary += rm_rf(path)?; } } } Err(err) if err.kind() == io::ErrorKind::NotFound => (), Err(err) => return Err(err), } for entry in walkdir::WalkDir::new(self.bucket(CacheBucket::SourceDistributions)) { let entry = entry?; // If the directory contains a `metadata.msgpack`, then it's a built wheel revision. if !entry.file_type().is_dir() { continue; } if !entry.path().join("metadata.msgpack").exists() { continue; } // Remove everything except the built wheel archive and the metadata. for entry in fs_err::read_dir(entry.path())? { let entry = entry?; let path = entry.path(); // Retain the resolved metadata (`metadata.msgpack`). if path .file_name() .is_some_and(|file_name| file_name == "metadata.msgpack") { continue; } // Retain any built wheel archives. if path .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("whl")) { continue; } debug!("Removing unzipped built wheel entry: {}", path.display()); summary += rm_rf(path)?; } } } // Fourth, remove any unused archives (by searching for archives that are not symlinked). let references = self.find_archive_references()?; match fs_err::read_dir(self.bucket(CacheBucket::Archive)) { Ok(entries) => { for entry in entries { let entry = entry?; let path = fs_err::canonicalize(entry.path())?; if !references.contains_key(&path) { debug!("Removing dangling cache archive: {}", path.display()); summary += rm_rf(path)?; } } } Err(err) if err.kind() == io::ErrorKind::NotFound => (), Err(err) => return Err(err), } Ok(summary) } /// Find all references to entries in the archive bucket. /// /// Archive entries are often referenced by symlinks in other cache buckets. This method /// searches for all such references. /// /// Returns a map from archive path to paths that reference it. fn find_archive_references(&self) -> Result>, io::Error> { let mut references = FxHashMap::>::default(); for bucket in [CacheBucket::SourceDistributions, CacheBucket::Wheels] { let bucket_path = self.bucket(bucket); if bucket_path.is_dir() { let walker = walkdir::WalkDir::new(&bucket_path).into_iter(); for entry in walker.filter_entry(|entry| { !( // As an optimization, ignore any `.lock`, `.whl`, `.msgpack`, `.rev`, or // `.http` files, along with the `src` directory, which represents the // unpacked source distribution. entry.file_name() == "src" || entry.file_name() == ".lock" || entry.file_name() == ".gitignore" || entry.path().extension().is_some_and(|ext| { ext.eq_ignore_ascii_case("lock") || ext.eq_ignore_ascii_case("whl") || ext.eq_ignore_ascii_case("http") || ext.eq_ignore_ascii_case("rev") || ext.eq_ignore_ascii_case("msgpack") }) ) }) { let entry = entry?; // On Unix, archive references use symlinks. if cfg!(unix) { if !entry.file_type().is_symlink() { continue; } } // On Windows, archive references are files containing structured data. if cfg!(windows) { if !entry.file_type().is_file() { continue; } } if let Ok(target) = self.resolve_link(entry.path()) { references .entry(target) .or_default() .push(entry.path().to_path_buf()); } } } } Ok(references) } /// Create a link to a directory in the archive bucket. /// /// On Windows, we write structured data ([`Link`]) to a file containing the archive ID and /// version. On Unix, we create a symlink to the target directory. #[cfg(windows)] pub fn create_link(&self, id: &ArchiveId, dst: impl AsRef) -> io::Result<()> { // Serialize the link. let link = Link::new(id.clone()); let contents = link.to_string(); // First, attempt to create a file at the location, but fail if it already exists. match fs_err::OpenOptions::new() .write(true) .create_new(true) .open(dst.as_ref()) { Ok(mut file) => { // Write the target path to the file. file.write_all(contents.as_bytes())?; Ok(()) } Err(err) if err.kind() == io::ErrorKind::AlreadyExists => { // Write to a temporary file, then move it into place. let temp_dir = tempfile::tempdir_in(dst.as_ref().parent().unwrap())?; let temp_file = temp_dir.path().join("link"); fs_err::write(&temp_file, contents.as_bytes())?; // Move the symlink into the target location. fs_err::rename(&temp_file, dst.as_ref())?; Ok(()) } Err(err) => Err(err), } } /// Resolve an archive link, returning the fully-resolved path. /// /// Returns an error if the link target does not exist. #[cfg(windows)] pub fn resolve_link(&self, path: impl AsRef) -> io::Result { // Deserialize the link. let contents = fs_err::read_to_string(path.as_ref())?; let link = Link::from_str(&contents)?; // Ignore stale links. if link.version != ARCHIVE_VERSION { return Err(io::Error::new( io::ErrorKind::NotFound, "The link target does not exist.", )); } // Reconstruct the path. let path = self.archive(&link.id); path.canonicalize() } /// Create a link to a directory in the archive bucket. /// /// On Windows, we write structured data ([`Link`]) to a file containing the archive ID and /// version. On Unix, we create a symlink to the target directory. #[cfg(unix)] pub fn create_link(&self, id: &ArchiveId, dst: impl AsRef) -> io::Result<()> { // Construct the link target. let src = self.archive(id); let dst = dst.as_ref(); // Attempt to create the symlink directly. match fs_err::os::unix::fs::symlink(&src, dst) { Ok(()) => Ok(()), Err(err) if err.kind() == io::ErrorKind::AlreadyExists => { // Create a symlink, using a temporary file to ensure atomicity. let temp_dir = tempfile::tempdir_in(dst.parent().unwrap())?; let temp_file = temp_dir.path().join("link"); fs_err::os::unix::fs::symlink(&src, &temp_file)?; // Move the symlink into the target location. fs_err::rename(&temp_file, dst)?; Ok(()) } Err(err) => Err(err), } } /// Resolve an archive link, returning the fully-resolved path. /// /// Returns an error if the link target does not exist. #[cfg(unix)] pub fn resolve_link(&self, path: impl AsRef) -> io::Result { path.as_ref().canonicalize() } } /// An archive (unzipped wheel) that exists in the local cache. #[derive(Debug, Clone)] #[allow(unused)] struct Link { /// The unique ID of the entry in the archive bucket. id: ArchiveId, /// The version of the archive bucket. version: u8, } #[allow(unused)] impl Link { /// Create a new [`Archive`] with the given ID and hashes. fn new(id: ArchiveId) -> Self { Self { id, version: ARCHIVE_VERSION, } } } impl Display for Link { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "archive-v{}/{}", self.version, self.id) } } impl FromStr for Link { type Err = io::Error; fn from_str(s: &str) -> Result { let mut parts = s.splitn(2, '/'); let version = parts .next() .filter(|s| !s.is_empty()) .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing version"))?; let id = parts .next() .filter(|s| !s.is_empty()) .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing ID"))?; // Parse the archive version from `archive-v{version}/{id}`. let version = version .strip_prefix("archive-v") .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing version prefix"))?; let version = u8::from_str(version).map_err(|err| { io::Error::new( io::ErrorKind::InvalidData, format!("failed to parse version: {err}"), ) })?; // Parse the ID from `archive-v{version}/{id}`. let id = ArchiveId::from_str(id).map_err(|err| { io::Error::new( io::ErrorKind::InvalidData, format!("failed to parse ID: {err}"), ) })?; Ok(Self { id, version }) } } pub trait CleanReporter: Send + Sync { /// Called after one file or directory is removed. fn on_clean(&self); /// Called after all files and directories are removed. fn on_complete(&self); } /// The different kinds of data in the cache are stored in different bucket, which in our case /// are subdirectories of the cache root. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub enum CacheBucket { /// Wheels (excluding built wheels), alongside their metadata and cache policy. /// /// There are three kinds from cache entries: Wheel metadata and policy as `MsgPack` files, the /// wheels themselves, and the unzipped wheel archives. If a wheel file is over an in-memory /// size threshold, we first download the zip file into the cache, then unzip it into a /// directory with the same name (exclusive of the `.whl` extension). /// /// Cache structure: /// * `wheel-metadata-v0/pypi/foo/{foo-1.0.0-py3-none-any.msgpack, foo-1.0.0-py3-none-any.whl}` /// * `wheel-metadata-v0//foo/{foo-1.0.0-py3-none-any.msgpack, foo-1.0.0-py3-none-any.whl}` /// * `wheel-metadata-v0/url//foo/{foo-1.0.0-py3-none-any.msgpack, foo-1.0.0-py3-none-any.whl}` /// /// See `uv_client::RegistryClient::wheel_metadata` for information on how wheel metadata /// is fetched. /// /// # Example /// /// Consider the following `requirements.in`: /// ```text /// # pypi wheel /// pandas /// # url wheel /// flask @ https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl /// ``` /// /// When we run `pip compile`, it will only fetch and cache the metadata (and cache policy), it /// doesn't need the actual wheels yet: /// ```text /// wheel-v0 /// ├── pypi /// │ ... /// │ ├── pandas /// │ │ └── pandas-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.msgpack /// │ ... /// └── url /// └── 4b8be67c801a7ecb /// └── flask /// └── flask-3.0.0-py3-none-any.msgpack /// ``` /// /// We get the following `requirement.txt` from `pip compile`: /// /// ```text /// [...] /// flask @ https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl /// [...] /// pandas==2.1.3 /// [...] /// ``` /// /// If we run `pip sync` on `requirements.txt` on a different machine, it also fetches the /// wheels: /// /// TODO(konstin): This is still wrong, we need to store the cache policy too! /// ```text /// wheel-v0 /// ├── pypi /// │ ... /// │ ├── pandas /// │ │ ├── pandas-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl /// │ │ ├── pandas-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64 /// │ ... /// └── url /// └── 4b8be67c801a7ecb /// └── flask /// └── flask-3.0.0-py3-none-any.whl /// ├── flask /// │ └── ... /// └── flask-3.0.0.dist-info /// └── ... /// ``` /// /// If we run first `pip compile` and then `pip sync` on the same machine, we get both: /// /// ```text /// wheels-v0 /// ├── pypi /// │ ├── ... /// │ ├── pandas /// │ │ ├── pandas-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.msgpack /// │ │ ├── pandas-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl /// │ │ └── pandas-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64 /// │ │ ├── pandas /// │ │ │ ├── ... /// │ │ ├── pandas-2.1.3.dist-info /// │ │ │ ├── ... /// │ │ └── pandas.libs /// │ ├── ... /// └── url /// └── 4b8be67c801a7ecb /// └── flask /// ├── flask-3.0.0-py3-none-any.msgpack /// ├── flask-3.0.0-py3-none-any.msgpack /// └── flask-3.0.0-py3-none-any /// ├── flask /// │ └── ... /// └── flask-3.0.0.dist-info /// └── ... Wheels, /// Source distributions, wheels built from source distributions, their extracted metadata, and the /// cache policy of the source distribution. /// /// The structure is similar of that of the `Wheel` bucket, except we have an additional layer /// for the source distribution filename and the metadata is at the source distribution-level, /// not at the wheel level. /// /// TODO(konstin): The cache policy should be on the source distribution level, the metadata we /// can put next to the wheels as in the `Wheels` bucket. /// /// The unzipped source distribution is stored in a directory matching the source distribution /// archive name. /// /// Source distributions are built into zipped wheel files (as PEP 517 specifies) and unzipped /// lazily before installing. So when resolving, we only build the wheel and store the archive /// file in the cache, when installing, we unpack it under the same name (exclusive of the /// `.whl` extension). You may find a mix of wheel archive zip files and unzipped wheel /// directories in the cache. /// /// Cache structure: /// * `built-wheels-v0/pypi/foo/34a17436ed1e9669/{manifest.msgpack, metadata.msgpack, foo-1.0.0.zip, foo-1.0.0-py3-none-any.whl, ...other wheels}` /// * `built-wheels-v0//foo/foo-1.0.0.zip/{manifest.msgpack, metadata.msgpack, foo-1.0.0-py3-none-any.whl, ...other wheels}` /// * `built-wheels-v0/url//foo/foo-1.0.0.zip/{manifest.msgpack, metadata.msgpack, foo-1.0.0-py3-none-any.whl, ...other wheels}` /// * `built-wheels-v0/git///foo/foo-1.0.0.zip/{metadata.msgpack, foo-1.0.0-py3-none-any.whl, ...other wheels}` /// /// But the url filename does not need to be a valid source dist filename /// (), /// so it could also be the following and we have to take any string as filename: /// * `built-wheels-v0/url//master.zip/metadata.msgpack` /// /// # Example /// /// The following requirements: /// ```text /// # git source dist /// pydantic-extra-types @ git+https://github.com/pydantic/pydantic-extra-types.git /// # pypi source dist /// django_allauth==0.51.0 /// # url source dist /// werkzeug @ https://files.pythonhosted.org/packages/0d/cc/ff1904eb5eb4b455e442834dabf9427331ac0fa02853bf83db817a7dd53d/werkzeug-3.0.1.tar.gz /// ``` /// /// ...may be cached as: /// ```text /// built-wheels-v4/ /// ├── git /// │   └── 2122faf3e081fb7a /// │      └── 7a2d650a4a7b4d04 /// │      ├── metadata.msgpack /// │       └── pydantic_extra_types-2.9.0-py3-none-any.whl /// ├── pypi /// │ └── django-allauth /// │ └── 0.51.0 /// │ ├── 0gH-_fwv8tdJ7JwwjJsUc /// │ │   ├── django-allauth-0.51.0.tar.gz /// │ │ │ └── [UNZIPPED CONTENTS] /// │ │   ├── django_allauth-0.51.0-py3-none-any.whl /// │ │   └── metadata.msgpack /// │ └── revision.http /// └── url /// └── 6781bd6440ae72c2 /// ├── APYY01rbIfpAo_ij9sCY6 /// │   ├── metadata.msgpack /// │   ├── werkzeug-3.0.1-py3-none-any.whl /// │   └── werkzeug-3.0.1.tar.gz /// │ └── [UNZIPPED CONTENTS] /// └── revision.http /// ``` /// /// Structurally, the `manifest.msgpack` is empty, and only contains the caching information /// needed to invalidate the cache. The `metadata.msgpack` contains the metadata of the source /// distribution. SourceDistributions, /// Flat index responses, a format very similar to the simple metadata API. /// /// Cache structure: /// * `flat-index-v0/index/.msgpack` /// /// The response is stored as `Vec`. FlatIndex, /// Git repositories. Git, /// Information about an interpreter at a path. /// /// To avoid caching pyenv shims, bash scripts which may redirect to a new python version /// without the shim itself changing, we only cache when the path equals `sys.executable`, i.e. /// the path we're running is the python executable itself and not a shim. /// /// Cache structure: `interpreter-v0/.msgpack` /// /// # Example /// /// The contents of each of the `MsgPack` files has a timestamp field in unix time, the [PEP 508] /// markers and some information from the `sys`/`sysconfig` modules. /// /// ```json /// { /// "timestamp": 1698047994491, /// "data": { /// "markers": { /// "implementation_name": "cpython", /// "implementation_version": "3.12.0", /// "os_name": "posix", /// "platform_machine": "x86_64", /// "platform_python_implementation": "CPython", /// "platform_release": "6.5.0-13-generic", /// "platform_system": "Linux", /// "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023", /// "python_full_version": "3.12.0", /// "python_version": "3.12", /// "sys_platform": "linux" /// }, /// "base_exec_prefix": "/home/ferris/.pyenv/versions/3.12.0", /// "base_prefix": "/home/ferris/.pyenv/versions/3.12.0", /// "sys_executable": "/home/ferris/projects/uv/.venv/bin/python" /// } /// } /// ``` /// /// [PEP 508]: https://peps.python.org/pep-0508/#environment-markers Interpreter, /// Index responses through the simple metadata API. /// /// Cache structure: /// * `simple-v0/pypi/.rkyv` /// * `simple-v0//.rkyv` /// /// The response is parsed into `uv_client::SimpleMetadata` before storage. Simple, /// A cache of unzipped wheels, stored as directories. This is used internally within the cache. /// When other buckets need to store directories, they should persist them to /// [`CacheBucket::Archive`], and then symlink them into the appropriate bucket. This ensures /// that cache entries can be atomically replaced and removed, as storing directories in the /// other buckets directly would make atomic operations impossible. Archive, /// Ephemeral virtual environments used to execute PEP 517 builds and other operations. Builds, /// Reusable virtual environments used to invoke Python tools. Environments, /// Cached Python downloads Python, /// Downloaded tool binaries (e.g., Ruff). Binaries, } impl CacheBucket { fn to_str(self) -> &'static str { match self { // Note that when bumping this, you'll also need to bump it // in `crates/uv/tests/it/cache_prune.rs`. Self::SourceDistributions => "sdists-v9", Self::FlatIndex => "flat-index-v2", Self::Git => "git-v0", Self::Interpreter => "interpreter-v4", // Note that when bumping this, you'll also need to bump it // in `crates/uv/tests/it/cache_clean.rs`. Self::Simple => "simple-v18", // Note that when bumping this, you'll also need to bump it // in `crates/uv/tests/it/cache_prune.rs`. Self::Wheels => "wheels-v5", // Note that when bumping this, you'll also need to bump // `ARCHIVE_VERSION` in `crates/uv-cache/src/lib.rs`. Self::Archive => "archive-v0", Self::Builds => "builds-v0", Self::Environments => "environments-v2", Self::Python => "python-v0", Self::Binaries => "binaries-v0", } } /// Remove a package from the cache bucket. /// /// Returns the number of entries removed from the cache. fn remove(self, cache: &Cache, name: &PackageName) -> Result { /// Returns `true` if the [`Path`] represents a built wheel for the given package. fn is_match(path: &Path, name: &PackageName) -> bool { let Ok(metadata) = fs_err::read(path.join("metadata.msgpack")) else { return false; }; let Ok(metadata) = rmp_serde::from_slice::(&metadata) else { return false; }; metadata.name == *name } let mut summary = Removal::default(); match self { Self::Wheels => { // For `pypi` wheels, we expect a directory per package (indexed by name). let root = cache.bucket(self).join(WheelCacheKind::Pypi); summary += rm_rf(root.join(name.to_string()))?; // For alternate indices, we expect a directory for every index (under an `index` // subdirectory), followed by a directory per package (indexed by name). let root = cache.bucket(self).join(WheelCacheKind::Index); for directory in directories(root)? { summary += rm_rf(directory.join(name.to_string()))?; } // For direct URLs, we expect a directory for every URL, followed by a // directory per package (indexed by name). let root = cache.bucket(self).join(WheelCacheKind::Url); for directory in directories(root)? { summary += rm_rf(directory.join(name.to_string()))?; } } Self::SourceDistributions => { // For `pypi` wheels, we expect a directory per package (indexed by name). let root = cache.bucket(self).join(WheelCacheKind::Pypi); summary += rm_rf(root.join(name.to_string()))?; // For alternate indices, we expect a directory for every index (under an `index` // subdirectory), followed by a directory per package (indexed by name). let root = cache.bucket(self).join(WheelCacheKind::Index); for directory in directories(root)? { summary += rm_rf(directory.join(name.to_string()))?; } // For direct URLs, we expect a directory for every URL, followed by a // directory per version. To determine whether the URL is relevant, we need to // search for a wheel matching the package name. let root = cache.bucket(self).join(WheelCacheKind::Url); for url in directories(root)? { if directories(&url)?.any(|version| is_match(&version, name)) { summary += rm_rf(url)?; } } // For local dependencies, we expect a directory for every path, followed by a // directory per version. To determine whether the path is relevant, we need to // search for a wheel matching the package name. let root = cache.bucket(self).join(WheelCacheKind::Path); for path in directories(root)? { if directories(&path)?.any(|version| is_match(&version, name)) { summary += rm_rf(path)?; } } // For Git dependencies, we expect a directory for every repository, followed by a // directory for every SHA. To determine whether the SHA is relevant, we need to // search for a wheel matching the package name. let root = cache.bucket(self).join(WheelCacheKind::Git); for repository in directories(root)? { for sha in directories(repository)? { if is_match(&sha, name) { summary += rm_rf(sha)?; } } } } Self::Simple => { // For `pypi` wheels, we expect a rkyv file per package, indexed by name. let root = cache.bucket(self).join(WheelCacheKind::Pypi); summary += rm_rf(root.join(format!("{name}.rkyv")))?; // For alternate indices, we expect a directory for every index (under an `index` // subdirectory), followed by a directory per package (indexed by name). let root = cache.bucket(self).join(WheelCacheKind::Index); for directory in directories(root)? { summary += rm_rf(directory.join(format!("{name}.rkyv")))?; } } Self::FlatIndex => { // We can't know if the flat index includes a package, so we just remove the entire // cache entry. let root = cache.bucket(self); summary += rm_rf(root)?; } Self::Git | Self::Interpreter | Self::Archive | Self::Builds | Self::Environments | Self::Python | Self::Binaries => { // Nothing to do. } } Ok(summary) } /// Return an iterator over all cache buckets. pub fn iter() -> impl Iterator { [ Self::Wheels, Self::SourceDistributions, Self::FlatIndex, Self::Git, Self::Interpreter, Self::Simple, Self::Archive, Self::Builds, Self::Environments, Self::Binaries, ] .iter() .copied() } } impl Display for CacheBucket { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(self.to_str()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Freshness { /// The cache entry is fresh according to the [`Refresh`] policy. Fresh, /// The cache entry is stale according to the [`Refresh`] policy. Stale, /// The cache entry does not exist. Missing, } impl Freshness { pub const fn is_fresh(self) -> bool { matches!(self, Self::Fresh) } pub const fn is_stale(self) -> bool { matches!(self, Self::Stale) } } /// A refresh policy for cache entries. #[derive(Debug, Clone)] pub enum Refresh { /// Don't refresh any entries. None(Timestamp), /// Refresh entries linked to the given packages, if created before the given timestamp. Packages(Vec, Vec>, Timestamp), /// Refresh all entries created before the given timestamp. All(Timestamp), } impl Refresh { /// Determine the refresh strategy to use based on the command-line arguments. pub fn from_args(refresh: Option, refresh_package: Vec) -> Self { let timestamp = Timestamp::now(); match refresh { Some(true) => Self::All(timestamp), Some(false) => Self::None(timestamp), None => { if refresh_package.is_empty() { Self::None(timestamp) } else { Self::Packages(refresh_package, vec![], timestamp) } } } } /// Return the [`Timestamp`] associated with the refresh policy. pub fn timestamp(&self) -> Timestamp { match self { Self::None(timestamp) => *timestamp, Self::Packages(.., timestamp) => *timestamp, Self::All(timestamp) => *timestamp, } } /// Returns `true` if no packages should be reinstalled. pub fn is_none(&self) -> bool { matches!(self, Self::None(_)) } /// Combine two [`Refresh`] policies, taking the "max" of the two policies. #[must_use] pub fn combine(self, other: Self) -> Self { match (self, other) { // If the policy is `None`, return the existing refresh policy. // Take the `max` of the two timestamps. (Self::None(t1), Self::None(t2)) => Self::None(t1.max(t2)), (Self::None(t1), Self::All(t2)) => Self::All(t1.max(t2)), (Self::None(t1), Self::Packages(packages, paths, t2)) => { Self::Packages(packages, paths, t1.max(t2)) } // If the policy is `All`, refresh all packages. (Self::All(t1), Self::None(t2) | Self::All(t2) | Self::Packages(.., t2)) => { Self::All(t1.max(t2)) } // If the policy is `Packages`, take the "max" of the two policies. (Self::Packages(packages, paths, t1), Self::None(t2)) => { Self::Packages(packages, paths, t1.max(t2)) } (Self::Packages(.., t1), Self::All(t2)) => Self::All(t1.max(t2)), (Self::Packages(packages1, paths1, t1), Self::Packages(packages2, paths2, t2)) => { Self::Packages( packages1.into_iter().chain(packages2).collect(), paths1.into_iter().chain(paths2).collect(), t1.max(t2), ) } } } } #[cfg(test)] mod tests { use std::str::FromStr; use crate::ArchiveId; use super::Link; #[test] fn test_link_round_trip() { let id = ArchiveId::new(); let link = Link::new(id); let s = link.to_string(); let parsed = Link::from_str(&s).unwrap(); assert_eq!(link.id, parsed.id); assert_eq!(link.version, parsed.version); } #[test] fn test_link_deserialize() { assert!(Link::from_str("archive-v0/foo").is_ok()); assert!(Link::from_str("archive/foo").is_err()); assert!(Link::from_str("v1/foo").is_err()); assert!(Link::from_str("archive-v0/").is_err()); } } uv-0.9.17+ds1/crates/uv-cache/src/removal.rs000066400000000000000000000206211520155276700205160ustar00rootroot00000000000000//! Derived from Cargo's `clean` implementation. //! Cargo is dual-licensed under either Apache 2.0 or MIT, at the user's choice. //! Source: use std::io; use std::path::Path; use crate::CleanReporter; /// Remove a file or directory and all its contents, returning a [`Removal`] with /// the number of files and directories removed, along with a total byte count. pub fn rm_rf(path: impl AsRef) -> io::Result { Remover::default().rm_rf(path, false) } /// A builder for a [`Remover`] that can remove files and directories. #[derive(Default)] pub(crate) struct Remover { reporter: Option>, } impl Remover { /// Create a new [`Remover`] with the given reporter. pub(crate) fn new(reporter: Box) -> Self { Self { reporter: Some(reporter), } } /// Remove a file or directory and all its contents, returning a [`Removal`] with /// the number of files and directories removed, along with a total byte count. pub(crate) fn rm_rf( &self, path: impl AsRef, skip_locked_file: bool, ) -> io::Result { let mut removal = Removal::default(); removal.rm_rf(path.as_ref(), self.reporter.as_deref(), skip_locked_file)?; Ok(removal) } } /// A removal operation with statistics on the number of files and directories removed. #[derive(Debug, Default)] pub struct Removal { /// The number of files removed. pub num_files: u64, /// The number of directories removed. pub num_dirs: u64, /// The total number of bytes removed. /// /// Note: this will both over-count bytes removed for hard-linked files, and under-count /// bytes in general since it's a measure of the exact byte size (as opposed to the block size). pub total_bytes: u64, } impl Removal { /// Recursively remove a file or directory and all its contents. fn rm_rf( &mut self, path: &Path, reporter: Option<&dyn CleanReporter>, skip_locked_file: bool, ) -> io::Result<()> { let metadata = match fs_err::symlink_metadata(path) { Ok(metadata) => metadata, Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()), Err(err) => return Err(err), }; if !metadata.is_dir() { self.num_files += 1; // Remove the file. self.total_bytes += metadata.len(); if metadata.is_symlink() { #[cfg(windows)] { use std::os::windows::fs::FileTypeExt; if metadata.file_type().is_symlink_dir() { remove_dir(path)?; } else { remove_file(path)?; } } #[cfg(not(windows))] { remove_file(path)?; } } else { remove_file(path)?; } reporter.map(CleanReporter::on_clean); return Ok(()); } for entry in walkdir::WalkDir::new(path).contents_first(true) { // If we hit a directory that lacks read permissions, try to make it readable. if let Err(ref err) = entry { if err .io_error() .is_some_and(|err| err.kind() == io::ErrorKind::PermissionDenied) { if let Some(dir) = err.path() { if set_readable(dir).unwrap_or(false) { // Retry the operation; if we _just_ `self.rm_rf(dir)` and continue, // `walkdir` may give us duplicate entries for the directory. return self.rm_rf(path, reporter, skip_locked_file); } } } } let entry = entry?; // Remove the exclusive lock last. if skip_locked_file && entry.file_name() == ".lock" && entry .path() .strip_prefix(path) .is_ok_and(|suffix| suffix == Path::new(".lock")) { continue; } if entry.file_type().is_symlink() && { #[cfg(windows)] { use std::os::windows::fs::FileTypeExt; entry.file_type().is_symlink_dir() } #[cfg(not(windows))] { false } } { self.num_files += 1; remove_dir(entry.path())?; } else if entry.file_type().is_dir() { // Remove the directory with the exclusive lock last. if skip_locked_file && entry.path() == path { continue; } self.num_dirs += 1; // The contents should have been removed by now, but sometimes a race condition is // hit where other files have been added by the OS. Fall back to `remove_dir_all`, // which will remove the directory robustly across platforms. remove_dir_all(entry.path())?; } else { self.num_files += 1; // Remove the file. if let Ok(meta) = entry.metadata() { self.total_bytes += meta.len(); } remove_file(entry.path())?; } reporter.map(CleanReporter::on_clean); } reporter.map(CleanReporter::on_complete); Ok(()) } } impl std::ops::AddAssign for Removal { fn add_assign(&mut self, other: Self) { self.num_files += other.num_files; self.num_dirs += other.num_dirs; self.total_bytes += other.total_bytes; } } /// If the directory isn't readable by the current user, change the permissions to make it readable. #[cfg_attr(windows, allow(unused_variables, clippy::unnecessary_wraps))] fn set_readable(path: &Path) -> io::Result { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perms = fs_err::metadata(path)?.permissions(); if perms.mode() & 0o500 == 0 { perms.set_mode(perms.mode() | 0o500); fs_err::set_permissions(path, perms)?; return Ok(true); } } Ok(false) } /// If the file is readonly, change the permissions to make it _not_ readonly. fn set_not_readonly(path: &Path) -> io::Result { let mut perms = fs_err::metadata(path)?.permissions(); if !perms.readonly() { return Ok(false); } // We're about to delete the file, so it's fine to set the permissions to world-writable. #[allow(clippy::permissions_set_readonly_false)] perms.set_readonly(false); fs_err::set_permissions(path, perms)?; Ok(true) } /// Like [`fs_err::remove_file`], but attempts to change the permissions to force the file to be /// deleted (if it is readonly). fn remove_file(path: &Path) -> io::Result<()> { match fs_err::remove_file(path) { Ok(()) => Ok(()), Err(err) if err.kind() == io::ErrorKind::PermissionDenied && set_not_readonly(path).unwrap_or(false) => { fs_err::remove_file(path) } Err(err) => Err(err), } } /// Like [`fs_err::remove_dir`], but attempts to change the permissions to force the directory to /// be deleted (if it is readonly). fn remove_dir(path: &Path) -> io::Result<()> { match fs_err::remove_dir(path) { Ok(()) => Ok(()), Err(err) if err.kind() == io::ErrorKind::PermissionDenied && set_readable(path).unwrap_or(false) => { fs_err::remove_dir(path) } Err(err) => Err(err), } } /// Like [`fs_err::remove_dir_all`], but attempts to change the permissions to force the directory /// to be deleted (if it is readonly). fn remove_dir_all(path: &Path) -> io::Result<()> { match fs_err::remove_dir_all(path) { Ok(()) => Ok(()), Err(err) if err.kind() == io::ErrorKind::PermissionDenied && set_readable(path).unwrap_or(false) => { fs_err::remove_dir_all(path) } Err(err) => Err(err), } } uv-0.9.17+ds1/crates/uv-cache/src/wheel.rs000066400000000000000000000056071520155276700201640ustar00rootroot00000000000000use std::path::{Path, PathBuf}; use uv_cache_key::{CanonicalUrl, cache_digest}; use uv_distribution_types::IndexUrl; use uv_redacted::DisplaySafeUrl; /// Cache wheels and their metadata, both from remote wheels and built from source distributions. #[derive(Debug, Clone)] pub enum WheelCache<'a> { /// Either PyPI or an alternative index, which we key by index URL. Index(&'a IndexUrl), /// A direct URL dependency, which we key by URL. Url(&'a DisplaySafeUrl), /// A path dependency, which we key by URL. Path(&'a DisplaySafeUrl), /// An editable dependency, which we key by URL. Editable(&'a DisplaySafeUrl), /// A Git dependency, which we key by URL (including LFS state), SHA. /// /// Note that this variant only exists for source distributions; wheels can't be delivered /// through Git. Git(&'a DisplaySafeUrl, &'a str), } impl WheelCache<'_> { /// The root directory for a cache bucket. pub fn root(&self) -> PathBuf { match self { Self::Index(IndexUrl::Pypi(_)) => WheelCacheKind::Pypi.root(), Self::Index(url) => WheelCacheKind::Index .root() .join(cache_digest(&CanonicalUrl::new(url.url()))), Self::Url(url) => WheelCacheKind::Url .root() .join(cache_digest(&CanonicalUrl::new(url))), Self::Path(url) => WheelCacheKind::Path .root() .join(cache_digest(&CanonicalUrl::new(url))), Self::Editable(url) => WheelCacheKind::Editable .root() .join(cache_digest(&CanonicalUrl::new(url))), Self::Git(url, sha) => WheelCacheKind::Git .root() .join(cache_digest(&CanonicalUrl::new(url))) .join(sha), } } /// A subdirectory in a bucket for wheels for a specific package. pub fn wheel_dir(&self, package_name: impl AsRef) -> PathBuf { self.root().join(package_name) } } #[derive(Debug, Clone, Copy)] pub(crate) enum WheelCacheKind { /// A cache of data from PyPI. Pypi, /// A cache of data from an alternative index. Index, /// A cache of data from an arbitrary URL. Url, /// A cache of data from a local path. Path, /// A cache of data from an editable URL. Editable, /// A cache of data from a Git repository. Git, } impl WheelCacheKind { pub(crate) fn to_str(self) -> &'static str { match self { Self::Pypi => "pypi", Self::Index => "index", Self::Url => "url", Self::Path => "path", Self::Editable => "editable", Self::Git => "git", } } pub(crate) fn root(self) -> PathBuf { Path::new(self.to_str()).to_path_buf() } } impl AsRef for WheelCacheKind { fn as_ref(&self) -> &Path { self.to_str().as_ref() } } uv-0.9.17+ds1/crates/uv-cli/000077500000000000000000000000001520155276700154175ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-cli/Cargo.toml000066400000000000000000000030631520155276700173510ustar00rootroot00000000000000[package] name = "uv-cli" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-auth = { workspace = true } uv-cache = { workspace = true, features = ["clap"] } uv-configuration = { workspace = true, features = ["clap"] } uv-distribution-types = { workspace = true } uv-install-wheel = { workspace = true, features = ["clap"], default-features = false } uv-normalize = { workspace = true } uv-pep508 = { workspace = true } uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true, features = ["clap", "schemars"]} uv-redacted = { workspace = true } uv-resolver = { workspace = true, features = ["clap"] } uv-settings = { workspace = true, features = ["schemars"] } uv-static = { workspace = true } uv-torch = { workspace = true, features = ["clap"] } uv-version = { workspace = true } uv-warnings = { workspace = true } uv-workspace = { workspace = true } anstream = { workspace = true } anyhow = { workspace = true } clap = { workspace = true, features = ["derive", "string"] } clap_complete_command = { workspace = true } serde = { workspace = true } url = { workspace = true } [dev-dependencies] insta = { workspace = true } [features] default = [] self-update = [] [build-dependencies] uv-static = { workspace = true } fs-err = { workspace = true } uv-0.9.17+ds1/crates/uv-cli/README.md000066400000000000000000000010171520155276700166750ustar00rootroot00000000000000 # uv-cli This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-cli). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-cli/build.rs000066400000000000000000000101151520155276700170620ustar00rootroot00000000000000use std::{ path::{Path, PathBuf}, process::Command, }; use fs_err as fs; use uv_static::EnvVars; fn main() { // The workspace root directory is not available without walking up the tree // https://github.com/rust-lang/cargo/issues/3946 let workspace_root = Path::new(&std::env::var(EnvVars::CARGO_MANIFEST_DIR).unwrap()) .parent() .expect("CARGO_MANIFEST_DIR should be nested in workspace") .parent() .expect("CARGO_MANIFEST_DIR should be doubly nested in workspace") .to_path_buf(); commit_info(&workspace_root); #[allow(clippy::disallowed_methods)] let target = std::env::var(EnvVars::TARGET).unwrap(); println!("cargo:rustc-env=RUST_HOST_TARGET={target}"); } fn commit_info(workspace_root: &Path) { // If not in a git repository, do not attempt to retrieve commit information let git_dir = workspace_root.join(".git"); if !git_dir.exists() { return; } if let Some(git_head_path) = git_head(&git_dir) { println!("cargo:rerun-if-changed={}", git_head_path.display()); let git_head_contents = fs::read_to_string(git_head_path); if let Ok(git_head_contents) = git_head_contents { // The contents are either a commit or a reference in the following formats // - "" when the head is detached // - "ref " when working on a branch // If a commit, checking if the HEAD file has changed is sufficient // If a ref, we need to add the head file for that ref to rebuild on commit let mut git_ref_parts = git_head_contents.split_whitespace(); git_ref_parts.next(); if let Some(git_ref) = git_ref_parts.next() { let git_ref_path = git_dir.join(git_ref); println!("cargo:rerun-if-changed={}", git_ref_path.display()); } } } let output = match Command::new("git") .arg("log") .arg("-1") .arg("--date=short") .arg("--abbrev=9") .arg("--format=%H %h %cd %(describe:tags)") .output() { Ok(output) if output.status.success() => output, _ => return, }; let stdout = String::from_utf8(output.stdout).unwrap(); let mut parts = stdout.split_whitespace(); let mut next = || parts.next().unwrap(); println!("cargo:rustc-env={}={}", EnvVars::UV_COMMIT_HASH, next()); println!( "cargo:rustc-env={}={}", EnvVars::UV_COMMIT_SHORT_HASH, next() ); println!("cargo:rustc-env={}={}", EnvVars::UV_COMMIT_DATE, next()); // Describe can fail for some commits // https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emdescribeoptionsem if let Some(describe) = parts.next() { let mut describe_parts = describe.split('-'); println!( "cargo:rustc-env={}={}", EnvVars::UV_LAST_TAG, describe_parts.next().unwrap() ); // If this is the tagged commit, this component will be missing println!( "cargo:rustc-env={}={}", EnvVars::UV_LAST_TAG_DISTANCE, describe_parts.next().unwrap_or("0") ); } } fn git_head(git_dir: &Path) -> Option { // The typical case is a standard git repository. let git_head_path = git_dir.join("HEAD"); if git_head_path.exists() { return Some(git_head_path); } if !git_dir.is_file() { return None; } // If `.git/HEAD` doesn't exist and `.git` is actually a file, // then let's try to attempt to read it as a worktree. If it's // a worktree, then its contents will look like this, e.g.: // // gitdir: /home/andrew/astral/uv/main/.git/worktrees/pr2 // // And the HEAD file we want to watch will be at: // // /home/andrew/astral/uv/main/.git/worktrees/pr2/HEAD let contents = fs::read_to_string(git_dir).ok()?; let (label, worktree_path) = contents.split_once(':')?; if label != "gitdir" { return None; } let worktree_path = worktree_path.trim(); Some(PathBuf::from(worktree_path)) } uv-0.9.17+ds1/crates/uv-cli/src/000077500000000000000000000000001520155276700162065ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-cli/src/comma.rs000066400000000000000000000074731520155276700176630ustar00rootroot00000000000000use std::str::FromStr; /// A comma-separated string of requirements, e.g., `"flask,anyio"`, that takes extras into account /// (i.e., treats `"psycopg[binary,pool]"` as a single requirement). #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct CommaSeparatedRequirements(Vec); impl IntoIterator for CommaSeparatedRequirements { type Item = String; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl FromStr for CommaSeparatedRequirements { type Err = String; fn from_str(input: &str) -> Result { // Split on commas _outside_ of brackets. let mut requirements = Vec::new(); let mut depth = 0usize; let mut start = 0usize; for (i, c) in input.char_indices() { match c { '[' => { depth = depth.saturating_add(1); } ']' => { depth = depth.saturating_sub(1); } ',' if depth == 0 => { // If the next character is a version identifier, skip the comma, as in: // `requests>=2.1,<3`. if let Some(c) = input .get(i + ','.len_utf8()..) .and_then(|s| s.chars().find(|c| !c.is_whitespace())) { if matches!(c, '!' | '=' | '<' | '>' | '~') { continue; } } let requirement = input[start..i].trim().to_string(); if !requirement.is_empty() { requirements.push(requirement); } start = i + ','.len_utf8(); } _ => {} } } let requirement = input[start..].trim().to_string(); if !requirement.is_empty() { requirements.push(requirement); } Ok(Self(requirements)) } } #[cfg(test)] mod tests { use super::CommaSeparatedRequirements; use std::str::FromStr; #[test] fn single() { assert_eq!( CommaSeparatedRequirements::from_str("flask").unwrap(), CommaSeparatedRequirements(vec!["flask".to_string()]) ); } #[test] fn double() { assert_eq!( CommaSeparatedRequirements::from_str("flask,anyio").unwrap(), CommaSeparatedRequirements(vec!["flask".to_string(), "anyio".to_string()]) ); } #[test] fn empty() { assert_eq!( CommaSeparatedRequirements::from_str("flask,,anyio").unwrap(), CommaSeparatedRequirements(vec!["flask".to_string(), "anyio".to_string()]) ); } #[test] fn single_extras() { assert_eq!( CommaSeparatedRequirements::from_str("psycopg[binary,pool]").unwrap(), CommaSeparatedRequirements(vec!["psycopg[binary,pool]".to_string()]) ); } #[test] fn double_extras() { assert_eq!( CommaSeparatedRequirements::from_str("psycopg[binary,pool], flask").unwrap(), CommaSeparatedRequirements(vec![ "psycopg[binary,pool]".to_string(), "flask".to_string() ]) ); } #[test] fn single_specifiers() { assert_eq!( CommaSeparatedRequirements::from_str("requests>=2.1,<3").unwrap(), CommaSeparatedRequirements(vec!["requests>=2.1,<3".to_string()]) ); } #[test] fn double_specifiers() { assert_eq!( CommaSeparatedRequirements::from_str("requests>=2.1,<3, flask").unwrap(), CommaSeparatedRequirements(vec!["requests>=2.1,<3".to_string(), "flask".to_string()]) ); } } uv-0.9.17+ds1/crates/uv-cli/src/compat.rs000066400000000000000000000252121520155276700200410ustar00rootroot00000000000000use anyhow::{Result, anyhow}; use clap::{Args, ValueEnum}; use uv_warnings::warn_user; pub trait CompatArgs { fn validate(&self) -> Result<()>; } /// Arguments for `pip-compile` compatibility. /// /// These represent a subset of the `pip-compile` interface that uv supports by default. /// For example, users often pass `--allow-unsafe`, which is unnecessary with uv. But it's a /// nice user experience to warn, rather than fail, when users pass `--allow-unsafe`. #[derive(Args)] pub struct PipCompileCompatArgs { #[clap(long, hide = true)] allow_unsafe: bool, #[clap(long, hide = true)] no_allow_unsafe: bool, #[clap(long, hide = true)] reuse_hashes: bool, #[clap(long, hide = true)] no_reuse_hashes: bool, #[clap(long, hide = true)] resolver: Option, #[clap(long, hide = true)] max_rounds: Option, #[clap(long, hide = true)] cert: Option, #[clap(long, hide = true)] client_cert: Option, #[clap(long, hide = true)] emit_trusted_host: bool, #[clap(long, hide = true)] no_emit_trusted_host: bool, #[clap(long, hide = true)] config: Option, #[clap(long, hide = true)] no_config: bool, #[clap(long, hide = true)] emit_options: bool, #[clap(long, hide = true)] no_emit_options: bool, #[clap(long, hide = true)] pip_args: Option, } impl CompatArgs for PipCompileCompatArgs { /// Validate the arguments passed for `pip-compile` compatibility. /// /// This method will warn when an argument is passed that has no effect but matches uv's /// behavior. If an argument is passed that does _not_ match uv's behavior (e.g., /// `--no-build-isolation`), this method will return an error. fn validate(&self) -> Result<()> { if self.allow_unsafe { warn_user!( "pip-compile's `--allow-unsafe` has no effect (uv can safely pin `pip` and other packages)" ); } if self.no_allow_unsafe { warn_user!( "pip-compile's `--no-allow-unsafe` has no effect (uv can safely pin `pip` and other packages)" ); } if self.reuse_hashes { return Err(anyhow!( "pip-compile's `--reuse-hashes` is unsupported (uv doesn't reuse hashes)" )); } if self.no_reuse_hashes { warn_user!("pip-compile's `--no-reuse-hashes` has no effect (uv doesn't reuse hashes)"); } if let Some(resolver) = self.resolver { match resolver { Resolver::Backtracking => { warn_user!( "pip-compile's `--resolver=backtracking` has no effect (uv always backtracks)" ); } Resolver::Legacy => { return Err(anyhow!( "pip-compile's `--resolver=legacy` is unsupported (uv always backtracks)" )); } } } if self.max_rounds.is_some() { return Err(anyhow!( "pip-compile's `--max-rounds` is unsupported (uv always resolves until convergence)" )); } if self.client_cert.is_some() { return Err(anyhow!( "pip-compile's `--client-cert` is unsupported (uv doesn't support dedicated client certificates)" )); } if self.emit_trusted_host { return Err(anyhow!( "pip-compile's `--emit-trusted-host` is unsupported" )); } if self.no_emit_trusted_host { warn_user!( "pip-compile's `--no-emit-trusted-host` has no effect (uv never emits trusted hosts)" ); } if self.config.is_some() { return Err(anyhow!( "pip-compile's `--config` is unsupported (uv does not use a configuration file)" )); } if self.emit_options { return Err(anyhow!( "pip-compile's `--emit-options` is unsupported (uv never emits options)" )); } if self.no_emit_options { warn_user!("pip-compile's `--no-emit-options` has no effect (uv never emits options)"); } if self.pip_args.is_some() { return Err(anyhow!( "pip-compile's `--pip-args` is unsupported (try passing arguments to uv directly)" )); } Ok(()) } } /// Arguments for `pip list` compatibility. /// /// These represent a subset of the `pip list` interface that uv supports by default. #[derive(Args)] pub struct PipListCompatArgs { #[clap(long, hide = true)] disable_pip_version_check: bool, } impl CompatArgs for PipListCompatArgs { /// Validate the arguments passed for `pip list` compatibility. /// /// This method will warn when an argument is passed that has no effect but matches uv's /// behavior. If an argument is passed that does _not_ match uv's behavior (e.g., /// `--disable-pip-version-check`), this method will return an error. fn validate(&self) -> Result<()> { if self.disable_pip_version_check { warn_user!("pip's `--disable-pip-version-check` has no effect"); } Ok(()) } } /// Arguments for `pip-sync` compatibility. /// /// These represent a subset of the `pip-sync` interface that uv supports by default. #[derive(Args)] pub struct PipSyncCompatArgs { #[clap(short, long, hide = true)] ask: bool, #[clap(long, hide = true)] python_executable: Option, #[clap(long, hide = true)] user: bool, #[clap(long, hide = true)] cert: Option, #[clap(long, hide = true)] client_cert: Option, #[clap(long, hide = true)] config: Option, #[clap(long, hide = true)] no_config: bool, #[clap(long, hide = true)] pip_args: Option, } impl CompatArgs for PipSyncCompatArgs { /// Validate the arguments passed for `pip-sync` compatibility. /// /// This method will warn when an argument is passed that has no effect but matches uv's /// behavior. If an argument is passed that does _not_ match uv's behavior, this method will /// return an error. fn validate(&self) -> Result<()> { if self.ask { return Err(anyhow!( "pip-sync's `--ask` is unsupported (uv never asks for confirmation)" )); } if self.python_executable.is_some() { return Err(anyhow!( "pip-sync's `--python-executable` is unsupported (to install into a separate Python environment, try setting `VIRTUAL_ENV` instead)" )); } if self.user { return Err(anyhow!( "pip-sync's `--user` is unsupported (use a virtual environment instead)" )); } if self.client_cert.is_some() { return Err(anyhow!( "pip-sync's `--client-cert` is unsupported (uv doesn't support dedicated client certificates)" )); } if self.config.is_some() { return Err(anyhow!( "pip-sync's `--config` is unsupported (uv does not use a configuration file)" )); } if self.pip_args.is_some() { return Err(anyhow!( "pip-sync's `--pip-args` is unsupported (try passing arguments to uv directly)" )); } Ok(()) } } #[derive(Debug, Copy, Clone, ValueEnum)] enum Resolver { Backtracking, Legacy, } /// Arguments for `venv` compatibility. /// /// These represent a subset of the `virtualenv` interface that uv supports by default. #[derive(Args)] pub struct VenvCompatArgs { #[clap(long, hide = true)] no_seed: bool, #[clap(long, hide = true)] no_pip: bool, #[clap(long, hide = true)] no_setuptools: bool, #[clap(long, hide = true)] no_wheel: bool, } impl CompatArgs for VenvCompatArgs { /// Validate the arguments passed for `venv` compatibility. /// /// This method will warn when an argument is passed that has no effect but matches uv's /// behavior. If an argument is passed that does _not_ match uv's behavior, this method will /// return an error. fn validate(&self) -> Result<()> { if self.no_seed { warn_user!( "virtualenv's `--no-seed` has no effect (uv omits seed packages by default)" ); } if self.no_pip { warn_user!("virtualenv's `--no-pip` has no effect (uv omits `pip` by default)"); } if self.no_setuptools { warn_user!( "virtualenv's `--no-setuptools` has no effect (uv omits `setuptools` by default)" ); } if self.no_wheel { warn_user!("virtualenv's `--no-wheel` has no effect (uv omits `wheel` by default)"); } Ok(()) } } /// Arguments for `pip install` compatibility. /// /// These represent a subset of the `pip install` interface that uv supports by default. #[derive(Args)] pub struct PipInstallCompatArgs { #[clap(long, hide = true)] disable_pip_version_check: bool, #[clap(long, hide = false)] user: bool, } impl CompatArgs for PipInstallCompatArgs { /// Validate the arguments passed for `pip install` compatibility. /// /// This method will warn when an argument is passed that has no effect but matches uv's /// behavior. If an argument is passed that does _not_ match uv's behavior, this method will /// return an error. fn validate(&self) -> Result<()> { if self.disable_pip_version_check { warn_user!("pip's `--disable-pip-version-check` has no effect"); } if self.user { return Err(anyhow!( "pip's `--user` is unsupported (use a virtual environment instead)" )); } Ok(()) } } /// Arguments for generic `pip` command compatibility. /// /// These represent a subset of the `pip` interface that exists on all commands. #[derive(Args)] pub struct PipGlobalCompatArgs { #[clap(long, hide = true)] disable_pip_version_check: bool, } impl CompatArgs for PipGlobalCompatArgs { /// Validate the arguments passed for `pip` compatibility. /// /// This method will warn when an argument is passed that has no effect but matches uv's /// behavior. If an argument is passed that does _not_ match uv's behavior, this method will /// return an error. fn validate(&self) -> Result<()> { if self.disable_pip_version_check { warn_user!("pip's `--disable-pip-version-check` has no effect"); } Ok(()) } } uv-0.9.17+ds1/crates/uv-cli/src/lib.rs000066400000000000000000010711631520155276700173330ustar00rootroot00000000000000use std::ffi::OsString; use std::fmt::{self, Display, Formatter}; use std::ops::{Deref, DerefMut}; use std::path::PathBuf; use std::str::FromStr; use anyhow::{Result, anyhow}; use clap::ValueEnum; use clap::builder::styling::{AnsiColor, Effects, Style}; use clap::builder::{PossibleValue, Styles, TypedValueParser, ValueParserFactory}; use clap::error::ErrorKind; use clap::{Args, Parser, Subcommand}; use uv_auth::Service; use uv_cache::CacheArgs; use uv_configuration::{ ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, PipCompileFormat, ProjectBuildBackend, TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem, }; use uv_distribution_types::{ ConfigSettingEntry, ConfigSettingPackageEntry, Index, IndexUrl, Origin, PipExtraIndex, PipFindLinks, PipIndex, }; use uv_normalize::{ExtraName, GroupName, PackageName, PipGroupName}; use uv_pep508::{MarkerTree, Requirement}; use uv_preview::PreviewFeatures; use uv_pypi_types::VerbatimParsedUrl; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; use uv_redacted::DisplaySafeUrl; use uv_resolver::{ AnnotationStyle, ExcludeNewerPackageEntry, ExcludeNewerValue, ForkStrategy, PrereleaseMode, ResolutionMode, }; use uv_settings::PythonInstallMirrors; use uv_static::EnvVars; use uv_torch::TorchMode; use uv_workspace::pyproject_mut::AddBoundsKind; pub mod comma; pub mod compat; pub mod options; pub mod version; #[derive(Debug, Clone, Copy, clap::ValueEnum)] pub enum VersionFormat { /// Display the version as plain text. Text, /// Display the version as JSON. Json, } #[derive(Debug, Default, Clone, Copy, clap::ValueEnum)] pub enum PythonListFormat { /// Plain text (for humans). #[default] Text, /// JSON (for computers). Json, } #[derive(Debug, Default, Clone, Copy, clap::ValueEnum)] pub enum SyncFormat { /// Display the result in a human-readable format. #[default] Text, /// Display the result in JSON format. Json, } #[derive(Debug, Default, Clone, clap::ValueEnum)] pub enum ListFormat { /// Display the list of packages in a human-readable table. #[default] Columns, /// Display the list of packages in a `pip freeze`-like format, with one package per line /// alongside its version. Freeze, /// Display the list of packages in a machine-readable JSON format. Json, } fn extra_name_with_clap_error(arg: &str) -> Result { ExtraName::from_str(arg).map_err(|_err| { anyhow!( "Extra names must start and end with a letter or digit and may only \ contain -, _, ., and alphanumeric characters" ) }) } // Configures Clap v3-style help menu colors const STYLES: Styles = Styles::styled() .header(AnsiColor::Green.on_default().effects(Effects::BOLD)) .usage(AnsiColor::Green.on_default().effects(Effects::BOLD)) .literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD)) .placeholder(AnsiColor::Cyan.on_default()); #[derive(Parser)] #[command(name = "uv", author, long_version = crate::version::uv_self_version())] #[command(about = "An extremely fast Python package manager.")] #[command( after_help = "Use `uv help` for more details.", after_long_help = "", disable_help_flag = true, disable_help_subcommand = true, disable_version_flag = true )] #[command(styles=STYLES)] pub struct Cli { #[command(subcommand)] pub command: Box, #[command(flatten)] pub top_level: TopLevelArgs, } #[derive(Parser)] #[command(disable_help_flag = true, disable_version_flag = true)] pub struct TopLevelArgs { #[command(flatten)] pub cache_args: Box, #[command(flatten)] pub global_args: Box, /// The path to a `uv.toml` file to use for configuration. /// /// While uv configuration can be included in a `pyproject.toml` file, it is /// not allowed in this context. #[arg( global = true, long, env = EnvVars::UV_CONFIG_FILE, help_heading = "Global options" )] pub config_file: Option, /// Avoid discovering configuration files (`pyproject.toml`, `uv.toml`). /// /// Normally, configuration files are discovered in the current directory, /// parent directories, or user configuration directories. #[arg(global = true, long, env = EnvVars::UV_NO_CONFIG, value_parser = clap::builder::BoolishValueParser::new(), help_heading = "Global options")] pub no_config: bool, /// Display the concise help for this command. #[arg(global = true, short, long, action = clap::ArgAction::HelpShort, help_heading = "Global options")] help: Option, /// Display the uv version. #[arg(short = 'V', long, action = clap::ArgAction::Version)] version: Option, } #[derive(Parser, Debug, Clone)] #[command(next_help_heading = "Global options", next_display_order = 1000)] pub struct GlobalArgs { #[arg( global = true, long, help_heading = "Python options", display_order = 700, env = EnvVars::UV_PYTHON_PREFERENCE, hide = true )] pub python_preference: Option, /// Require use of uv-managed Python versions. /// /// By default, uv prefers using Python versions it manages. However, it /// will use system Python versions if a uv-managed Python is not /// installed. This option disables use of system Python versions. #[arg( global = true, long, help_heading = "Python options", env = EnvVars::UV_MANAGED_PYTHON, value_parser = clap::builder::BoolishValueParser::new(), overrides_with = "no_managed_python", conflicts_with = "python_preference" )] pub managed_python: bool, /// Disable use of uv-managed Python versions. /// /// Instead, uv will search for a suitable Python version on the system. #[arg( global = true, long, help_heading = "Python options", env = EnvVars::UV_NO_MANAGED_PYTHON, value_parser = clap::builder::BoolishValueParser::new(), overrides_with = "managed_python", conflicts_with = "python_preference" )] pub no_managed_python: bool, #[allow(clippy::doc_markdown)] /// Allow automatically downloading Python when required. [env: "UV_PYTHON_DOWNLOADS=auto"] #[arg(global = true, long, help_heading = "Python options", hide = true)] pub allow_python_downloads: bool, #[allow(clippy::doc_markdown)] /// Disable automatic downloads of Python. [env: "UV_PYTHON_DOWNLOADS=never"] #[arg(global = true, long, help_heading = "Python options")] pub no_python_downloads: bool, /// Deprecated version of [`Self::python_downloads`]. #[arg(global = true, long, hide = true)] pub python_fetch: Option, /// Use quiet output. /// /// Repeating this option, e.g., `-qq`, will enable a silent mode in which /// uv will write no output to stdout. #[arg(global = true, action = clap::ArgAction::Count, long, short, conflicts_with = "verbose")] pub quiet: u8, /// Use verbose output. /// /// You can configure fine-grained logging using the `RUST_LOG` environment variable. /// () #[arg(global = true, action = clap::ArgAction::Count, long, short, conflicts_with = "quiet")] pub verbose: u8, /// Disable colors. /// /// Provided for compatibility with `pip`, use `--color` instead. #[arg(global = true, long, hide = true, conflicts_with = "color")] pub no_color: bool, /// Control the use of color in output. /// /// By default, uv will automatically detect support for colors when writing to a terminal. #[arg( global = true, long, value_enum, conflicts_with = "no_color", value_name = "COLOR_CHOICE" )] pub color: Option, /// Whether to load TLS certificates from the platform's native certificate store. /// /// By default, uv loads certificates from the bundled `webpki-roots` crate. The /// `webpki-roots` are a reliable set of trust roots from Mozilla, and including them in uv /// improves portability and performance (especially on macOS). /// /// However, in some cases, you may want to use the platform's native certificate store, /// especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's /// included in your system's certificate store. #[arg(global = true, long, env = EnvVars::UV_NATIVE_TLS, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_native_tls"))] pub native_tls: bool, #[arg(global = true, long, overrides_with("native_tls"), hide = true)] pub no_native_tls: bool, /// Disable network access. /// /// When disabled, uv will only use locally cached data and locally available files. #[arg(global = true, long, overrides_with("no_offline"), env = EnvVars::UV_OFFLINE, value_parser = clap::builder::BoolishValueParser::new())] pub offline: bool, #[arg(global = true, long, overrides_with("offline"), hide = true)] pub no_offline: bool, /// Allow insecure connections to a host. /// /// Can be provided multiple times. /// /// Expects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g., /// `localhost:8080`), or a URL (e.g., `https://localhost`). /// /// WARNING: Hosts included in this list will not be verified against the system's certificate /// store. Only use `--allow-insecure-host` in a secure network with verified sources, as it /// bypasses SSL verification and could expose you to MITM attacks. #[arg( global = true, long, alias = "trusted-host", env = EnvVars::UV_INSECURE_HOST, value_delimiter = ' ', value_parser = parse_insecure_host, )] pub allow_insecure_host: Option>>, /// Whether to enable all experimental preview features. /// /// Preview features may change without warning. #[arg(global = true, long, hide = true, env = EnvVars::UV_PREVIEW, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_preview"))] pub preview: bool, #[arg(global = true, long, overrides_with("preview"), hide = true)] pub no_preview: bool, /// Enable experimental preview features. /// /// Preview features may change without warning. /// /// Use comma-separated values or pass multiple times to enable multiple features. /// /// The following features are available: `python-install-default`, `python-upgrade`, /// `json-output`, `pylock`, `add-bounds`. #[arg( global = true, long = "preview-features", env = EnvVars::UV_PREVIEW_FEATURES, value_delimiter = ',', hide = true, alias = "preview-feature", value_enum, )] pub preview_features: Vec, /// Avoid discovering a `pyproject.toml` or `uv.toml` file. /// /// Normally, configuration files are discovered in the current directory, /// parent directories, or user configuration directories. /// /// This option is deprecated in favor of `--no-config`. #[arg(global = true, long, hide = true, env = EnvVars::UV_ISOLATED, value_parser = clap::builder::BoolishValueParser::new())] pub isolated: bool, /// Show the resolved settings for the current command. /// /// This option is used for debugging and development purposes. #[arg(global = true, long, hide = true)] pub show_settings: bool, /// Hide all progress outputs. /// /// For example, spinners or progress bars. #[arg(global = true, long, env = EnvVars::UV_NO_PROGRESS, value_parser = clap::builder::BoolishValueParser::new())] pub no_progress: bool, /// Skip writing `uv` installer metadata files (e.g., `INSTALLER`, `REQUESTED`, and `direct_url.json`) to site-packages `.dist-info` directories. #[arg(global = true, long, hide = true, env = EnvVars::UV_NO_INSTALLER_METADATA, value_parser = clap::builder::BoolishValueParser::new())] pub no_installer_metadata: bool, /// Change to the given directory prior to running the command. /// /// Relative paths are resolved with the given directory as the base. /// /// See `--project` to only change the project root directory. #[arg(global = true, long, env = EnvVars::UV_WORKING_DIR)] pub directory: Option, /// Discover a project in the given directory. /// /// All `pyproject.toml`, `uv.toml`, and `.python-version` files will be discovered by walking /// up the directory tree from the project root, as will the project's virtual environment /// (`.venv`). /// /// Other command-line arguments (such as relative paths) will be resolved relative /// to the current working directory. /// /// See `--directory` to change the working directory entirely. /// /// This setting has no effect when used in the `uv pip` interface. #[arg(global = true, long, env = EnvVars::UV_PROJECT)] pub project: Option, } #[derive(Debug, Copy, Clone, clap::ValueEnum)] pub enum ColorChoice { /// Enables colored output only when the output is going to a terminal or TTY with support. Auto, /// Enables colored output regardless of the detected environment. Always, /// Disables colored output. Never, } impl ColorChoice { /// Combine self (higher priority) with an [`anstream::ColorChoice`] (lower priority). /// /// This method allows prioritizing the user choice, while using the inferred choice for a /// stream as default. #[must_use] pub fn and_colorchoice(self, next: anstream::ColorChoice) -> Self { match self { Self::Auto => match next { anstream::ColorChoice::Auto => Self::Auto, anstream::ColorChoice::Always | anstream::ColorChoice::AlwaysAnsi => Self::Always, anstream::ColorChoice::Never => Self::Never, }, Self::Always | Self::Never => self, } } } impl From for anstream::ColorChoice { fn from(value: ColorChoice) -> Self { match value { ColorChoice::Auto => Self::Auto, ColorChoice::Always => Self::Always, ColorChoice::Never => Self::Never, } } } #[derive(Subcommand)] #[allow(clippy::large_enum_variant)] pub enum Commands { /// Manage authentication. #[command( after_help = "Use `uv help auth` for more details.", after_long_help = "" )] Auth(AuthNamespace), /// Manage Python projects. #[command(flatten)] Project(Box), /// Run and install commands provided by Python packages. #[command( after_help = "Use `uv help tool` for more details.", after_long_help = "" )] Tool(ToolNamespace), /// Manage Python versions and installations /// /// Generally, uv first searches for Python in a virtual environment, either active or in a /// `.venv` directory in the current working directory or any parent directory. If a virtual /// environment is not required, uv will then search for a Python interpreter. Python /// interpreters are found by searching for Python executables in the `PATH` environment /// variable. /// /// On Windows, the registry is also searched for Python executables. /// /// By default, uv will download Python if a version cannot be found. This behavior can be /// disabled with the `--no-python-downloads` flag or the `python-downloads` setting. /// /// The `--python` option allows requesting a different interpreter. /// /// The following Python version request formats are supported: /// /// - `` e.g. `3`, `3.12`, `3.12.3` /// - `` e.g. `>=3.12,<3.13` /// - `` (e.g., `3.13t`, `3.12.0d`) /// - `+` (e.g., `3.13+freethreaded`, `3.12.0+debug`) /// - `` e.g. `cpython` or `cp` /// - `@` e.g. `cpython@3.12` /// - `` e.g. `cpython3.12` or `cp312` /// - `` e.g. `cpython>=3.12,<3.13` /// - `----` e.g. `cpython-3.12.3-macos-aarch64-none` /// /// Additionally, a specific system Python interpreter can often be requested with: /// /// - `` e.g. `/opt/homebrew/bin/python3` /// - `` e.g. `mypython3` /// - `` e.g. `/some/environment/` /// /// When the `--python` option is used, normal discovery rules apply but discovered interpreters /// are checked for compatibility with the request, e.g., if `pypy` is requested, uv will first /// check if the virtual environment contains a PyPy interpreter then check if each executable /// in the path is a PyPy interpreter. /// /// uv supports discovering CPython, PyPy, and GraalPy interpreters. Unsupported interpreters /// will be skipped during discovery. If an unsupported interpreter implementation is requested, /// uv will exit with an error. #[clap(verbatim_doc_comment)] #[command( after_help = "Use `uv help python` for more details.", after_long_help = "" )] Python(PythonNamespace), /// Manage Python packages with a pip-compatible interface. #[command( after_help = "Use `uv help pip` for more details.", after_long_help = "" )] Pip(PipNamespace), /// Create a virtual environment. /// /// By default, creates a virtual environment named `.venv` in the working /// directory. An alternative path may be provided positionally. /// /// If in a project, the default environment name can be changed with /// the `UV_PROJECT_ENVIRONMENT` environment variable; this only applies /// when run from the project root directory. /// /// If a virtual environment exists at the target path, it will be removed /// and a new, empty virtual environment will be created. /// /// When using uv, the virtual environment does not need to be activated. uv /// will find a virtual environment (named `.venv`) in the working directory /// or any parent directories. #[command( alias = "virtualenv", alias = "v", after_help = "Use `uv help venv` for more details.", after_long_help = "" )] Venv(VenvArgs), /// Build Python packages into source distributions and wheels. /// /// `uv build` accepts a path to a directory or source distribution, /// which defaults to the current working directory. /// /// By default, if passed a directory, `uv build` will build a source /// distribution ("sdist") from the source directory, and a binary /// distribution ("wheel") from the source distribution. /// /// `uv build --sdist` can be used to build only the source distribution, /// `uv build --wheel` can be used to build only the binary distribution, /// and `uv build --sdist --wheel` can be used to build both distributions /// from source. /// /// If passed a source distribution, `uv build --wheel` will build a wheel /// from the source distribution. #[command( after_help = "Use `uv help build` for more details.", after_long_help = "" )] Build(BuildArgs), /// Upload distributions to an index. Publish(PublishArgs), /// Inspect uv workspaces. #[command( after_help = "Use `uv help workspace` for more details.", after_long_help = "", hide = true )] Workspace(WorkspaceNamespace), /// The implementation of the build backend. /// /// These commands are not directly exposed to the user, instead users invoke their build /// frontend (PEP 517) which calls the Python shims which calls back into uv with this method. #[command(hide = true)] BuildBackend { #[command(subcommand)] command: BuildBackendCommand, }, /// Manage uv's cache. #[command( after_help = "Use `uv help cache` for more details.", after_long_help = "" )] Cache(CacheNamespace), /// Manage the uv executable. #[command(name = "self")] Self_(SelfNamespace), /// Clear the cache, removing all entries or those linked to specific packages. #[command(hide = true)] Clean(CleanArgs), /// Generate shell completion #[command(alias = "--generate-shell-completion", hide = true)] GenerateShellCompletion(GenerateShellCompletionArgs), /// Display documentation for a command. // To avoid showing the global options when displaying help for the help command, we are // responsible for maintaining the options using the `after_help`. #[command(help_template = "\ {about-with-newline} {usage-heading} {usage}{after-help} ", after_help = format!("\ {heading}Options:{heading:#} {option}--no-pager{option:#} Disable pager when printing help ", heading = Style::new().bold().underline(), option = Style::new().bold(), ), )] Help(HelpArgs), } #[derive(Args, Debug)] pub struct HelpArgs { /// Disable pager when printing help #[arg(long)] pub no_pager: bool, pub command: Option>, } #[derive(Args)] #[command(group = clap::ArgGroup::new("operation"))] pub struct VersionArgs { /// Set the project version to this value /// /// To update the project using semantic versioning components instead, use `--bump`. #[arg(group = "operation")] pub value: Option, /// Update the project version using the given semantics /// /// This flag can be passed multiple times. #[arg(group = "operation", long, value_name = "BUMP[=VALUE]")] pub bump: Vec, /// Don't write a new version to the `pyproject.toml` /// /// Instead, the version will be displayed. #[arg(long)] pub dry_run: bool, /// Only show the version /// /// By default, uv will show the project name before the version. #[arg(long)] pub short: bool, /// The format of the output #[arg(long, value_enum, default_value = "text")] pub output_format: VersionFormat, /// Avoid syncing the virtual environment after re-locking the project. #[arg(long, env = EnvVars::UV_NO_SYNC, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "frozen")] pub no_sync: bool, /// Prefer the active virtual environment over the project's virtual environment. /// /// If the project virtual environment is active or no virtual environment is active, this has /// no effect. #[arg(long, overrides_with = "no_active")] pub active: bool, /// Prefer project's virtual environment over an active environment. /// /// This is the default behavior. #[arg(long, overrides_with = "active", hide = true)] pub no_active: bool, /// Assert that the `uv.lock` will remain unchanged. /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, /// uv will exit with an error. #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, /// Update the version without re-locking the project. /// /// The project environment will not be synced. #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, #[command(flatten)] pub installer: ResolverInstallerArgs, #[command(flatten)] pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, /// Update the version of a specific package in the workspace. #[arg(long, conflicts_with = "isolated")] pub package: Option, /// The Python interpreter to use for resolving and syncing. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, } // Note that the ordering of the variants is significant, as when given a list of operations // to perform, we sort them and apply them in order, so users don't have to think too hard about it. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] pub enum VersionBump { /// Increase the major version (e.g., 1.2.3 => 2.0.0) Major, /// Increase the minor version (e.g., 1.2.3 => 1.3.0) Minor, /// Increase the patch version (e.g., 1.2.3 => 1.2.4) Patch, /// Move from a pre-release to stable version (e.g., 1.2.3b4.post5.dev6 => 1.2.3) /// /// Removes all pre-release components, but will not remove "local" components. Stable, /// Increase the alpha version (e.g., 1.2.3a4 => 1.2.3a5) /// /// To move from a stable to a pre-release version, combine this with a stable component, e.g., /// for 1.2.3 => 2.0.0a1, you'd also include [`VersionBump::Major`]. Alpha, /// Increase the beta version (e.g., 1.2.3b4 => 1.2.3b5) /// /// To move from a stable to a pre-release version, combine this with a stable component, e.g., /// for 1.2.3 => 2.0.0b1, you'd also include [`VersionBump::Major`]. Beta, /// Increase the rc version (e.g., 1.2.3rc4 => 1.2.3rc5) /// /// To move from a stable to a pre-release version, combine this with a stable component, e.g., /// for 1.2.3 => 2.0.0rc1, you'd also include [`VersionBump::Major`].] Rc, /// Increase the post version (e.g., 1.2.3.post5 => 1.2.3.post6) Post, /// Increase the dev version (e.g., 1.2.3a4.dev6 => 1.2.3.dev7) Dev, } impl Display for VersionBump { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let string = match self { Self::Major => "major", Self::Minor => "minor", Self::Patch => "patch", Self::Stable => "stable", Self::Alpha => "alpha", Self::Beta => "beta", Self::Rc => "rc", Self::Post => "post", Self::Dev => "dev", }; string.fmt(f) } } impl FromStr for VersionBump { type Err = String; fn from_str(value: &str) -> Result { match value { "major" => Ok(Self::Major), "minor" => Ok(Self::Minor), "patch" => Ok(Self::Patch), "stable" => Ok(Self::Stable), "alpha" => Ok(Self::Alpha), "beta" => Ok(Self::Beta), "rc" => Ok(Self::Rc), "post" => Ok(Self::Post), "dev" => Ok(Self::Dev), _ => Err(format!("invalid bump component `{value}`")), } } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct VersionBumpSpec { pub bump: VersionBump, pub value: Option, } impl Display for VersionBumpSpec { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self.value { Some(value) => write!(f, "{}={value}", self.bump), None => self.bump.fmt(f), } } } impl FromStr for VersionBumpSpec { type Err = String; fn from_str(input: &str) -> Result { let (name, value) = match input.split_once('=') { Some((name, value)) => (name, Some(value)), None => (input, None), }; let bump = name.parse::()?; if bump == VersionBump::Stable && value.is_some() { return Err("`--bump stable` does not accept a value".to_string()); } let value = match value { Some("") => { return Err("`--bump` values cannot be empty".to_string()); } Some(raw) => Some( raw.parse::() .map_err(|_| format!("invalid numeric value `{raw}` for `--bump {name}`"))?, ), None => None, }; Ok(Self { bump, value }) } } impl ValueParserFactory for VersionBumpSpec { type Parser = VersionBumpSpecValueParser; fn value_parser() -> Self::Parser { VersionBumpSpecValueParser } } #[derive(Clone, Debug)] pub struct VersionBumpSpecValueParser; impl TypedValueParser for VersionBumpSpecValueParser { type Value = VersionBumpSpec; fn parse_ref( &self, _cmd: &clap::Command, _arg: Option<&clap::Arg>, value: &std::ffi::OsStr, ) -> Result { let raw = value.to_str().ok_or_else(|| { clap::Error::raw( ErrorKind::InvalidUtf8, "`--bump` values must be valid UTF-8", ) })?; VersionBumpSpec::from_str(raw) .map_err(|message| clap::Error::raw(ErrorKind::InvalidValue, message)) } fn possible_values(&self) -> Option + '_>> { Some(Box::new( VersionBump::value_variants() .iter() .filter_map(ValueEnum::to_possible_value), )) } } #[derive(Args)] pub struct SelfNamespace { #[command(subcommand)] pub command: SelfCommand, } #[derive(Subcommand)] pub enum SelfCommand { /// Update uv. Update(SelfUpdateArgs), /// Display uv's version Version { /// Only print the version #[arg(long)] short: bool, #[arg(long, value_enum, default_value = "text")] output_format: VersionFormat, }, } #[derive(Args, Debug)] pub struct SelfUpdateArgs { /// Update to the specified version. If not provided, uv will update to the latest version. pub target_version: Option, /// A GitHub token for authentication. /// A token is not required but can be used to reduce the chance of encountering rate limits. #[arg(long, env = EnvVars::UV_GITHUB_TOKEN)] pub token: Option, /// Run without performing the update. #[arg(long)] pub dry_run: bool, } #[derive(Args)] pub struct CacheNamespace { #[command(subcommand)] pub command: CacheCommand, } #[derive(Subcommand)] pub enum CacheCommand { /// Clear the cache, removing all entries or those linked to specific packages. Clean(CleanArgs), /// Prune all unreachable objects from the cache. Prune(PruneArgs), /// Show the cache directory. /// /// By default, the cache is stored in `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on Unix and /// `%LOCALAPPDATA%\uv\cache` on Windows. /// /// When `--no-cache` is used, the cache is stored in a temporary directory and discarded when /// the process exits. /// /// An alternative cache directory may be specified via the `cache-dir` setting, the /// `--cache-dir` option, or the `$UV_CACHE_DIR` environment variable. /// /// Note that it is important for performance for the cache directory to be located on the same /// file system as the Python environment uv is operating on. Dir, /// Show the cache size. /// /// Displays the total size of the cache directory. This includes all downloaded and built /// wheels, source distributions, and other cached data. By default, outputs the size in raw /// bytes; use `--human` for human-readable output. Size(SizeArgs), } #[derive(Args, Debug)] pub struct CleanArgs { /// The packages to remove from the cache. pub package: Vec, /// Force removal of the cache, ignoring in-use checks. /// /// By default, `uv cache clean` will block until no process is reading the cache. When /// `--force` is used, `uv cache clean` will proceed without taking a lock. #[arg(long)] pub force: bool, } #[derive(Args, Debug)] pub struct PruneArgs { /// Optimize the cache for persistence in a continuous integration environment, like GitHub /// Actions. /// /// By default, uv caches both the wheels that it builds from source and the pre-built wheels /// that it downloads directly, to enable high-performance package installation. In some /// scenarios, though, persisting pre-built wheels may be undesirable. For example, in GitHub /// Actions, it's faster to omit pre-built wheels from the cache and instead have re-download /// them on each run. However, it typically _is_ faster to cache wheels that are built from /// source, since the wheel building process can be expensive, especially for extension /// modules. /// /// In `--ci` mode, uv will prune any pre-built wheels from the cache, but retain any wheels /// that were built from source. #[arg(long)] pub ci: bool, /// Force removal of the cache, ignoring in-use checks. /// /// By default, `uv cache prune` will block until no process is reading the cache. When /// `--force` is used, `uv cache prune` will proceed without taking a lock. #[arg(long)] pub force: bool, } #[derive(Args, Debug)] pub struct SizeArgs { /// Display the cache size in human-readable format (e.g., `1.2 GiB` instead of raw bytes). #[arg(long = "human", short = 'H', alias = "human-readable")] pub human: bool, } #[derive(Args)] pub struct PipNamespace { #[command(subcommand)] pub command: PipCommand, } #[derive(Subcommand)] pub enum PipCommand { /// Compile a `requirements.in` file to a `requirements.txt` or `pylock.toml` file. #[command( after_help = "Use `uv help pip compile` for more details.", after_long_help = "" )] Compile(PipCompileArgs), /// Sync an environment with a `requirements.txt` or `pylock.toml` file. /// /// When syncing an environment, any packages not listed in the `requirements.txt` or /// `pylock.toml` file will be removed. To retain extraneous packages, use `uv pip install` /// instead. /// /// The input file is presumed to be the output of a `pip compile` or `uv export` operation, /// in which it will include all transitive dependencies. If transitive dependencies are not /// present in the file, they will not be installed. Use `--strict` to warn if any transitive /// dependencies are missing. #[command( after_help = "Use `uv help pip sync` for more details.", after_long_help = "" )] Sync(Box), /// Install packages into an environment. #[command( after_help = "Use `uv help pip install` for more details.", after_long_help = "" )] Install(PipInstallArgs), /// Uninstall packages from an environment. #[command( after_help = "Use `uv help pip uninstall` for more details.", after_long_help = "" )] Uninstall(PipUninstallArgs), /// List, in requirements format, packages installed in an environment. #[command( after_help = "Use `uv help pip freeze` for more details.", after_long_help = "" )] Freeze(PipFreezeArgs), /// List, in tabular format, packages installed in an environment. #[command( after_help = "Use `uv help pip list` for more details.", after_long_help = "", alias = "ls" )] List(PipListArgs), /// Show information about one or more installed packages. #[command( after_help = "Use `uv help pip show` for more details.", after_long_help = "" )] Show(PipShowArgs), /// Display the dependency tree for an environment. #[command( after_help = "Use `uv help pip tree` for more details.", after_long_help = "" )] Tree(PipTreeArgs), /// Verify installed packages have compatible dependencies. #[command( after_help = "Use `uv help pip check` for more details.", after_long_help = "" )] Check(PipCheckArgs), /// Display debug information (unsupported) #[command(hide = true)] Debug(PipDebugArgs), } #[derive(Subcommand)] pub enum ProjectCommand { /// Run a command or script. /// /// Ensures that the command runs in a Python environment. /// /// When used with a file ending in `.py` or an HTTP(S) URL, the file will be treated as a /// script and run with a Python interpreter, i.e., `uv run file.py` is equivalent to `uv run /// python file.py`. For URLs, the script is temporarily downloaded before execution. If the /// script contains inline dependency metadata, it will be installed into an isolated, ephemeral /// environment. When used with `-`, the input will be read from stdin, and treated as a Python /// script. /// /// When used in a project, the project environment will be created and updated before invoking /// the command. /// /// When used outside a project, if a virtual environment can be found in the current directory /// or a parent directory, the command will be run in that environment. Otherwise, the command /// will be run in the environment of the discovered interpreter. /// /// Arguments following the command (or script) are not interpreted as arguments to uv. All /// options to uv must be provided before the command, e.g., `uv run --verbose foo`. A `--` can /// be used to separate the command from uv options for clarity, e.g., `uv run --python 3.12 -- /// python`. #[command( after_help = "Use `uv help run` for more details.", after_long_help = "" )] Run(RunArgs), /// Create a new project. /// /// Follows the `pyproject.toml` specification. /// /// If a `pyproject.toml` already exists at the target, uv will exit with an error. /// /// If a `pyproject.toml` is found in any of the parent directories of the target path, the /// project will be added as a workspace member of the parent. /// /// Some project state is not created until needed, e.g., the project virtual environment /// (`.venv`) and lockfile (`uv.lock`) are lazily created during the first sync. Init(InitArgs), /// Add dependencies to the project. /// /// Dependencies are added to the project's `pyproject.toml` file. /// /// If a given dependency exists already, it will be updated to the new version specifier unless /// it includes markers that differ from the existing specifier in which case another entry for /// the dependency will be added. /// /// The lockfile and project environment will be updated to reflect the added dependencies. To /// skip updating the lockfile, use `--frozen`. To skip updating the environment, use /// `--no-sync`. /// /// If any of the requested dependencies cannot be found, uv will exit with an error, unless the /// `--frozen` flag is provided, in which case uv will add the dependencies verbatim without /// checking that they exist or are compatible with the project. /// /// uv will search for a project in the current directory or any parent directory. If a project /// cannot be found, uv will exit with an error. #[command( after_help = "Use `uv help add` for more details.", after_long_help = "" )] Add(AddArgs), /// Remove dependencies from the project. /// /// Dependencies are removed from the project's `pyproject.toml` file. /// /// If multiple entries exist for a given dependency, i.e., each with different markers, all of /// the entries will be removed. /// /// The lockfile and project environment will be updated to reflect the removed dependencies. To /// skip updating the lockfile, use `--frozen`. To skip updating the environment, use /// `--no-sync`. /// /// If any of the requested dependencies are not present in the project, uv will exit with an /// error. /// /// If a package has been manually installed in the environment, i.e., with `uv pip install`, it /// will not be removed by `uv remove`. /// /// uv will search for a project in the current directory or any parent directory. If a project /// cannot be found, uv will exit with an error. #[command( after_help = "Use `uv help remove` for more details.", after_long_help = "" )] Remove(RemoveArgs), /// Read or update the project's version. Version(VersionArgs), /// Update the project's environment. /// /// Syncing ensures that all project dependencies are installed and up-to-date with the /// lockfile. /// /// By default, an exact sync is performed: uv removes packages that are not declared as /// dependencies of the project. Use the `--inexact` flag to keep extraneous packages. Note that /// if an extraneous package conflicts with a project dependency, it will still be removed. /// Additionally, if `--no-build-isolation` is used, uv will not remove extraneous packages to /// avoid removing possible build dependencies. /// /// If the project virtual environment (`.venv`) does not exist, it will be created. /// /// The project is re-locked before syncing unless the `--locked` or `--frozen` flag is /// provided. /// /// uv will search for a project in the current directory or any parent directory. If a project /// cannot be found, uv will exit with an error. /// /// Note that, when installing from a lockfile, uv will not provide warnings for yanked package /// versions. #[command( after_help = "Use `uv help sync` for more details.", after_long_help = "" )] Sync(SyncArgs), /// Update the project's lockfile. /// /// If the project lockfile (`uv.lock`) does not exist, it will be created. If a lockfile is /// present, its contents will be used as preferences for the resolution. /// /// If there are no changes to the project's dependencies, locking will have no effect unless /// the `--upgrade` flag is provided. #[command( after_help = "Use `uv help lock` for more details.", after_long_help = "" )] Lock(LockArgs), /// Export the project's lockfile to an alternate format. /// /// At present, both `requirements.txt` and `pylock.toml` (PEP 751) formats are supported. /// /// The project is re-locked before exporting unless the `--locked` or `--frozen` flag is /// provided. /// /// uv will search for a project in the current directory or any parent directory. If a project /// cannot be found, uv will exit with an error. /// /// If operating in a workspace, the root will be exported by default; however, specific /// members can be selected using the `--package` option. #[command( after_help = "Use `uv help export` for more details.", after_long_help = "" )] Export(ExportArgs), /// Display the project's dependency tree. Tree(TreeArgs), /// Format Python code in the project. /// /// Formats Python code using the Ruff formatter. By default, all Python files in the project /// are formatted. This command has the same behavior as running `ruff format` in the project /// root. /// /// To check if files are formatted without modifying them, use `--check`. To see a diff of /// formatting changes, use `--diff`. /// /// Additional arguments can be passed to Ruff after `--`. #[command( after_help = "Use `uv help format` for more details.", after_long_help = "" )] Format(FormatArgs), } /// A re-implementation of `Option`, used to avoid Clap's automatic `Option` flattening in /// [`parse_index_url`]. #[derive(Debug, Clone)] pub enum Maybe { Some(T), None, } impl Maybe { pub fn into_option(self) -> Option { match self { Self::Some(value) => Some(value), Self::None => None, } } pub fn is_some(&self) -> bool { matches!(self, Self::Some(_)) } } /// Parse an `--index-url` argument into an [`PipIndex`], mapping the empty string to `None`. fn parse_index_url(input: &str) -> Result, String> { if input.is_empty() { Ok(Maybe::None) } else { IndexUrl::from_str(input) .map(Index::from_index_url) .map(|index| Index { origin: Some(Origin::Cli), ..index }) .map(PipIndex::from) .map(Maybe::Some) .map_err(|err| err.to_string()) } } /// Parse an `--extra-index-url` argument into an [`PipExtraIndex`], mapping the empty string to `None`. fn parse_extra_index_url(input: &str) -> Result, String> { if input.is_empty() { Ok(Maybe::None) } else { IndexUrl::from_str(input) .map(Index::from_extra_index_url) .map(|index| Index { origin: Some(Origin::Cli), ..index }) .map(PipExtraIndex::from) .map(Maybe::Some) .map_err(|err| err.to_string()) } } /// Parse a `--find-links` argument into an [`PipFindLinks`], mapping the empty string to `None`. fn parse_find_links(input: &str) -> Result, String> { if input.is_empty() { Ok(Maybe::None) } else { IndexUrl::from_str(input) .map(Index::from_find_links) .map(|index| Index { origin: Some(Origin::Cli), ..index }) .map(PipFindLinks::from) .map(Maybe::Some) .map_err(|err| err.to_string()) } } /// Parse an `--index` argument into a [`Vec`], mapping the empty string to an empty Vec. /// /// This function splits the input on all whitespace characters rather than a single delimiter, /// which is necessary to parse environment variables like `PIP_EXTRA_INDEX_URL`. /// The standard `clap::Args` `value_delimiter` only supports single-character delimiters. fn parse_indices(input: &str) -> Result>, String> { if input.trim().is_empty() { return Ok(Vec::new()); } let mut indices = Vec::new(); for token in input.split_whitespace() { match Index::from_str(token) { Ok(index) => indices.push(Maybe::Some(Index { default: false, origin: Some(Origin::Cli), ..index })), Err(e) => return Err(e.to_string()), } } Ok(indices) } /// Parse a `--default-index` argument into an [`Index`], mapping the empty string to `None`. fn parse_default_index(input: &str) -> Result, String> { if input.is_empty() { Ok(Maybe::None) } else { match Index::from_str(input) { Ok(index) => Ok(Maybe::Some(Index { default: true, origin: Some(Origin::Cli), ..index })), Err(err) => Err(err.to_string()), } } } /// Parse a string into an [`Url`], mapping the empty string to `None`. fn parse_insecure_host(input: &str) -> Result, String> { if input.is_empty() { Ok(Maybe::None) } else { match TrustedHost::from_str(input) { Ok(host) => Ok(Maybe::Some(host)), Err(err) => Err(err.to_string()), } } } /// Parse a string into a [`PathBuf`]. The string can represent a file, either as a path or a /// `file://` URL. fn parse_file_path(input: &str) -> Result { if input.starts_with("file://") { let url = match url::Url::from_str(input) { Ok(url) => url, Err(err) => return Err(err.to_string()), }; url.to_file_path() .map_err(|()| "invalid file URL".to_string()) } else { Ok(PathBuf::from(input)) } } /// Parse a string into a [`PathBuf`], mapping the empty string to `None`. fn parse_maybe_file_path(input: &str) -> Result, String> { if input.is_empty() { Ok(Maybe::None) } else { parse_file_path(input).map(Maybe::Some) } } // Parse a string, mapping the empty string to `None`. #[allow(clippy::unnecessary_wraps)] fn parse_maybe_string(input: &str) -> Result, String> { if input.is_empty() { Ok(Maybe::None) } else { Ok(Maybe::Some(input.to_string())) } } #[derive(Args)] #[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))] pub struct PipCompileArgs { /// Include the packages listed in the given files. /// /// The following formats are supported: `requirements.txt`, `.py` files with inline metadata, /// `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg`. /// /// If a `pyproject.toml`, `setup.py`, or `setup.cfg` file is provided, uv will extract the /// requirements for the relevant project. /// /// If `-` is provided, then requirements will be read from stdin. /// /// The order of the requirements files and the requirements in them is used to determine /// priority during resolution. #[arg(group = "sources", value_parser = parse_file_path)] pub src_file: Vec, /// Constrain versions using the given requirements files. /// /// Constraints files are `requirements.txt`-like files that only control the _version_ of a /// requirement that's installed. However, including a package in a constraints file will _not_ /// trigger the installation of that package. /// /// This is equivalent to pip's `--constraint` option. #[arg(long, short, alias = "constraint", env = EnvVars::UV_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub constraints: Vec>, /// Override versions using the given requirements files. /// /// Overrides files are `requirements.txt`-like files that force a specific version of a /// requirement to be installed, regardless of the requirements declared by any constituent /// package, and regardless of whether this would be considered an invalid resolution. /// /// While constraints are _additive_, in that they're combined with the requirements of the /// constituent packages, overrides are _absolute_, in that they completely replace the /// requirements of the constituent packages. #[arg(long, alias = "override", env = EnvVars::UV_OVERRIDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub overrides: Vec>, /// Exclude packages from resolution using the given requirements files. /// /// Excludes files are `requirements.txt`-like files that specify packages to exclude /// from the resolution. When a package is excluded, it will be omitted from the /// dependency list entirely and its own dependencies will be ignored during the resolution /// phase. Excludes are unconditional in that requirement specifiers and markers are ignored; /// any package listed in the provided file will be omitted from all resolved environments. #[arg(long, alias = "exclude", env = EnvVars::UV_EXCLUDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub excludes: Vec>, /// Constrain build dependencies using the given requirements files when building source /// distributions. /// /// Constraints files are `requirements.txt`-like files that only control the _version_ of a /// requirement that's installed. However, including a package in a constraints file will _not_ /// trigger the installation of that package. #[arg(long, short, alias = "build-constraint", env = EnvVars::UV_BUILD_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub build_constraints: Vec>, /// Include optional dependencies from the specified extra name; may be provided more than once. /// /// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources. #[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] pub extra: Option>, /// Include all optional dependencies. /// /// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources. #[arg(long, conflicts_with = "extra")] pub all_extras: bool, #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, /// Install the specified dependency group from a `pyproject.toml`. /// /// If no path is provided, the `pyproject.toml` in the working directory is used. /// /// May be provided multiple times. #[arg(long, group = "sources")] pub group: Vec, #[command(flatten)] pub resolver: ResolverArgs, #[command(flatten)] pub refresh: RefreshArgs, /// Ignore package dependencies, instead only add those packages explicitly listed /// on the command line to the resulting requirements file. #[arg(long)] pub no_deps: bool, #[arg(long, overrides_with("no_deps"), hide = true)] pub deps: bool, /// Write the compiled requirements to the given `requirements.txt` or `pylock.toml` file. /// /// If the file already exists, the existing versions will be preferred when resolving /// dependencies, unless `--upgrade` is also specified. #[arg(long, short)] pub output_file: Option, /// The format in which the resolution should be output. /// /// Supports both `requirements.txt` and `pylock.toml` (PEP 751) output formats. /// /// uv will infer the output format from the file extension of the output file, if /// provided. Otherwise, defaults to `requirements.txt`. #[arg(long, value_enum)] pub format: Option, /// Include extras in the output file. /// /// By default, uv strips extras, as any packages pulled in by the extras are already included /// as dependencies in the output file directly. Further, output files generated with /// `--no-strip-extras` cannot be used as constraints files in `install` and `sync` invocations. #[arg(long, overrides_with("strip_extras"))] pub no_strip_extras: bool, #[arg(long, overrides_with("no_strip_extras"), hide = true)] pub strip_extras: bool, /// Include environment markers in the output file. /// /// By default, uv strips environment markers, as the resolution generated by `compile` is /// only guaranteed to be correct for the target environment. #[arg(long, overrides_with("strip_markers"))] pub no_strip_markers: bool, #[arg(long, overrides_with("no_strip_markers"), hide = true)] pub strip_markers: bool, /// Exclude comment annotations indicating the source of each package. #[arg(long, overrides_with("annotate"))] pub no_annotate: bool, #[arg(long, overrides_with("no_annotate"), hide = true)] pub annotate: bool, /// Exclude the comment header at the top of the generated output file. #[arg(long, overrides_with("header"))] pub no_header: bool, #[arg(long, overrides_with("no_header"), hide = true)] pub header: bool, /// The style of the annotation comments included in the output file, used to indicate the /// source of each package. /// /// Defaults to `split`. #[arg(long, value_enum)] pub annotation_style: Option, /// The header comment to include at the top of the output file generated by `uv pip compile`. /// /// Used to reflect custom build scripts and commands that wrap `uv pip compile`. #[arg(long, env = EnvVars::UV_CUSTOM_COMPILE_COMMAND)] pub custom_compile_command: Option, /// The Python interpreter to use during resolution. /// /// A Python interpreter is required for building source distributions to determine package /// metadata when there are not wheels. /// /// The interpreter is also used to determine the default minimum Python version, unless /// `--python-version` is provided. /// /// This option respects `UV_PYTHON`, but when set via environment variable, it is overridden /// by `--python-version`. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string )] pub python: Option>, /// Install packages into the system Python environment. /// /// By default, uv uses the virtual environment in the current working directory or any parent /// directory, falling back to searching for a Python executable in `PATH`. The `--system` /// option instructs uv to avoid using a virtual environment Python and restrict its search to /// the system path. #[arg( long, env = EnvVars::UV_SYSTEM_PYTHON, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_system") )] pub system: bool, #[arg(long, overrides_with("system"), hide = true)] pub no_system: bool, /// Include distribution hashes in the output file. #[arg(long, overrides_with("no_generate_hashes"))] pub generate_hashes: bool, #[arg(long, overrides_with("generate_hashes"), hide = true)] pub no_generate_hashes: bool, /// Don't build source distributions. /// /// When enabled, resolving will not run arbitrary Python code. The cached wheels of /// already-built source distributions will be reused, but operations that require building /// distributions will exit with an error. /// /// Alias for `--only-binary :all:`. #[arg( long, conflicts_with = "no_binary", conflicts_with = "only_binary", overrides_with("build") )] pub no_build: bool, #[arg( long, conflicts_with = "no_binary", conflicts_with = "only_binary", overrides_with("no_build"), hide = true )] pub build: bool, /// Don't install pre-built wheels. /// /// The given packages will be built and installed from source. The resolver will still use /// pre-built wheels to extract package metadata, if available. /// /// Multiple packages may be provided. Disable binaries for all packages with `:all:`. /// Clear previously specified packages with `:none:`. #[arg(long, conflicts_with = "no_build")] pub no_binary: Option>, /// Only use pre-built wheels; don't build source distributions. /// /// When enabled, resolving will not run code from the given packages. The cached wheels of already-built /// source distributions will be reused, but operations that require building distributions will /// exit with an error. /// /// Multiple packages may be provided. Disable binaries for all packages with `:all:`. /// Clear previously specified packages with `:none:`. #[arg(long, conflicts_with = "no_build")] pub only_binary: Option>, /// The Python version to use for resolution. /// /// For example, `3.8` or `3.8.17`. /// /// Defaults to the version of the Python interpreter used for resolution. /// /// Defines the minimum Python version that must be supported by the /// resolved requirements. /// /// If a patch version is omitted, the minimum patch version is assumed. For /// example, `3.8` is mapped to `3.8.0`. #[arg(long, help_heading = "Python options")] pub python_version: Option, /// The platform for which requirements should be resolved. /// /// Represented as a "target triple", a string that describes the target platform in terms of /// its CPU, vendor, and operating system name, like `x86_64-unknown-linux-gnu` or /// `aarch64-apple-darwin`. /// /// When targeting macOS (Darwin), the default minimum version is `13.0`. Use /// `MACOSX_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting iOS, the default minimum version is `13.0`. Use /// `IPHONEOS_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting Android, the default minimum Android API level is `24`. Use /// `ANDROID_API_LEVEL` to specify a different minimum version, e.g., `26`. #[arg(long)] pub python_platform: Option, /// Perform a universal resolution, attempting to generate a single `requirements.txt` output /// file that is compatible with all operating systems, architectures, and Python /// implementations. /// /// In universal mode, the current Python version (or user-provided `--python-version`) will be /// treated as a lower bound. For example, `--universal --python-version 3.7` would produce a /// universal resolution for Python 3.7 and later. /// /// Implies `--no-strip-markers`. #[arg( long, overrides_with("no_universal"), conflicts_with("python_platform"), conflicts_with("strip_markers") )] pub universal: bool, #[arg(long, overrides_with("universal"), hide = true)] pub no_universal: bool, /// Specify a package to omit from the output resolution. Its dependencies will still be /// included in the resolution. Equivalent to pip-compile's `--unsafe-package` option. #[arg(long, alias = "unsafe-package")] pub no_emit_package: Option>, /// Include `--index-url` and `--extra-index-url` entries in the generated output file. #[arg(long, overrides_with("no_emit_index_url"))] pub emit_index_url: bool, #[arg(long, overrides_with("emit_index_url"), hide = true)] pub no_emit_index_url: bool, /// Include `--find-links` entries in the generated output file. #[arg(long, overrides_with("no_emit_find_links"))] pub emit_find_links: bool, #[arg(long, overrides_with("emit_find_links"), hide = true)] pub no_emit_find_links: bool, /// Include `--no-binary` and `--only-binary` entries in the generated output file. #[arg(long, overrides_with("no_emit_build_options"))] pub emit_build_options: bool, #[arg(long, overrides_with("emit_build_options"), hide = true)] pub no_emit_build_options: bool, /// Whether to emit a marker string indicating when it is known that the /// resulting set of pinned dependencies is valid. /// /// The pinned dependencies may be valid even when the marker expression is /// false, but when the expression is true, the requirements are known to /// be correct. #[arg(long, overrides_with("no_emit_marker_expression"), hide = true)] pub emit_marker_expression: bool, #[arg(long, overrides_with("emit_marker_expression"), hide = true)] pub no_emit_marker_expression: bool, /// Include comment annotations indicating the index used to resolve each package (e.g., /// `# from https://pypi.org/simple`). #[arg(long, overrides_with("no_emit_index_annotation"))] pub emit_index_annotation: bool, #[arg(long, overrides_with("emit_index_annotation"), hide = true)] pub no_emit_index_annotation: bool, /// The backend to use when fetching packages in the PyTorch ecosystem (e.g., `cpu`, `cu126`, or `auto`). /// /// When set, uv will ignore the configured index URLs for packages in the PyTorch ecosystem, /// and will instead use the defined backend. /// /// For example, when set to `cpu`, uv will use the CPU-only PyTorch index; when set to `cu126`, /// uv will use the PyTorch index for CUDA 12.6. /// /// The `auto` mode will attempt to detect the appropriate PyTorch index based on the currently /// installed CUDA drivers. /// /// This option is in preview and may change in any future release. #[arg(long, value_enum, env = EnvVars::UV_TORCH_BACKEND)] pub torch_backend: Option, #[command(flatten)] pub compat_args: compat::PipCompileCompatArgs, } #[derive(Args)] pub struct PipSyncArgs { /// Include the packages listed in the given files. /// /// The following formats are supported: `requirements.txt`, `.py` files with inline metadata, /// `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg`. /// /// If a `pyproject.toml`, `setup.py`, or `setup.cfg` file is provided, uv will /// extract the requirements for the relevant project. /// /// If `-` is provided, then requirements will be read from stdin. #[arg(required(true), value_parser = parse_file_path)] pub src_file: Vec, /// Constrain versions using the given requirements files. /// /// Constraints files are `requirements.txt`-like files that only control the _version_ of a /// requirement that's installed. However, including a package in a constraints file will _not_ /// trigger the installation of that package. /// /// This is equivalent to pip's `--constraint` option. #[arg(long, short, alias = "constraint", env = EnvVars::UV_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub constraints: Vec>, /// Constrain build dependencies using the given requirements files when building source /// distributions. /// /// Constraints files are `requirements.txt`-like files that only control the _version_ of a /// requirement that's installed. However, including a package in a constraints file will _not_ /// trigger the installation of that package. #[arg(long, short, alias = "build-constraint", env = EnvVars::UV_BUILD_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub build_constraints: Vec>, /// Include optional dependencies from the specified extra name; may be provided more than once. /// /// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources. #[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] pub extra: Option>, /// Include all optional dependencies. /// /// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources. #[arg(long, conflicts_with = "extra", overrides_with = "no_all_extras")] pub all_extras: bool, #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, /// Install the specified dependency group from a `pylock.toml` or `pyproject.toml`. /// /// If no path is provided, the `pylock.toml` or `pyproject.toml` in the working directory is /// used. /// /// May be provided multiple times. #[arg(long, group = "sources")] pub group: Vec, #[command(flatten)] pub installer: InstallerArgs, #[command(flatten)] pub refresh: RefreshArgs, /// Require a matching hash for each requirement. /// /// By default, uv will verify any available hashes in the requirements file, but will not /// require that all requirements have an associated hash. /// /// When `--require-hashes` is enabled, _all_ requirements must include a hash or set of hashes, /// and _all_ requirements must either be pinned to exact versions (e.g., `==1.0.0`), or be /// specified via direct URL. /// /// Hash-checking mode introduces a number of additional constraints: /// /// - Git dependencies are not supported. /// - Editable installations are not supported. /// - Local dependencies are not supported, unless they point to a specific wheel (`.whl`) or /// source archive (`.zip`, `.tar.gz`), as opposed to a directory. #[arg( long, env = EnvVars::UV_REQUIRE_HASHES, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_require_hashes"), )] pub require_hashes: bool, #[arg(long, overrides_with("require_hashes"), hide = true)] pub no_require_hashes: bool, #[arg(long, overrides_with("no_verify_hashes"), hide = true)] pub verify_hashes: bool, /// Disable validation of hashes in the requirements file. /// /// By default, uv will verify any available hashes in the requirements file, but will not /// require that all requirements have an associated hash. To enforce hash validation, use /// `--require-hashes`. #[arg( long, env = EnvVars::UV_NO_VERIFY_HASHES, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("verify_hashes"), )] pub no_verify_hashes: bool, /// The Python interpreter into which packages should be installed. /// /// By default, syncing requires a virtual environment. A path to an alternative Python can be /// provided, but it is only recommended in continuous integration (CI) environments and should /// be used with caution, as it can modify the system Python installation. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// Install packages into the system Python environment. /// /// By default, uv installs into the virtual environment in the current working directory or any /// parent directory. The `--system` option instructs uv to instead use the first Python found /// in the system `PATH`. /// /// WARNING: `--system` is intended for use in continuous integration (CI) environments and /// should be used with caution, as it can modify the system Python installation. #[arg( long, env = EnvVars::UV_SYSTEM_PYTHON, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_system") )] pub system: bool, #[arg(long, overrides_with("system"), hide = true)] pub no_system: bool, /// Allow uv to modify an `EXTERNALLY-MANAGED` Python installation. /// /// WARNING: `--break-system-packages` is intended for use in continuous integration (CI) /// environments, when installing into Python installations that are managed by an external /// package manager, like `apt`. It should be used with caution, as such Python installations /// explicitly recommend against modifications by other package managers (like uv or `pip`). #[arg( long, env = EnvVars::UV_BREAK_SYSTEM_PACKAGES, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_break_system_packages") )] pub break_system_packages: bool, #[arg(long, overrides_with("break_system_packages"))] pub no_break_system_packages: bool, /// Install packages into the specified directory, rather than into the virtual or system Python /// environment. The packages will be installed at the top-level of the directory. /// /// Unlike other install operations, this command does not require discovery of an existing Python /// environment and only searches for a Python interpreter to use for package resolution. /// If a suitable Python interpreter cannot be found, uv will install one. /// To disable this, add `--no-python-downloads`. #[arg(long, conflicts_with = "prefix")] pub target: Option, /// Install packages into `lib`, `bin`, and other top-level folders under the specified /// directory, as if a virtual environment were present at that location. /// /// In general, prefer the use of `--python` to install into an alternate environment, as /// scripts and other artifacts installed via `--prefix` will reference the installing /// interpreter, rather than any interpreter added to the `--prefix` directory, rendering them /// non-portable. /// /// Unlike other install operations, this command does not require discovery of an existing Python /// environment and only searches for a Python interpreter to use for package resolution. /// If a suitable Python interpreter cannot be found, uv will install one. /// To disable this, add `--no-python-downloads`. #[arg(long, conflicts_with = "target")] pub prefix: Option, /// Don't build source distributions. /// /// When enabled, resolving will not run arbitrary Python code. The cached wheels of /// already-built source distributions will be reused, but operations that require building /// distributions will exit with an error. /// /// Alias for `--only-binary :all:`. #[arg( long, conflicts_with = "no_binary", conflicts_with = "only_binary", overrides_with("build") )] pub no_build: bool, #[arg( long, conflicts_with = "no_binary", conflicts_with = "only_binary", overrides_with("no_build"), hide = true )] pub build: bool, /// Don't install pre-built wheels. /// /// The given packages will be built and installed from source. The resolver will still use /// pre-built wheels to extract package metadata, if available. /// /// Multiple packages may be provided. Disable binaries for all packages with `:all:`. Clear /// previously specified packages with `:none:`. #[arg(long, conflicts_with = "no_build")] pub no_binary: Option>, /// Only use pre-built wheels; don't build source distributions. /// /// When enabled, resolving will not run code from the given packages. The cached wheels of /// already-built source distributions will be reused, but operations that require building /// distributions will exit with an error. /// /// Multiple packages may be provided. Disable binaries for all packages with `:all:`. Clear /// previously specified packages with `:none:`. #[arg(long, conflicts_with = "no_build")] pub only_binary: Option>, /// Allow sync of empty requirements, which will clear the environment of all packages. #[arg(long, overrides_with("no_allow_empty_requirements"))] pub allow_empty_requirements: bool, #[arg(long, overrides_with("allow_empty_requirements"))] pub no_allow_empty_requirements: bool, /// The minimum Python version that should be supported by the requirements (e.g., `3.7` or /// `3.7.9`). /// /// If a patch version is omitted, the minimum patch version is assumed. For example, `3.7` is /// mapped to `3.7.0`. #[arg(long)] pub python_version: Option, /// The platform for which requirements should be installed. /// /// Represented as a "target triple", a string that describes the target platform in terms of /// its CPU, vendor, and operating system name, like `x86_64-unknown-linux-gnu` or /// `aarch64-apple-darwin`. /// /// When targeting macOS (Darwin), the default minimum version is `13.0`. Use /// `MACOSX_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting iOS, the default minimum version is `13.0`. Use /// `IPHONEOS_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting Android, the default minimum Android API level is `24`. Use /// `ANDROID_API_LEVEL` to specify a different minimum version, e.g., `26`. /// /// WARNING: When specified, uv will select wheels that are compatible with the _target_ /// platform; as a result, the installed distributions may not be compatible with the _current_ /// platform. Conversely, any distributions that are built from source may be incompatible with /// the _target_ platform, as they will be built for the _current_ platform. The /// `--python-platform` option is intended for advanced use cases. #[arg(long)] pub python_platform: Option, /// Validate the Python environment after completing the installation, to detect packages with /// missing dependencies or other issues. #[arg(long, overrides_with("no_strict"))] pub strict: bool, #[arg(long, overrides_with("strict"), hide = true)] pub no_strict: bool, /// Perform a dry run, i.e., don't actually install anything but resolve the dependencies and /// print the resulting plan. #[arg(long)] pub dry_run: bool, /// The backend to use when fetching packages in the PyTorch ecosystem (e.g., `cpu`, `cu126`, or `auto`). /// /// When set, uv will ignore the configured index URLs for packages in the PyTorch ecosystem, /// and will instead use the defined backend. /// /// For example, when set to `cpu`, uv will use the CPU-only PyTorch index; when set to `cu126`, /// uv will use the PyTorch index for CUDA 12.6. /// /// The `auto` mode will attempt to detect the appropriate PyTorch index based on the currently /// installed CUDA drivers. /// /// This option is in preview and may change in any future release. #[arg(long, value_enum, env = EnvVars::UV_TORCH_BACKEND)] pub torch_backend: Option, #[command(flatten)] pub compat_args: compat::PipSyncCompatArgs, } #[derive(Args)] #[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))] pub struct PipInstallArgs { /// Install all listed packages. /// /// The order of the packages is used to determine priority during resolution. #[arg(group = "sources")] pub package: Vec, /// Install the packages listed in the given files. /// /// The following formats are supported: `requirements.txt`, `.py` files with inline metadata, /// `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg`. /// /// If a `pyproject.toml`, `setup.py`, or `setup.cfg` file is provided, uv will extract the /// requirements for the relevant project. /// /// If `-` is provided, then requirements will be read from stdin. #[arg(long, short, alias = "requirement", group = "sources", value_parser = parse_file_path)] pub requirements: Vec, /// Install the editable package based on the provided local file path. #[arg(long, short, group = "sources")] pub editable: Vec, /// Constrain versions using the given requirements files. /// /// Constraints files are `requirements.txt`-like files that only control the _version_ of a /// requirement that's installed. However, including a package in a constraints file will _not_ /// trigger the installation of that package. /// /// This is equivalent to pip's `--constraint` option. #[arg(long, short, alias = "constraint", env = EnvVars::UV_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub constraints: Vec>, /// Override versions using the given requirements files. /// /// Overrides files are `requirements.txt`-like files that force a specific version of a /// requirement to be installed, regardless of the requirements declared by any constituent /// package, and regardless of whether this would be considered an invalid resolution. /// /// While constraints are _additive_, in that they're combined with the requirements of the /// constituent packages, overrides are _absolute_, in that they completely replace the /// requirements of the constituent packages. #[arg(long, alias = "override", env = EnvVars::UV_OVERRIDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub overrides: Vec>, /// Exclude packages from resolution using the given requirements files. /// /// Excludes files are `requirements.txt`-like files that specify packages to exclude /// from the resolution. When a package is excluded, it will be omitted from the /// dependency list entirely and its own dependencies will be ignored during the resolution /// phase. Excludes are unconditional in that requirement specifiers and markers are ignored; /// any package listed in the provided file will be omitted from all resolved environments. #[arg(long, alias = "exclude", env = EnvVars::UV_EXCLUDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub excludes: Vec>, /// Constrain build dependencies using the given requirements files when building source /// distributions. /// /// Constraints files are `requirements.txt`-like files that only control the _version_ of a /// requirement that's installed. However, including a package in a constraints file will _not_ /// trigger the installation of that package. #[arg(long, short, alias = "build-constraint", env = EnvVars::UV_BUILD_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub build_constraints: Vec>, /// Include optional dependencies from the specified extra name; may be provided more than once. /// /// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources. #[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] pub extra: Option>, /// Include all optional dependencies. /// /// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources. #[arg(long, conflicts_with = "extra", overrides_with = "no_all_extras")] pub all_extras: bool, #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, /// Install the specified dependency group from a `pylock.toml` or `pyproject.toml`. /// /// If no path is provided, the `pylock.toml` or `pyproject.toml` in the working directory is /// used. /// /// May be provided multiple times. #[arg(long, group = "sources")] pub group: Vec, #[command(flatten)] pub installer: ResolverInstallerArgs, #[command(flatten)] pub refresh: RefreshArgs, /// Ignore package dependencies, instead only installing those packages explicitly listed /// on the command line or in the requirements files. #[arg(long, overrides_with("deps"))] pub no_deps: bool, #[arg(long, overrides_with("no_deps"), hide = true)] pub deps: bool, /// Require a matching hash for each requirement. /// /// By default, uv will verify any available hashes in the requirements file, but will not /// require that all requirements have an associated hash. /// /// When `--require-hashes` is enabled, _all_ requirements must include a hash or set of hashes, /// and _all_ requirements must either be pinned to exact versions (e.g., `==1.0.0`), or be /// specified via direct URL. /// /// Hash-checking mode introduces a number of additional constraints: /// /// - Git dependencies are not supported. /// - Editable installations are not supported. /// - Local dependencies are not supported, unless they point to a specific wheel (`.whl`) or /// source archive (`.zip`, `.tar.gz`), as opposed to a directory. #[arg( long, env = EnvVars::UV_REQUIRE_HASHES, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_require_hashes"), )] pub require_hashes: bool, #[arg(long, overrides_with("require_hashes"), hide = true)] pub no_require_hashes: bool, #[arg(long, overrides_with("no_verify_hashes"), hide = true)] pub verify_hashes: bool, /// Disable validation of hashes in the requirements file. /// /// By default, uv will verify any available hashes in the requirements file, but will not /// require that all requirements have an associated hash. To enforce hash validation, use /// `--require-hashes`. #[arg( long, env = EnvVars::UV_NO_VERIFY_HASHES, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("verify_hashes"), )] pub no_verify_hashes: bool, /// The Python interpreter into which packages should be installed. /// /// By default, installation requires a virtual environment. A path to an alternative Python can /// be provided, but it is only recommended in continuous integration (CI) environments and /// should be used with caution, as it can modify the system Python installation. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// Install packages into the system Python environment. /// /// By default, uv installs into the virtual environment in the current working directory or any /// parent directory. The `--system` option instructs uv to instead use the first Python found /// in the system `PATH`. /// /// WARNING: `--system` is intended for use in continuous integration (CI) environments and /// should be used with caution, as it can modify the system Python installation. #[arg( long, env = EnvVars::UV_SYSTEM_PYTHON, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_system") )] pub system: bool, #[arg(long, overrides_with("system"), hide = true)] pub no_system: bool, /// Allow uv to modify an `EXTERNALLY-MANAGED` Python installation. /// /// WARNING: `--break-system-packages` is intended for use in continuous integration (CI) /// environments, when installing into Python installations that are managed by an external /// package manager, like `apt`. It should be used with caution, as such Python installations /// explicitly recommend against modifications by other package managers (like uv or `pip`). #[arg( long, env = EnvVars::UV_BREAK_SYSTEM_PACKAGES, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_break_system_packages") )] pub break_system_packages: bool, #[arg(long, overrides_with("break_system_packages"))] pub no_break_system_packages: bool, /// Install packages into the specified directory, rather than into the virtual or system Python /// environment. The packages will be installed at the top-level of the directory. /// /// Unlike other install operations, this command does not require discovery of an existing Python /// environment and only searches for a Python interpreter to use for package resolution. /// If a suitable Python interpreter cannot be found, uv will install one. /// To disable this, add `--no-python-downloads`. #[arg(long, conflicts_with = "prefix")] pub target: Option, /// Install packages into `lib`, `bin`, and other top-level folders under the specified /// directory, as if a virtual environment were present at that location. /// /// In general, prefer the use of `--python` to install into an alternate environment, as /// scripts and other artifacts installed via `--prefix` will reference the installing /// interpreter, rather than any interpreter added to the `--prefix` directory, rendering them /// non-portable. /// /// Unlike other install operations, this command does not require discovery of an existing Python /// environment and only searches for a Python interpreter to use for package resolution. /// If a suitable Python interpreter cannot be found, uv will install one. /// To disable this, add `--no-python-downloads`. #[arg(long, conflicts_with = "target")] pub prefix: Option, /// Don't build source distributions. /// /// When enabled, resolving will not run arbitrary Python code. The cached wheels of /// already-built source distributions will be reused, but operations that require building /// distributions will exit with an error. /// /// Alias for `--only-binary :all:`. #[arg( long, conflicts_with = "no_binary", conflicts_with = "only_binary", overrides_with("build") )] pub no_build: bool, #[arg( long, conflicts_with = "no_binary", conflicts_with = "only_binary", overrides_with("no_build"), hide = true )] pub build: bool, /// Don't install pre-built wheels. /// /// The given packages will be built and installed from source. The resolver will still use /// pre-built wheels to extract package metadata, if available. /// /// Multiple packages may be provided. Disable binaries for all packages with `:all:`. Clear /// previously specified packages with `:none:`. #[arg(long, conflicts_with = "no_build")] pub no_binary: Option>, /// Only use pre-built wheels; don't build source distributions. /// /// When enabled, resolving will not run code from the given packages. The cached wheels of /// already-built source distributions will be reused, but operations that require building /// distributions will exit with an error. /// /// Multiple packages may be provided. Disable binaries for all packages with `:all:`. Clear /// previously specified packages with `:none:`. #[arg(long, conflicts_with = "no_build")] pub only_binary: Option>, /// The minimum Python version that should be supported by the requirements (e.g., `3.7` or /// `3.7.9`). /// /// If a patch version is omitted, the minimum patch version is assumed. For example, `3.7` is /// mapped to `3.7.0`. #[arg(long)] pub python_version: Option, /// The platform for which requirements should be installed. /// /// Represented as a "target triple", a string that describes the target platform in terms of /// its CPU, vendor, and operating system name, like `x86_64-unknown-linux-gnu` or /// `aarch64-apple-darwin`. /// /// When targeting macOS (Darwin), the default minimum version is `13.0`. Use /// `MACOSX_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting iOS, the default minimum version is `13.0`. Use /// `IPHONEOS_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting Android, the default minimum Android API level is `24`. Use /// `ANDROID_API_LEVEL` to specify a different minimum version, e.g., `26`. /// /// WARNING: When specified, uv will select wheels that are compatible with the _target_ /// platform; as a result, the installed distributions may not be compatible with the _current_ /// platform. Conversely, any distributions that are built from source may be incompatible with /// the _target_ platform, as they will be built for the _current_ platform. The /// `--python-platform` option is intended for advanced use cases. #[arg(long)] pub python_platform: Option, /// Do not remove extraneous packages present in the environment. #[arg(long, overrides_with("exact"), alias = "no-exact", hide = true)] pub inexact: bool, /// Perform an exact sync, removing extraneous packages. /// /// By default, installing will make the minimum necessary changes to satisfy the requirements. /// When enabled, uv will update the environment to exactly match the requirements, removing /// packages that are not included in the requirements. #[arg(long, overrides_with("inexact"))] pub exact: bool, /// Validate the Python environment after completing the installation, to detect packages with /// missing dependencies or other issues. #[arg(long, overrides_with("no_strict"))] pub strict: bool, #[arg(long, overrides_with("strict"), hide = true)] pub no_strict: bool, /// Perform a dry run, i.e., don't actually install anything but resolve the dependencies and /// print the resulting plan. #[arg(long)] pub dry_run: bool, /// The backend to use when fetching packages in the PyTorch ecosystem (e.g., `cpu`, `cu126`, or `auto`) /// /// When set, uv will ignore the configured index URLs for packages in the PyTorch ecosystem, /// and will instead use the defined backend. /// /// For example, when set to `cpu`, uv will use the CPU-only PyTorch index; when set to `cu126`, /// uv will use the PyTorch index for CUDA 12.6. /// /// The `auto` mode will attempt to detect the appropriate PyTorch index based on the currently /// installed CUDA drivers. /// /// This option is in preview and may change in any future release. #[arg(long, value_enum, env = EnvVars::UV_TORCH_BACKEND)] pub torch_backend: Option, #[command(flatten)] pub compat_args: compat::PipInstallCompatArgs, } #[derive(Args)] #[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))] pub struct PipUninstallArgs { /// Uninstall all listed packages. #[arg(group = "sources")] pub package: Vec, /// Uninstall the packages listed in the given files. /// /// The following formats are supported: `requirements.txt`, `.py` files with inline metadata, /// `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg`. #[arg(long, short, alias = "requirement", group = "sources", value_parser = parse_file_path)] pub requirements: Vec, /// The Python interpreter from which packages should be uninstalled. /// /// By default, uninstallation requires a virtual environment. A path to an alternative Python /// can be provided, but it is only recommended in continuous integration (CI) environments and /// should be used with caution, as it can modify the system Python installation. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// Attempt to use `keyring` for authentication for remote requirements files. /// /// At present, only `--keyring-provider subprocess` is supported, which configures uv to use /// the `keyring` CLI to handle authentication. /// /// Defaults to `disabled`. #[arg(long, value_enum, env = EnvVars::UV_KEYRING_PROVIDER)] pub keyring_provider: Option, /// Use the system Python to uninstall packages. /// /// By default, uv uninstalls from the virtual environment in the current working directory or /// any parent directory. The `--system` option instructs uv to instead use the first Python /// found in the system `PATH`. /// /// WARNING: `--system` is intended for use in continuous integration (CI) environments and /// should be used with caution, as it can modify the system Python installation. #[arg( long, env = EnvVars::UV_SYSTEM_PYTHON, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_system") )] pub system: bool, #[arg(long, overrides_with("system"), hide = true)] pub no_system: bool, /// Allow uv to modify an `EXTERNALLY-MANAGED` Python installation. /// /// WARNING: `--break-system-packages` is intended for use in continuous integration (CI) /// environments, when installing into Python installations that are managed by an external /// package manager, like `apt`. It should be used with caution, as such Python installations /// explicitly recommend against modifications by other package managers (like uv or `pip`). #[arg( long, env = EnvVars::UV_BREAK_SYSTEM_PACKAGES, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_break_system_packages") )] pub break_system_packages: bool, #[arg(long, overrides_with("break_system_packages"))] pub no_break_system_packages: bool, /// Uninstall packages from the specified `--target` directory. #[arg(long, conflicts_with = "prefix")] pub target: Option, /// Uninstall packages from the specified `--prefix` directory. #[arg(long, conflicts_with = "target")] pub prefix: Option, /// Perform a dry run, i.e., don't actually uninstall anything but print the resulting plan. #[arg(long)] pub dry_run: bool, #[command(flatten)] pub compat_args: compat::PipGlobalCompatArgs, } #[derive(Args)] pub struct PipFreezeArgs { /// Exclude any editable packages from output. #[arg(long)] pub exclude_editable: bool, /// Validate the Python environment, to detect packages with missing dependencies and other /// issues. #[arg(long, overrides_with("no_strict"))] pub strict: bool, #[arg(long, overrides_with("strict"), hide = true)] pub no_strict: bool, /// The Python interpreter for which packages should be listed. /// /// By default, uv lists packages in a virtual environment but will show packages in a system /// Python environment if no virtual environment is found. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// Restrict to the specified installation path for listing packages (can be used multiple times). #[arg(long("path"), value_parser = parse_file_path)] pub paths: Option>, /// List packages in the system Python environment. /// /// Disables discovery of virtual environments. /// /// See `uv help python` for details on Python discovery. #[arg( long, env = EnvVars::UV_SYSTEM_PYTHON, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_system") )] pub system: bool, #[arg(long, overrides_with("system"), hide = true)] pub no_system: bool, /// List packages from the specified `--target` directory. #[arg(long, conflicts_with_all = ["prefix", "paths"])] pub target: Option, /// List packages from the specified `--prefix` directory. #[arg(long, conflicts_with_all = ["target", "paths"])] pub prefix: Option, #[command(flatten)] pub compat_args: compat::PipGlobalCompatArgs, } #[derive(Args)] pub struct PipListArgs { /// Only include editable projects. #[arg(short, long)] pub editable: bool, /// Exclude any editable packages from output. #[arg(long, conflicts_with = "editable")] pub exclude_editable: bool, /// Exclude the specified package(s) from the output. #[arg(long)] pub r#exclude: Vec, /// Select the output format. #[arg(long, value_enum, default_value_t = ListFormat::default())] pub format: ListFormat, /// List outdated packages. /// /// The latest version of each package will be shown alongside the installed version. Up-to-date /// packages will be omitted from the output. #[arg(long, overrides_with("no_outdated"))] pub outdated: bool, #[arg(long, overrides_with("outdated"), hide = true)] pub no_outdated: bool, /// Validate the Python environment, to detect packages with missing dependencies and other /// issues. #[arg(long, overrides_with("no_strict"))] pub strict: bool, #[arg(long, overrides_with("strict"), hide = true)] pub no_strict: bool, #[command(flatten)] pub fetch: FetchArgs, /// The Python interpreter for which packages should be listed. /// /// By default, uv lists packages in a virtual environment but will show packages in a system /// Python environment if no virtual environment is found. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// List packages in the system Python environment. /// /// Disables discovery of virtual environments. /// /// See `uv help python` for details on Python discovery. #[arg( long, env = EnvVars::UV_SYSTEM_PYTHON, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_system") )] pub system: bool, #[arg(long, overrides_with("system"), hide = true)] pub no_system: bool, /// List packages from the specified `--target` directory. #[arg(long, conflicts_with = "prefix")] pub target: Option, /// List packages from the specified `--prefix` directory. #[arg(long, conflicts_with = "target")] pub prefix: Option, #[command(flatten)] pub compat_args: compat::PipListCompatArgs, } #[derive(Args)] pub struct PipCheckArgs { /// The Python interpreter for which packages should be checked. /// /// By default, uv checks packages in a virtual environment but will check packages in a system /// Python environment if no virtual environment is found. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// Check packages in the system Python environment. /// /// Disables discovery of virtual environments. /// /// See `uv help python` for details on Python discovery. #[arg( long, env = EnvVars::UV_SYSTEM_PYTHON, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_system") )] pub system: bool, #[arg(long, overrides_with("system"), hide = true)] pub no_system: bool, /// The Python version against which packages should be checked. /// /// By default, the installed packages are checked against the version of the current /// interpreter. #[arg(long)] pub python_version: Option, /// The platform for which packages should be checked. /// /// By default, the installed packages are checked against the platform of the current /// interpreter. /// /// Represented as a "target triple", a string that describes the target platform in terms of /// its CPU, vendor, and operating system name, like `x86_64-unknown-linux-gnu` or /// `aarch64-apple-darwin`. /// /// When targeting macOS (Darwin), the default minimum version is `13.0`. Use /// `MACOSX_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting iOS, the default minimum version is `13.0`. Use /// `IPHONEOS_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting Android, the default minimum Android API level is `24`. Use /// `ANDROID_API_LEVEL` to specify a different minimum version, e.g., `26`. #[arg(long)] pub python_platform: Option, } #[derive(Args)] pub struct PipShowArgs { /// The package(s) to display. pub package: Vec, /// Validate the Python environment, to detect packages with missing dependencies and other /// issues. #[arg(long, overrides_with("no_strict"))] pub strict: bool, #[arg(long, overrides_with("strict"), hide = true)] pub no_strict: bool, /// Show the full list of installed files for each package. #[arg(short, long)] pub files: bool, /// The Python interpreter to find the package in. /// /// By default, uv looks for packages in a virtual environment but will look for packages in a /// system Python environment if no virtual environment is found. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// Show a package in the system Python environment. /// /// Disables discovery of virtual environments. /// /// See `uv help python` for details on Python discovery. #[arg( long, env = EnvVars::UV_SYSTEM_PYTHON, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_system") )] pub system: bool, #[arg(long, overrides_with("system"), hide = true)] pub no_system: bool, /// Show a package from the specified `--target` directory. #[arg(long, conflicts_with = "prefix")] pub target: Option, /// Show a package from the specified `--prefix` directory. #[arg(long, conflicts_with = "target")] pub prefix: Option, #[command(flatten)] pub compat_args: compat::PipGlobalCompatArgs, } #[derive(Args)] pub struct PipTreeArgs { /// Show the version constraint(s) imposed on each package. #[arg(long)] pub show_version_specifiers: bool, #[command(flatten)] pub tree: DisplayTreeArgs, /// Validate the Python environment, to detect packages with missing dependencies and other /// issues. #[arg(long, overrides_with("no_strict"))] pub strict: bool, #[arg(long, overrides_with("strict"), hide = true)] pub no_strict: bool, #[command(flatten)] pub fetch: FetchArgs, /// The Python interpreter for which packages should be listed. /// /// By default, uv lists packages in a virtual environment but will show packages in a system /// Python environment if no virtual environment is found. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// List packages in the system Python environment. /// /// Disables discovery of virtual environments. /// /// See `uv help python` for details on Python discovery. #[arg( long, env = EnvVars::UV_SYSTEM_PYTHON, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_system") )] pub system: bool, #[arg(long, overrides_with("system"), hide = true)] pub no_system: bool, #[command(flatten)] pub compat_args: compat::PipGlobalCompatArgs, } #[derive(Args)] pub struct PipDebugArgs { #[arg(long, hide = true)] pub platform: Option, #[arg(long, hide = true)] pub python_version: Option, #[arg(long, hide = true)] pub implementation: Option, #[arg(long, hide = true)] pub abi: Option, } #[derive(Args)] pub struct BuildArgs { /// The directory from which distributions should be built, or a source /// distribution archive to build into a wheel. /// /// Defaults to the current working directory. #[arg(value_parser = parse_file_path)] pub src: Option, /// Build a specific package in the workspace. /// /// The workspace will be discovered from the provided source directory, or the current /// directory if no source directory is provided. /// /// If the workspace member does not exist, uv will exit with an error. #[arg(long, conflicts_with("all_packages"))] pub package: Option, /// Builds all packages in the workspace. /// /// The workspace will be discovered from the provided source directory, or the current /// directory if no source directory is provided. /// /// If the workspace member does not exist, uv will exit with an error. #[arg(long, alias = "all", conflicts_with("package"))] pub all_packages: bool, /// The output directory to which distributions should be written. /// /// Defaults to the `dist` subdirectory within the source directory, or the /// directory containing the source distribution archive. #[arg(long, short, value_parser = parse_file_path)] pub out_dir: Option, /// Build a source distribution ("sdist") from the given directory. #[arg(long)] pub sdist: bool, /// Build a binary distribution ("wheel") from the given directory. #[arg(long)] pub wheel: bool, /// When using the uv build backend, list the files that would be included when building. /// /// Skips building the actual distribution, except when the source distribution is needed to /// build the wheel. The file list is collected directly without a PEP 517 environment. It only /// works with the uv build backend, there is no PEP 517 file list build hook. /// /// This option can be combined with `--sdist` and `--wheel` for inspecting different build /// paths. // Hidden while in preview. #[arg(long, hide = true)] pub list: bool, #[arg(long, overrides_with("no_build_logs"), hide = true)] pub build_logs: bool, /// Hide logs from the build backend. #[arg(long, overrides_with("build_logs"))] pub no_build_logs: bool, /// Always build through PEP 517, don't use the fast path for the uv build backend. /// /// By default, uv won't create a PEP 517 build environment for packages using the uv build /// backend, but use a fast path that calls into the build backend directly. This option forces /// always using PEP 517. #[arg(long, conflicts_with = "list")] pub force_pep517: bool, /// Clear the output directory before the build, removing stale artifacts. #[arg(long)] pub clear: bool, #[arg(long, overrides_with("no_create_gitignore"), hide = true)] pub create_gitignore: bool, /// Do not create a `.gitignore` file in the output directory. /// /// By default, uv creates a `.gitignore` file in the output directory to exclude build /// artifacts from version control. When this flag is used, the file will be omitted. #[arg(long, overrides_with("create_gitignore"))] pub no_create_gitignore: bool, /// Constrain build dependencies using the given requirements files when building distributions. /// /// Constraints files are `requirements.txt`-like files that only control the _version_ of a /// build dependency that's installed. However, including a package in a constraints file will /// _not_ trigger the inclusion of that package on its own. #[arg(long, short, alias = "build-constraint", env = EnvVars::UV_BUILD_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub build_constraints: Vec>, /// Require a matching hash for each requirement. /// /// By default, uv will verify any available hashes in the requirements file, but will not /// require that all requirements have an associated hash. /// /// When `--require-hashes` is enabled, _all_ requirements must include a hash or set of hashes, /// and _all_ requirements must either be pinned to exact versions (e.g., `==1.0.0`), or be /// specified via direct URL. /// /// Hash-checking mode introduces a number of additional constraints: /// /// - Git dependencies are not supported. /// - Editable installations are not supported. /// - Local dependencies are not supported, unless they point to a specific wheel (`.whl`) or /// source archive (`.zip`, `.tar.gz`), as opposed to a directory. #[arg( long, env = EnvVars::UV_REQUIRE_HASHES, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_require_hashes"), )] pub require_hashes: bool, #[arg(long, overrides_with("require_hashes"), hide = true)] pub no_require_hashes: bool, #[arg(long, overrides_with("no_verify_hashes"), hide = true)] pub verify_hashes: bool, /// Disable validation of hashes in the requirements file. /// /// By default, uv will verify any available hashes in the requirements file, but will not /// require that all requirements have an associated hash. To enforce hash validation, use /// `--require-hashes`. #[arg( long, env = EnvVars::UV_NO_VERIFY_HASHES, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("verify_hashes"), )] pub no_verify_hashes: bool, /// The Python interpreter to use for the build environment. /// /// By default, builds are executed in isolated virtual environments. The discovered interpreter /// will be used to create those environments, and will be symlinked or copied in depending on /// the platform. /// /// See `uv help python` to view supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, #[command(flatten)] pub resolver: ResolverArgs, #[command(flatten)] pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, } #[derive(Args)] pub struct VenvArgs { /// The Python interpreter to use for the virtual environment. /// /// During virtual environment creation, uv will not look for Python interpreters in virtual /// environments. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// Ignore virtual environments when searching for the Python interpreter. /// /// This is the default behavior and has no effect. #[arg( long, env = EnvVars::UV_SYSTEM_PYTHON, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_system"), hide = true, )] pub system: bool, /// This flag is included for compatibility only, it has no effect. /// /// uv will never search for interpreters in virtual environments when creating a virtual /// environment. #[arg(long, overrides_with("system"), hide = true)] pub no_system: bool, /// Avoid discovering a project or workspace. /// /// By default, uv searches for projects in the current directory or any parent directory to /// determine the default path of the virtual environment and check for Python version /// constraints, if any. #[arg(long, alias = "no-workspace")] pub no_project: bool, /// Install seed packages (one or more of: `pip`, `setuptools`, and `wheel`) into the virtual environment. /// /// Note that `setuptools` and `wheel` are not included in Python 3.12+ environments. #[arg(long, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_VENV_SEED)] pub seed: bool, /// Remove any existing files or directories at the target path. /// /// By default, `uv venv` will exit with an error if the given path is non-empty. The /// `--clear` option will instead clear a non-empty path before creating a new virtual /// environment. #[clap(long, short, overrides_with = "allow_existing", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_VENV_CLEAR)] pub clear: bool, /// Fail without prompting if any existing files or directories are present at the target path. /// /// By default, when a TTY is available, `uv venv` will prompt to clear a non-empty directory. /// When `--no-clear` is used, the command will exit with an error instead of prompting. #[clap( long, overrides_with = "clear", conflicts_with = "allow_existing", hide = true )] pub no_clear: bool, /// Preserve any existing files or directories at the target path. /// /// By default, `uv venv` will exit with an error if the given path is non-empty. The /// `--allow-existing` option will instead write to the given path, regardless of its contents, /// and without clearing it beforehand. /// /// WARNING: This option can lead to unexpected behavior if the existing virtual environment and /// the newly-created virtual environment are linked to different Python interpreters. #[clap(long, overrides_with = "clear")] pub allow_existing: bool, /// The path to the virtual environment to create. /// /// Default to `.venv` in the working directory. /// /// Relative paths are resolved relative to the working directory. pub path: Option, /// Provide an alternative prompt prefix for the virtual environment. /// /// By default, the prompt is dependent on whether a path was provided to `uv venv`. If provided /// (e.g, `uv venv project`), the prompt is set to the directory name. If not provided /// (`uv venv`), the prompt is set to the current directory's name. /// /// If "." is provided, the current directory name will be used regardless of whether a path was /// provided to `uv venv`. #[arg(long, verbatim_doc_comment)] pub prompt: Option, /// Give the virtual environment access to the system site packages directory. /// /// Unlike `pip`, when a virtual environment is created with `--system-site-packages`, uv will /// _not_ take system site packages into account when running commands like `uv pip list` or `uv /// pip install`. The `--system-site-packages` flag will provide the virtual environment with /// access to the system site packages directory at runtime, but will not affect the behavior of /// uv commands. #[arg(long)] pub system_site_packages: bool, /// Make the virtual environment relocatable. /// /// A relocatable virtual environment can be moved around and redistributed without invalidating /// its associated entrypoint and activation scripts. /// /// Note that this can only be guaranteed for standard `console_scripts` and `gui_scripts`. /// Other scripts may be adjusted if they ship with a generic `#!python[w]` shebang, and /// binaries are left as-is. /// /// As a result of making the environment relocatable (by way of writing relative, rather than /// absolute paths), the entrypoints and scripts themselves will _not_ be relocatable. In other /// words, copying those entrypoints and scripts to a location outside the environment will not /// work, as they reference paths relative to the environment itself. #[arg(long)] pub relocatable: bool, #[command(flatten)] pub index_args: IndexArgs, /// The strategy to use when resolving against multiple index URLs. /// /// By default, uv will stop at the first index on which a given package is available, and /// limit resolutions to those present on that first index (`first-index`). This prevents /// "dependency confusion" attacks, whereby an attacker can upload a malicious package under the /// same name to an alternate index. #[arg(long, value_enum, env = EnvVars::UV_INDEX_STRATEGY)] pub index_strategy: Option, /// Attempt to use `keyring` for authentication for index URLs. /// /// At present, only `--keyring-provider subprocess` is supported, which configures uv to use /// the `keyring` CLI to handle authentication. /// /// Defaults to `disabled`. #[arg(long, value_enum, env = EnvVars::UV_KEYRING_PROVIDER)] pub keyring_provider: Option, /// Limit candidate packages to those that were uploaded prior to the given date. /// /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format /// (e.g., `2006-12-02`) resolved based on your system's configured time zone, a "friendly" /// duration (e.g., `24 hours`, `1 week`, `30 days`), or an ISO 8601 duration (e.g., `PT24H`, /// `P7D`, `P30D`). /// /// Durations do not respect semantics of the local time zone and are always resolved to a fixed /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). /// Calendar units such as months and years are not allowed. #[arg(long, env = EnvVars::UV_EXCLUDE_NEWER)] pub exclude_newer: Option, /// Limit candidate packages for a specific package to those that were uploaded prior to the /// given date. /// /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 /// timestamp (e.g., `2006-12-02T02:07:43Z`), a local date in the same format (e.g., /// `2006-12-02`) resolved based on your system's configured time zone, a "friendly" duration /// (e.g., `24 hours`, `1 week`, `30 days`), or a ISO 8601 duration (e.g., `PT24H`, `P7D`, /// `P30D`). /// /// Durations do not respect semantics of the local time zone and are always resolved to a fixed /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). /// Calendar units such as months and years are not allowed. /// /// Can be provided multiple times for different packages. #[arg(long)] pub exclude_newer_package: Option>, /// The method to use when installing packages from the global cache. /// /// This option is only used for installing seed packages. /// /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and /// Windows. /// /// WARNING: The use of symlink link mode is discouraged, as they create tight coupling between /// the cache and the target environment. For example, clearing the cache (`uv cache clean`) /// will break all installed packages by way of removing the underlying source files. Use /// symlinks with caution. #[arg(long, value_enum, env = EnvVars::UV_LINK_MODE)] pub link_mode: Option, #[command(flatten)] pub refresh: RefreshArgs, #[command(flatten)] pub compat_args: compat::VenvCompatArgs, } #[derive(Parser, Debug, Clone)] pub enum ExternalCommand { #[command(external_subcommand)] Cmd(Vec), } impl Deref for ExternalCommand { type Target = Vec; fn deref(&self) -> &Self::Target { match self { Self::Cmd(cmd) => cmd, } } } impl DerefMut for ExternalCommand { fn deref_mut(&mut self) -> &mut Self::Target { match self { Self::Cmd(cmd) => cmd, } } } impl ExternalCommand { pub fn split(&self) -> (Option<&OsString>, &[OsString]) { match self.as_slice() { [] => (None, &[]), [cmd, args @ ..] => (Some(cmd), args), } } } #[derive(Debug, Default, Copy, Clone, clap::ValueEnum)] pub enum AuthorFrom { /// Fetch the author information from some sources (e.g., Git) automatically. #[default] Auto, /// Fetch the author information from Git configuration only. Git, /// Do not infer the author information. None, } #[derive(Args)] pub struct InitArgs { /// The path to use for the project/script. /// /// Defaults to the current working directory when initializing an app or library; required when /// initializing a script. Accepts relative and absolute paths. /// /// If a `pyproject.toml` is found in any of the parent directories of the target path, the /// project will be added as a workspace member of the parent, unless `--no-workspace` is /// provided. #[arg(required_if_eq("script", "true"))] pub path: Option, /// The name of the project. /// /// Defaults to the name of the directory. #[arg(long, conflicts_with = "script")] pub name: Option, /// Only create a `pyproject.toml`. /// /// Disables creating extra files like `README.md`, the `src/` tree, `.python-version` files, /// etc. #[arg(long, conflicts_with = "script")] pub bare: bool, /// Create a virtual project, rather than a package. /// /// This option is deprecated and will be removed in a future release. #[arg(long, hide = true, conflicts_with = "package")] pub r#virtual: bool, /// Set up the project to be built as a Python package. /// /// Defines a `[build-system]` for the project. /// /// This is the default behavior when using `--lib` or `--build-backend`. /// /// When using `--app`, this will include a `[project.scripts]` entrypoint and use a `src/` /// project structure. #[arg(long, overrides_with = "no_package")] pub r#package: bool, /// Do not set up the project to be built as a Python package. /// /// Does not include a `[build-system]` for the project. /// /// This is the default behavior when using `--app`. #[arg(long, overrides_with = "package", conflicts_with_all = ["lib", "build_backend"])] pub r#no_package: bool, /// Create a project for an application. /// /// This is the default behavior if `--lib` is not requested. /// /// This project kind is for web servers, scripts, and command-line interfaces. /// /// By default, an application is not intended to be built and distributed as a Python package. /// The `--package` option can be used to create an application that is distributable, e.g., if /// you want to distribute a command-line interface via PyPI. #[arg(long, alias = "application", conflicts_with_all = ["lib", "script"])] pub r#app: bool, /// Create a project for a library. /// /// A library is a project that is intended to be built and distributed as a Python package. #[arg(long, alias = "library", conflicts_with_all=["app", "script"])] pub r#lib: bool, /// Create a script. /// /// A script is a standalone file with embedded metadata enumerating its dependencies, along /// with any Python version requirements, as defined in the PEP 723 specification. /// /// PEP 723 scripts can be executed directly with `uv run`. /// /// By default, adds a requirement on the system Python version; use `--python` to specify an /// alternative Python version requirement. #[arg(long, conflicts_with_all=["app", "lib", "package", "build_backend", "description"])] pub r#script: bool, /// Set the project description. #[arg(long, conflicts_with = "script", overrides_with = "no_description")] pub description: Option, /// Disable the description for the project. #[arg(long, conflicts_with = "script", overrides_with = "description")] pub no_description: bool, /// Initialize a version control system for the project. /// /// By default, uv will initialize a Git repository (`git`). Use `--vcs none` to explicitly /// avoid initializing a version control system. #[arg(long, value_enum, conflicts_with = "script")] pub vcs: Option, /// Initialize a build-backend of choice for the project. /// /// Implicitly sets `--package`. #[arg(long, value_enum, conflicts_with_all=["script", "no_package"], env = EnvVars::UV_INIT_BUILD_BACKEND)] pub build_backend: Option, /// Invalid option name for build backend. #[arg( long, required(false), action(clap::ArgAction::SetTrue), value_parser=clap::builder::UnknownArgumentValueParser::suggest_arg("--build-backend"), hide(true) )] backend: Option, /// Do not create a `README.md` file. #[arg(long)] pub no_readme: bool, /// Fill in the `authors` field in the `pyproject.toml`. /// /// By default, uv will attempt to infer the author information from some sources (e.g., Git) /// (`auto`). Use `--author-from git` to only infer from Git configuration. Use `--author-from /// none` to avoid inferring the author information. #[arg(long, value_enum)] pub author_from: Option, /// Do not create a `.python-version` file for the project. /// /// By default, uv will create a `.python-version` file containing the minor version of the /// discovered Python interpreter, which will cause subsequent uv commands to use that version. #[arg(long)] pub no_pin_python: bool, /// Create a `.python-version` file for the project. /// /// This is the default. #[arg(long, hide = true)] pub pin_python: bool, /// Avoid discovering a workspace and create a standalone project. /// /// By default, uv searches for workspaces in the current directory or any parent directory. #[arg(long, alias = "no-project")] pub no_workspace: bool, /// The Python interpreter to use to determine the minimum supported Python version. /// /// See `uv help python` to view supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, } #[derive(Args)] pub struct RunArgs { /// Include optional dependencies from the specified extra name. /// /// May be provided more than once. /// /// Optional dependencies are defined via `project.optional-dependencies` in a `pyproject.toml`. /// /// This option is only available when running in a project. #[arg(long, conflicts_with = "all_extras", conflicts_with = "only_group", value_parser = extra_name_with_clap_error)] pub extra: Option>, /// Include all optional dependencies. /// /// Optional dependencies are defined via `project.optional-dependencies` in a `pyproject.toml`. /// /// This option is only available when running in a project. #[arg(long, conflicts_with = "extra", conflicts_with = "only_group")] pub all_extras: bool, /// Exclude the specified optional dependencies, if `--all-extras` is supplied. /// /// May be provided multiple times. #[arg(long)] pub no_extra: Vec, #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, /// Include the development dependency group. /// /// Development dependencies are defined via `dependency-groups.dev` or /// `tool.uv.dev-dependencies` in a `pyproject.toml`. /// /// This option is an alias for `--group dev`. /// /// This option is only available when running in a project. #[arg(long, overrides_with("no_dev"), hide = true, env = EnvVars::UV_DEV, value_parser = clap::builder::BoolishValueParser::new())] pub dev: bool, /// Disable the development dependency group. /// /// This option is an alias of `--no-group dev`. /// See `--no-default-groups` to disable all default groups instead. /// /// This option is only available when running in a project. #[arg(long, overrides_with("dev"), env = EnvVars::UV_NO_DEV, value_parser = clap::builder::BoolishValueParser::new())] pub no_dev: bool, /// Include dependencies from the specified dependency group. /// /// May be provided multiple times. #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub group: Vec, /// Disable the specified dependency group. /// /// This option always takes precedence over default groups, /// `--all-groups`, and `--group`. /// /// May be provided multiple times. #[arg(long, env = EnvVars::UV_NO_GROUP, value_delimiter = ' ')] pub no_group: Vec, /// Ignore the default dependency groups. /// /// uv includes the groups defined in `tool.uv.default-groups` by default. /// This disables that option, however, specific groups can still be included with `--group`. #[arg(long, env = EnvVars::UV_NO_DEFAULT_GROUPS)] pub no_default_groups: bool, /// Only include dependencies from the specified dependency group. /// /// The project and its dependencies will be omitted. /// /// May be provided multiple times. Implies `--no-default-groups`. #[arg(long, conflicts_with_all = ["group", "dev", "all_groups"])] pub only_group: Vec, /// Include dependencies from all dependency groups. /// /// `--no-group` can be used to exclude specific groups. #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub all_groups: bool, /// Run a Python module. /// /// Equivalent to `python -m `. #[arg(short, long, conflicts_with_all = ["script", "gui_script"])] pub module: bool, /// Only include the development dependency group. /// /// The project and its dependencies will be omitted. /// /// This option is an alias for `--only-group dev`. Implies `--no-default-groups`. #[arg(long, conflicts_with_all = ["group", "all_groups", "no_dev"])] pub only_dev: bool, /// Install any non-editable dependencies, including the project and any workspace members, as /// editable. #[arg(long, overrides_with = "no_editable", hide = true)] pub editable: bool, /// Install any editable dependencies, including the project and any workspace members, as /// non-editable. #[arg(long, overrides_with = "editable", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_EDITABLE)] pub no_editable: bool, /// Do not remove extraneous packages present in the environment. #[arg(long, overrides_with("exact"), alias = "no-exact", hide = true)] pub inexact: bool, /// Perform an exact sync, removing extraneous packages. /// /// When enabled, uv will remove any extraneous packages from the environment. By default, `uv /// run` will make the minimum necessary changes to satisfy the requirements. #[arg(long, overrides_with("inexact"))] pub exact: bool, /// Load environment variables from a `.env` file. /// /// Can be provided multiple times, with subsequent files overriding values defined in previous /// files. #[arg(long, env = EnvVars::UV_ENV_FILE)] pub env_file: Vec, /// Avoid reading environment variables from a `.env` file. #[arg(long, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_ENV_FILE)] pub no_env_file: bool, /// The command to run. /// /// If the path to a Python script (i.e., ending in `.py`), it will be /// executed with the Python interpreter. #[command(subcommand)] pub command: Option, /// Run with the given packages installed. /// /// When used in a project, these dependencies will be layered on top of the project environment /// in a separate, ephemeral environment. These dependencies are allowed to conflict with those /// specified by the project. #[arg(short = 'w', long)] pub with: Vec, /// Run with the given packages installed in editable mode. /// /// When used in a project, these dependencies will be layered on top of the project environment /// in a separate, ephemeral environment. These dependencies are allowed to conflict with those /// specified by the project. #[arg(long)] pub with_editable: Vec, /// Run with the packages listed in the given files. /// /// The following formats are supported: `requirements.txt`, `.py` files with inline metadata, /// and `pylock.toml`. /// /// The same environment semantics as `--with` apply. /// /// Using `pyproject.toml`, `setup.py`, or `setup.cfg` files is not allowed. #[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)] pub with_requirements: Vec>, /// Run the command in an isolated virtual environment. /// /// Usually, the project environment is reused for performance. This option forces a fresh /// environment to be used for the project, enforcing strict isolation between dependencies and /// declaration of requirements. /// /// An editable installation is still used for the project. /// /// When used with `--with` or `--with-requirements`, the additional dependencies will still be /// layered in a second environment. #[arg(long, env = EnvVars::UV_ISOLATED, value_parser = clap::builder::BoolishValueParser::new())] pub isolated: bool, /// Prefer the active virtual environment over the project's virtual environment. /// /// If the project virtual environment is active or no virtual environment is active, this has /// no effect. #[arg(long, overrides_with = "no_active")] pub active: bool, /// Prefer project's virtual environment over an active environment. /// /// This is the default behavior. #[arg(long, overrides_with = "active", hide = true)] pub no_active: bool, /// Avoid syncing the virtual environment. /// /// Implies `--frozen`, as the project dependencies will be ignored (i.e., the lockfile will not /// be updated, since the environment will not be synced regardless). #[arg(long, env = EnvVars::UV_NO_SYNC, value_parser = clap::builder::BoolishValueParser::new())] pub no_sync: bool, /// Assert that the `uv.lock` will remain unchanged. /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or /// needs to be updated, uv will exit with an error. #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, /// Run without updating the `uv.lock` file. /// /// Instead of checking if the lockfile is up-to-date, uses the versions in the lockfile as the /// source of truth. If the lockfile is missing, uv will exit with an error. If the /// `pyproject.toml` includes changes to dependencies that have not been included in the /// lockfile yet, they will not be present in the environment. #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, /// Run the given path as a Python script. /// /// Using `--script` will attempt to parse the path as a PEP 723 script, /// irrespective of its extension. #[arg(long, short, conflicts_with_all = ["module", "gui_script"])] pub script: bool, /// Run the given path as a Python GUI script. /// /// Using `--gui-script` will attempt to parse the path as a PEP 723 script and run it with /// `pythonw.exe`, irrespective of its extension. Only available on Windows. #[arg(long, conflicts_with_all = ["script", "module"])] pub gui_script: bool, #[command(flatten)] pub installer: ResolverInstallerArgs, #[command(flatten)] pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, /// Run the command with all workspace members installed. /// /// The workspace's environment (`.venv`) is updated to include all workspace members. /// /// Any extras or groups specified via `--extra`, `--group`, or related options will be applied /// to all workspace members. #[arg(long, conflicts_with = "package")] pub all_packages: bool, /// Run the command in a specific package in the workspace. /// /// If the workspace member does not exist, uv will exit with an error. #[arg(long, conflicts_with = "all_packages")] pub package: Option, /// Avoid discovering the project or workspace. /// /// Instead of searching for projects in the current directory and parent directories, run in an /// isolated, ephemeral environment populated by the `--with` requirements. /// /// If a virtual environment is active or found in a current or parent directory, it will be /// used as if there was no project or workspace. #[arg(long, alias = "no_workspace", conflicts_with = "package")] pub no_project: bool, /// The Python interpreter to use for the run environment. /// /// If the interpreter request is satisfied by a discovered environment, the environment will be /// used. /// /// See `uv help python` to view supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// Whether to show resolver and installer output from any environment modifications. /// /// By default, environment modifications are omitted, but enabled under `--verbose`. #[arg(long, env = EnvVars::UV_SHOW_RESOLUTION, value_parser = clap::builder::BoolishValueParser::new(), hide = true)] pub show_resolution: bool, /// Number of times that `uv run` will allow recursive invocations. /// /// The current recursion depth is tracked by environment variable. If environment variables are /// cleared, uv will fail to detect the recursion depth. /// /// If uv reaches the maximum recursion depth, it will exit with an error. #[arg(long, hide = true, env = EnvVars::UV_RUN_MAX_RECURSION_DEPTH)] pub max_recursion_depth: Option, /// The platform for which requirements should be installed. /// /// Represented as a "target triple", a string that describes the target platform in terms of /// its CPU, vendor, and operating system name, like `x86_64-unknown-linux-gnu` or /// `aarch64-apple-darwin`. /// /// When targeting macOS (Darwin), the default minimum version is `13.0`. Use /// `MACOSX_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting iOS, the default minimum version is `13.0`. Use /// `IPHONEOS_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting Android, the default minimum Android API level is `24`. Use /// `ANDROID_API_LEVEL` to specify a different minimum version, e.g., `26`. /// /// WARNING: When specified, uv will select wheels that are compatible with the _target_ /// platform; as a result, the installed distributions may not be compatible with the _current_ /// platform. Conversely, any distributions that are built from source may be incompatible with /// the _target_ platform, as they will be built for the _current_ platform. The /// `--python-platform` option is intended for advanced use cases. #[arg(long)] pub python_platform: Option, } #[derive(Args)] pub struct SyncArgs { /// Include optional dependencies from the specified extra name. /// /// May be provided more than once. /// /// When multiple extras or groups are specified that appear in `tool.uv.conflicts`, uv will /// report an error. /// /// Note that all optional dependencies are always included in the resolution; this option only /// affects the selection of packages to install. #[arg(long, conflicts_with = "all_extras", conflicts_with = "only_group", value_parser = extra_name_with_clap_error)] pub extra: Option>, /// Select the output format. #[arg(long, value_enum, default_value_t = SyncFormat::default())] pub output_format: SyncFormat, /// Include all optional dependencies. /// /// When two or more extras are declared as conflicting in `tool.uv.conflicts`, using this flag /// will always result in an error. /// /// Note that all optional dependencies are always included in the resolution; this option only /// affects the selection of packages to install. #[arg(long, conflicts_with = "extra", conflicts_with = "only_group")] pub all_extras: bool, /// Exclude the specified optional dependencies, if `--all-extras` is supplied. /// /// May be provided multiple times. #[arg(long)] pub no_extra: Vec, #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, /// Include the development dependency group. /// /// This option is an alias for `--group dev`. #[arg(long, overrides_with("no_dev"), hide = true, env = EnvVars::UV_DEV, value_parser = clap::builder::BoolishValueParser::new())] pub dev: bool, /// Disable the development dependency group. /// /// This option is an alias of `--no-group dev`. /// See `--no-default-groups` to disable all default groups instead. #[arg(long, overrides_with("dev"), env = EnvVars::UV_NO_DEV, value_parser = clap::builder::BoolishValueParser::new())] pub no_dev: bool, /// Only include the development dependency group. /// /// The project and its dependencies will be omitted. /// /// This option is an alias for `--only-group dev`. Implies `--no-default-groups`. #[arg(long, conflicts_with_all = ["group", "all_groups", "no_dev"])] pub only_dev: bool, /// Include dependencies from the specified dependency group. /// /// When multiple extras or groups are specified that appear in /// `tool.uv.conflicts`, uv will report an error. /// /// May be provided multiple times. #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub group: Vec, /// Disable the specified dependency group. /// /// This option always takes precedence over default groups, /// `--all-groups`, and `--group`. /// /// May be provided multiple times. #[arg(long, env = EnvVars::UV_NO_GROUP, value_delimiter = ' ')] pub no_group: Vec, /// Ignore the default dependency groups. /// /// uv includes the groups defined in `tool.uv.default-groups` by default. /// This disables that option, however, specific groups can still be included with `--group`. #[arg(long, env = EnvVars::UV_NO_DEFAULT_GROUPS)] pub no_default_groups: bool, /// Only include dependencies from the specified dependency group. /// /// The project and its dependencies will be omitted. /// /// May be provided multiple times. Implies `--no-default-groups`. #[arg(long, conflicts_with_all = ["group", "dev", "all_groups"])] pub only_group: Vec, /// Include dependencies from all dependency groups. /// /// `--no-group` can be used to exclude specific groups. #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub all_groups: bool, /// Install any non-editable dependencies, including the project and any workspace members, as /// editable. #[arg(long, overrides_with = "no_editable", hide = true)] pub editable: bool, /// Install any editable dependencies, including the project and any workspace members, as /// non-editable. #[arg(long, overrides_with = "editable", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_EDITABLE)] pub no_editable: bool, /// Do not remove extraneous packages present in the environment. /// /// When enabled, uv will make the minimum necessary changes to satisfy the requirements. /// By default, syncing will remove any extraneous packages from the environment #[arg(long, overrides_with("exact"), alias = "no-exact")] pub inexact: bool, /// Perform an exact sync, removing extraneous packages. #[arg(long, overrides_with("inexact"), hide = true)] pub exact: bool, /// Sync dependencies to the active virtual environment. /// /// Instead of creating or updating the virtual environment for the project or script, the /// active virtual environment will be preferred, if the `VIRTUAL_ENV` environment variable is /// set. #[arg(long, overrides_with = "no_active")] pub active: bool, /// Prefer project's virtual environment over an active environment. /// /// This is the default behavior. #[arg(long, overrides_with = "active", hide = true)] pub no_active: bool, /// Do not install the current project. /// /// By default, the current project is installed into the environment with all of its /// dependencies. The `--no-install-project` option allows the project to be excluded, but all /// of its dependencies are still installed. This is particularly useful in situations like /// building Docker images where installing the project separately from its dependencies allows /// optimal layer caching. /// /// The inverse `--only-install-project` can be used to install _only_ the project itself, /// excluding all dependencies. #[arg(long, conflicts_with = "only_install_project")] pub no_install_project: bool, /// Only install the current project. #[arg(long, conflicts_with = "no_install_project", hide = true)] pub only_install_project: bool, /// Do not install any workspace members, including the root project. /// /// By default, all workspace members and their dependencies are installed into the /// environment. The `--no-install-workspace` option allows exclusion of all the workspace /// members while retaining their dependencies. This is particularly useful in situations like /// building Docker images where installing the workspace separately from its dependencies /// allows optimal layer caching. /// /// The inverse `--only-install-workspace` can be used to install _only_ workspace members, /// excluding all other dependencies. #[arg(long, conflicts_with = "only_install_workspace")] pub no_install_workspace: bool, /// Only install workspace members, including the root project. #[arg(long, conflicts_with = "no_install_workspace", hide = true)] pub only_install_workspace: bool, /// Do not install local path dependencies /// /// Skips the current project, workspace members, and any other local (path or editable) /// packages. Only remote/indexed dependencies are installed. Useful in Docker builds to cache /// heavy third-party dependencies first and layer local packages separately. /// /// The inverse `--only-install-local` can be used to install _only_ local packages, excluding /// all remote dependencies. #[arg(long, conflicts_with = "only_install_local")] pub no_install_local: bool, /// Only install local path dependencies #[arg(long, conflicts_with = "no_install_local", hide = true)] pub only_install_local: bool, /// Do not install the given package(s). /// /// By default, all of the project's dependencies are installed into the environment. The /// `--no-install-package` option allows exclusion of specific packages. Note this can result /// in a broken environment, and should be used with caution. /// /// The inverse `--only-install-package` can be used to install _only_ the specified packages, /// excluding all others. #[arg(long, conflicts_with = "only_install_package")] pub no_install_package: Vec, /// Only install the given package(s). #[arg(long, conflicts_with = "no_install_package", hide = true)] pub only_install_package: Vec, /// Assert that the `uv.lock` will remain unchanged. /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, /// uv will exit with an error. #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, /// Sync without updating the `uv.lock` file. /// /// Instead of checking if the lockfile is up-to-date, uses the versions in the lockfile as the /// source of truth. If the lockfile is missing, uv will exit with an error. If the /// `pyproject.toml` includes changes to dependencies that have not been included in the /// lockfile yet, they will not be present in the environment. #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, /// Perform a dry run, without writing the lockfile or modifying the project environment. /// /// In dry-run mode, uv will resolve the project's dependencies and report on the resulting /// changes to both the lockfile and the project environment, but will not modify either. #[arg(long)] pub dry_run: bool, #[command(flatten)] pub installer: ResolverInstallerArgs, #[command(flatten)] pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, /// Sync all packages in the workspace. /// /// The workspace's environment (`.venv`) is updated to include all workspace members. /// /// Any extras or groups specified via `--extra`, `--group`, or related options will be applied /// to all workspace members. #[arg(long, conflicts_with = "package")] pub all_packages: bool, /// Sync for specific packages in the workspace. /// /// The workspace's environment (`.venv`) is updated to reflect the subset of dependencies /// declared by the specified workspace member packages. /// /// If any workspace member does not exist, uv will exit with an error. #[arg(long, conflicts_with = "all_packages")] pub package: Vec, /// Sync the environment for a Python script, rather than the current project. /// /// If provided, uv will sync the dependencies based on the script's inline metadata table, in /// adherence with PEP 723. #[arg( long, conflicts_with = "all_packages", conflicts_with = "package", conflicts_with = "no_install_project", conflicts_with = "no_install_workspace", conflicts_with = "no_install_local", conflicts_with = "extra", conflicts_with = "all_extras", conflicts_with = "no_extra", conflicts_with = "no_all_extras", conflicts_with = "dev", conflicts_with = "no_dev", conflicts_with = "only_dev", conflicts_with = "group", conflicts_with = "no_group", conflicts_with = "no_default_groups", conflicts_with = "only_group", conflicts_with = "all_groups" )] pub script: Option, /// The Python interpreter to use for the project environment. /// /// By default, the first interpreter that meets the project's `requires-python` constraint is /// used. /// /// If a Python interpreter in a virtual environment is provided, the packages will not be /// synced to the given environment. The interpreter will be used to create a virtual /// environment in the project. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// The platform for which requirements should be installed. /// /// Represented as a "target triple", a string that describes the target platform in terms of /// its CPU, vendor, and operating system name, like `x86_64-unknown-linux-gnu` or /// `aarch64-apple-darwin`. /// /// When targeting macOS (Darwin), the default minimum version is `13.0`. Use /// `MACOSX_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting iOS, the default minimum version is `13.0`. Use /// `IPHONEOS_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting Android, the default minimum Android API level is `24`. Use /// `ANDROID_API_LEVEL` to specify a different minimum version, e.g., `26`. /// /// WARNING: When specified, uv will select wheels that are compatible with the _target_ /// platform; as a result, the installed distributions may not be compatible with the _current_ /// platform. Conversely, any distributions that are built from source may be incompatible with /// the _target_ platform, as they will be built for the _current_ platform. The /// `--python-platform` option is intended for advanced use cases. #[arg(long)] pub python_platform: Option, /// Check if the Python environment is synchronized with the project. /// /// If the environment is not up to date, uv will exit with an error. #[arg(long, overrides_with("no_check"))] pub check: bool, #[arg(long, overrides_with("check"), hide = true)] pub no_check: bool, } #[derive(Args)] pub struct LockArgs { /// Check if the lockfile is up-to-date. /// /// Asserts that the `uv.lock` would remain unchanged after a resolution. If the lockfile is /// missing or needs to be updated, uv will exit with an error. /// /// Equivalent to `--locked`. #[arg(long, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["check_exists", "upgrade"], overrides_with = "check")] pub check: bool, /// Check if the lockfile is up-to-date. /// /// Asserts that the `uv.lock` would remain unchanged after a resolution. If the lockfile is /// missing or needs to be updated, uv will exit with an error. /// /// Equivalent to `--check`. #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["check_exists", "upgrade"], hide = true)] pub locked: bool, /// Assert that a `uv.lock` exists without checking if it is up-to-date. /// /// Equivalent to `--frozen`. #[arg(long, alias = "frozen", env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["check", "locked"])] pub check_exists: bool, /// Perform a dry run, without writing the lockfile. /// /// In dry-run mode, uv will resolve the project's dependencies and report on the resulting /// changes, but will not write the lockfile to disk. #[arg( long, conflicts_with = "check_exists", conflicts_with = "check", conflicts_with = "locked" )] pub dry_run: bool, /// Lock the specified Python script, rather than the current project. /// /// If provided, uv will lock the script (based on its inline metadata table, in adherence with /// PEP 723) to a `.lock` file adjacent to the script itself. #[arg(long)] pub script: Option, #[command(flatten)] pub resolver: ResolverArgs, #[command(flatten)] pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, /// The Python interpreter to use during resolution. /// /// A Python interpreter is required for building source distributions to determine package /// metadata when there are not wheels. /// /// The interpreter is also used as the fallback value for the minimum Python version if /// `requires-python` is not set. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, } #[derive(Args)] #[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))] pub struct AddArgs { /// The packages to add, as PEP 508 requirements (e.g., `ruff==0.5.0`). #[arg(group = "sources")] pub packages: Vec, /// Add the packages listed in the given files. /// /// The following formats are supported: `requirements.txt`, `.py` files with inline metadata, /// `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg`. #[arg(long, short, alias = "requirement", group = "sources", value_parser = parse_file_path)] pub requirements: Vec, /// Constrain versions using the given requirements files. /// /// Constraints files are `requirements.txt`-like files that only control the _version_ of a /// requirement that's installed. The constraints will _not_ be added to the project's /// `pyproject.toml` file, but _will_ be respected during dependency resolution. /// /// This is equivalent to pip's `--constraint` option. #[arg(long, short, alias = "constraint", env = EnvVars::UV_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub constraints: Vec>, /// Apply this marker to all added packages. #[arg(long, short, value_parser = MarkerTree::from_str)] pub marker: Option, /// Add the requirements to the development dependency group. /// /// This option is an alias for `--group dev`. #[arg( long, conflicts_with("optional"), conflicts_with("group"), conflicts_with("script"), env = EnvVars::UV_DEV, value_parser = clap::builder::BoolishValueParser::new() )] pub dev: bool, /// Add the requirements to the package's optional dependencies for the specified extra. /// /// The group may then be activated when installing the project with the `--extra` flag. /// /// To enable an optional extra for this requirement instead, see `--extra`. #[arg(long, conflicts_with("dev"), conflicts_with("group"))] pub optional: Option, /// Add the requirements to the specified dependency group. /// /// These requirements will not be included in the published metadata for the project. #[arg( long, conflicts_with("dev"), conflicts_with("optional"), conflicts_with("script") )] pub group: Option, /// Add the requirements as editable. #[arg(long, overrides_with = "no_editable")] pub editable: bool, #[arg(long, overrides_with = "editable", hide = true, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_EDITABLE)] pub no_editable: bool, /// Add a dependency as provided. /// /// By default, uv will use the `tool.uv.sources` section to record source information for Git, /// local, editable, and direct URL requirements. When `--raw` is provided, uv will add source /// requirements to `project.dependencies`, rather than `tool.uv.sources`. /// /// Additionally, by default, uv will add bounds to your dependency, e.g., `foo>=1.0.0`. When /// `--raw` is provided, uv will add the dependency without bounds. #[arg( long, conflicts_with = "editable", conflicts_with = "no_editable", conflicts_with = "rev", conflicts_with = "tag", conflicts_with = "branch", alias = "raw-sources" )] pub raw: bool, /// The kind of version specifier to use when adding dependencies. /// /// When adding a dependency to the project, if no constraint or URL is provided, a constraint /// is added based on the latest compatible version of the package. By default, a lower bound /// constraint is used, e.g., `>=1.2.3`. /// /// When `--frozen` is provided, no resolution is performed, and dependencies are always added /// without constraints. /// /// This option is in preview and may change in any future release. #[arg(long, value_enum)] pub bounds: Option, /// Commit to use when adding a dependency from Git. #[arg(long, group = "git-ref", action = clap::ArgAction::Set)] pub rev: Option, /// Tag to use when adding a dependency from Git. #[arg(long, group = "git-ref", action = clap::ArgAction::Set)] pub tag: Option, /// Branch to use when adding a dependency from Git. #[arg(long, group = "git-ref", action = clap::ArgAction::Set)] pub branch: Option, /// Whether to use Git LFS when adding a dependency from Git. #[arg(long, env = EnvVars::UV_GIT_LFS, value_parser = clap::builder::BoolishValueParser::new())] pub lfs: bool, /// Extras to enable for the dependency. /// /// May be provided more than once. /// /// To add this dependency to an optional extra instead, see `--optional`. #[arg(long)] pub extra: Option>, /// Avoid syncing the virtual environment. #[arg(long, env = EnvVars::UV_NO_SYNC, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "frozen")] pub no_sync: bool, /// Assert that the `uv.lock` will remain unchanged. /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, /// uv will exit with an error. #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, /// Add dependencies without re-locking the project. /// /// The project environment will not be synced. #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, /// Prefer the active virtual environment over the project's virtual environment. /// /// If the project virtual environment is active or no virtual environment is active, this has /// no effect. #[arg(long, overrides_with = "no_active")] pub active: bool, /// Prefer project's virtual environment over an active environment. /// /// This is the default behavior. #[arg(long, overrides_with = "active", hide = true)] pub no_active: bool, #[command(flatten)] pub installer: ResolverInstallerArgs, #[command(flatten)] pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, /// Add the dependency to a specific package in the workspace. #[arg(long, conflicts_with = "isolated")] pub package: Option, /// Add the dependency to the specified Python script, rather than to a project. /// /// If provided, uv will add the dependency to the script's inline metadata table, in adherence /// with PEP 723. If no such inline metadata table is present, a new one will be created and /// added to the script. When executed via `uv run`, uv will create a temporary environment for /// the script with all inline dependencies installed. #[arg( long, conflicts_with = "dev", conflicts_with = "optional", conflicts_with = "package", conflicts_with = "workspace" )] pub script: Option, /// The Python interpreter to use for resolving and syncing. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// Add the dependency as a workspace member. /// /// By default, uv will add path dependencies that are within the workspace directory /// as workspace members. When used with a path dependency, the package will be added /// to the workspace's `members` list in the root `pyproject.toml` file. #[arg(long, overrides_with = "no_workspace")] pub workspace: bool, /// Don't add the dependency as a workspace member. /// /// By default, when adding a dependency that's a local path and is within the workspace /// directory, uv will add it as a workspace member; pass `--no-workspace` to add the package /// as direct path dependency instead. #[arg(long, overrides_with = "workspace")] pub no_workspace: bool, /// Do not install the current project. /// /// By default, the current project is installed into the environment with all of its /// dependencies. The `--no-install-project` option allows the project to be excluded, but all of /// its dependencies are still installed. This is particularly useful in situations like building /// Docker images where installing the project separately from its dependencies allows optimal /// layer caching. /// /// The inverse `--only-install-project` can be used to install _only_ the project itself, /// excluding all dependencies. #[arg( long, conflicts_with = "frozen", conflicts_with = "no_sync", conflicts_with = "only_install_project" )] pub no_install_project: bool, /// Only install the current project. #[arg( long, conflicts_with = "frozen", conflicts_with = "no_sync", conflicts_with = "no_install_project", hide = true )] pub only_install_project: bool, /// Do not install any workspace members, including the current project. /// /// By default, all workspace members and their dependencies are installed into the /// environment. The `--no-install-workspace` option allows exclusion of all the workspace /// members while retaining their dependencies. This is particularly useful in situations like /// building Docker images where installing the workspace separately from its dependencies /// allows optimal layer caching. /// /// The inverse `--only-install-workspace` can be used to install _only_ workspace members, /// excluding all other dependencies. #[arg( long, conflicts_with = "frozen", conflicts_with = "no_sync", conflicts_with = "only_install_workspace" )] pub no_install_workspace: bool, /// Only install workspace members, including the current project. #[arg( long, conflicts_with = "frozen", conflicts_with = "no_sync", conflicts_with = "no_install_workspace", hide = true )] pub only_install_workspace: bool, /// Do not install local path dependencies /// /// Skips the current project, workspace members, and any other local (path or editable) /// packages. Only remote/indexed dependencies are installed. Useful in Docker builds to cache /// heavy third-party dependencies first and layer local packages separately. /// /// The inverse `--only-install-local` can be used to install _only_ local packages, excluding /// all remote dependencies. #[arg( long, conflicts_with = "frozen", conflicts_with = "no_sync", conflicts_with = "only_install_local" )] pub no_install_local: bool, /// Only install local path dependencies #[arg( long, conflicts_with = "frozen", conflicts_with = "no_sync", conflicts_with = "no_install_local", hide = true )] pub only_install_local: bool, /// Do not install the given package(s). /// /// By default, all project's dependencies are installed into the environment. The /// `--no-install-package` option allows exclusion of specific packages. Note this can result /// in a broken environment, and should be used with caution. /// /// The inverse `--only-install-package` can be used to install _only_ the specified packages, /// excluding all others. #[arg( long, conflicts_with = "frozen", conflicts_with = "no_sync", conflicts_with = "only_install_package" )] pub no_install_package: Vec, /// Only install the given package(s). #[arg( long, conflicts_with = "frozen", conflicts_with = "no_sync", conflicts_with = "no_install_package", hide = true )] pub only_install_package: Vec, } #[derive(Args)] pub struct RemoveArgs { /// The names of the dependencies to remove (e.g., `ruff`). #[arg(required = true)] pub packages: Vec>, /// Remove the packages from the development dependency group. /// /// This option is an alias for `--group dev`. #[arg(long, conflicts_with("optional"), conflicts_with("group"), env = EnvVars::UV_DEV, value_parser = clap::builder::BoolishValueParser::new())] pub dev: bool, /// Remove the packages from the project's optional dependencies for the specified extra. #[arg( long, conflicts_with("dev"), conflicts_with("group"), conflicts_with("script") )] pub optional: Option, /// Remove the packages from the specified dependency group. #[arg( long, conflicts_with("dev"), conflicts_with("optional"), conflicts_with("script") )] pub group: Option, /// Avoid syncing the virtual environment after re-locking the project. #[arg(long, env = EnvVars::UV_NO_SYNC, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "frozen")] pub no_sync: bool, /// Prefer the active virtual environment over the project's virtual environment. /// /// If the project virtual environment is active or no virtual environment is active, this has /// no effect. #[arg(long, overrides_with = "no_active")] pub active: bool, /// Prefer project's virtual environment over an active environment. /// /// This is the default behavior. #[arg(long, overrides_with = "active", hide = true)] pub no_active: bool, /// Assert that the `uv.lock` will remain unchanged. /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, /// uv will exit with an error. #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, /// Remove dependencies without re-locking the project. /// /// The project environment will not be synced. #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, #[command(flatten)] pub installer: ResolverInstallerArgs, #[command(flatten)] pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, /// Remove the dependencies from a specific package in the workspace. #[arg(long, conflicts_with = "isolated")] pub package: Option, /// Remove the dependency from the specified Python script, rather than from a project. /// /// If provided, uv will remove the dependency from the script's inline metadata table, in /// adherence with PEP 723. #[arg(long)] pub script: Option, /// The Python interpreter to use for resolving and syncing. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, } #[derive(Args)] pub struct TreeArgs { /// Show a platform-independent dependency tree. /// /// Shows resolved package versions for all Python versions and platforms, rather than filtering /// to those that are relevant for the current environment. /// /// Multiple versions may be shown for a each package. #[arg(long)] pub universal: bool, #[command(flatten)] pub tree: DisplayTreeArgs, /// Include the development dependency group. /// /// Development dependencies are defined via `dependency-groups.dev` or /// `tool.uv.dev-dependencies` in a `pyproject.toml`. /// /// This option is an alias for `--group dev`. #[arg(long, overrides_with("no_dev"), hide = true, env = EnvVars::UV_DEV, value_parser = clap::builder::BoolishValueParser::new())] pub dev: bool, /// Only include the development dependency group. /// /// The project and its dependencies will be omitted. /// /// This option is an alias for `--only-group dev`. Implies `--no-default-groups`. #[arg(long, conflicts_with_all = ["group", "all_groups", "no_dev"])] pub only_dev: bool, /// Disable the development dependency group. /// /// This option is an alias of `--no-group dev`. /// See `--no-default-groups` to disable all default groups instead. #[arg(long, overrides_with("dev"), env = EnvVars::UV_NO_DEV, value_parser = clap::builder::BoolishValueParser::new())] pub no_dev: bool, /// Include dependencies from the specified dependency group. /// /// May be provided multiple times. #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub group: Vec, /// Disable the specified dependency group. /// /// This option always takes precedence over default groups, /// `--all-groups`, and `--group`. /// /// May be provided multiple times. #[arg(long, env = EnvVars::UV_NO_GROUP, value_delimiter = ' ')] pub no_group: Vec, /// Ignore the default dependency groups. /// /// uv includes the groups defined in `tool.uv.default-groups` by default. /// This disables that option, however, specific groups can still be included with `--group`. #[arg(long, env = EnvVars::UV_NO_DEFAULT_GROUPS)] pub no_default_groups: bool, /// Only include dependencies from the specified dependency group. /// /// The project and its dependencies will be omitted. /// /// May be provided multiple times. Implies `--no-default-groups`. #[arg(long, conflicts_with_all = ["group", "dev", "all_groups"])] pub only_group: Vec, /// Include dependencies from all dependency groups. /// /// `--no-group` can be used to exclude specific groups. #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub all_groups: bool, /// Assert that the `uv.lock` will remain unchanged. /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, /// uv will exit with an error. #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, /// Display the requirements without locking the project. /// /// If the lockfile is missing, uv will exit with an error. #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, #[command(flatten)] pub build: BuildOptionsArgs, #[command(flatten)] pub resolver: ResolverArgs, /// Show the dependency tree the specified PEP 723 Python script, rather than the current /// project. /// /// If provided, uv will resolve the dependencies based on its inline metadata table, in /// adherence with PEP 723. #[arg(long)] pub script: Option, /// The Python version to use when filtering the tree. /// /// For example, pass `--python-version 3.10` to display the dependencies that would be included /// when installing on Python 3.10. /// /// Defaults to the version of the discovered Python interpreter. #[arg(long, conflicts_with = "universal")] pub python_version: Option, /// The platform to use when filtering the tree. /// /// For example, pass `--platform windows` to display the dependencies that would be included /// when installing on Windows. /// /// Represented as a "target triple", a string that describes the target platform in terms of /// its CPU, vendor, and operating system name, like `x86_64-unknown-linux-gnu` or /// `aarch64-apple-darwin`. #[arg(long, conflicts_with = "universal")] pub python_platform: Option, /// The Python interpreter to use for locking and filtering. /// /// By default, the tree is filtered to match the platform as reported by the Python /// interpreter. Use `--universal` to display the tree for all platforms, or use /// `--python-version` or `--python-platform` to override a subset of markers. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, } #[derive(Args)] pub struct ExportArgs { #[allow(clippy::doc_markdown)] /// The format to which `uv.lock` should be exported. /// /// Supports `requirements.txt`, `pylock.toml` (PEP 751) and CycloneDX v1.5 JSON output formats. /// /// uv will infer the output format from the file extension of the output file, if /// provided. Otherwise, defaults to `requirements.txt`. #[arg(long, value_enum)] pub format: Option, /// Export the entire workspace. /// /// The dependencies for all workspace members will be included in the exported requirements /// file. /// /// Any extras or groups specified via `--extra`, `--group`, or related options will be applied /// to all workspace members. #[arg(long, conflicts_with = "package")] pub all_packages: bool, /// Export the dependencies for specific packages in the workspace. /// /// If any workspace member does not exist, uv will exit with an error. #[arg(long, conflicts_with = "all_packages")] pub package: Vec, /// Prune the given package from the dependency tree. /// /// Pruned packages will be excluded from the exported requirements file, as will any /// dependencies that are no longer required after the pruned package is removed. #[arg(long, conflicts_with = "all_packages", value_name = "PACKAGE")] pub prune: Vec, /// Include optional dependencies from the specified extra name. /// /// May be provided more than once. #[arg(long, conflicts_with = "all_extras", conflicts_with = "only_group", value_parser = extra_name_with_clap_error)] pub extra: Option>, /// Include all optional dependencies. #[arg(long, conflicts_with = "extra", conflicts_with = "only_group")] pub all_extras: bool, /// Exclude the specified optional dependencies, if `--all-extras` is supplied. /// /// May be provided multiple times. #[arg(long)] pub no_extra: Vec, #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, /// Include the development dependency group. /// /// This option is an alias for `--group dev`. #[arg(long, overrides_with("no_dev"), hide = true, env = EnvVars::UV_DEV, value_parser = clap::builder::BoolishValueParser::new())] pub dev: bool, /// Disable the development dependency group. /// /// This option is an alias of `--no-group dev`. /// See `--no-default-groups` to disable all default groups instead. #[arg(long, overrides_with("dev"), env = EnvVars::UV_NO_DEV, value_parser = clap::builder::BoolishValueParser::new())] pub no_dev: bool, /// Only include the development dependency group. /// /// The project and its dependencies will be omitted. /// /// This option is an alias for `--only-group dev`. Implies `--no-default-groups`. #[arg(long, conflicts_with_all = ["group", "all_groups", "no_dev"])] pub only_dev: bool, /// Include dependencies from the specified dependency group. /// /// May be provided multiple times. #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub group: Vec, /// Disable the specified dependency group. /// /// This option always takes precedence over default groups, /// `--all-groups`, and `--group`. /// /// May be provided multiple times. #[arg(long, env = EnvVars::UV_NO_GROUP, value_delimiter = ' ')] pub no_group: Vec, /// Ignore the default dependency groups. /// /// uv includes the groups defined in `tool.uv.default-groups` by default. /// This disables that option, however, specific groups can still be included with `--group`. #[arg(long, env = EnvVars::UV_NO_DEFAULT_GROUPS)] pub no_default_groups: bool, /// Only include dependencies from the specified dependency group. /// /// The project and its dependencies will be omitted. /// /// May be provided multiple times. Implies `--no-default-groups`. #[arg(long, conflicts_with_all = ["group", "dev", "all_groups"])] pub only_group: Vec, /// Include dependencies from all dependency groups. /// /// `--no-group` can be used to exclude specific groups. #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub all_groups: bool, /// Exclude comment annotations indicating the source of each package. #[arg(long, overrides_with("annotate"))] pub no_annotate: bool, #[arg(long, overrides_with("no_annotate"), hide = true)] pub annotate: bool, /// Exclude the comment header at the top of the generated output file. #[arg(long, overrides_with("header"))] pub no_header: bool, #[arg(long, overrides_with("no_header"), hide = true)] pub header: bool, /// Export any non-editable dependencies, including the project and any workspace members, as /// editable. #[arg(long, overrides_with = "no_editable", hide = true)] pub editable: bool, /// Export any editable dependencies, including the project and any workspace members, as /// non-editable. #[arg(long, overrides_with = "editable", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_EDITABLE)] pub no_editable: bool, /// Include hashes for all dependencies. #[arg(long, overrides_with("no_hashes"), hide = true)] pub hashes: bool, /// Omit hashes in the generated output. #[arg(long, overrides_with("hashes"))] pub no_hashes: bool, /// Write the exported requirements to the given file. #[arg(long, short)] pub output_file: Option, /// Do not emit the current project. /// /// By default, the current project is included in the exported requirements file with all of /// its dependencies. The `--no-emit-project` option allows the project to be excluded, but all /// of its dependencies to remain included. /// /// The inverse `--only-emit-project` can be used to emit _only_ the project itself, excluding /// all dependencies. #[arg( long, alias = "no-install-project", conflicts_with = "only_emit_project" )] pub no_emit_project: bool, /// Only emit the current project. #[arg( long, alias = "only-install-project", conflicts_with = "no_emit_project", hide = true )] pub only_emit_project: bool, /// Do not emit any workspace members, including the root project. /// /// By default, all workspace members and their dependencies are included in the exported /// requirements file, with all of their dependencies. The `--no-emit-workspace` option allows /// exclusion of all the workspace members while retaining their dependencies. /// /// The inverse `--only-emit-workspace` can be used to emit _only_ workspace members, excluding /// all other dependencies. #[arg( long, alias = "no-install-workspace", conflicts_with = "only_emit_workspace" )] pub no_emit_workspace: bool, /// Only emit workspace members, including the root project. #[arg( long, alias = "only-install-workspace", conflicts_with = "no_emit_workspace", hide = true )] pub only_emit_workspace: bool, /// Do not include local path dependencies in the exported requirements. /// /// Omits the current project, workspace members, and any other local (path or editable) /// packages from the export. Only remote/indexed dependencies are written. Useful for Docker /// and CI flows that want to export and cache third-party dependencies first. /// /// The inverse `--only-emit-local` can be used to emit _only_ local packages, excluding all /// remote dependencies. #[arg(long, alias = "no-install-local", conflicts_with = "only_emit_local")] pub no_emit_local: bool, /// Only include local path dependencies in the exported requirements. #[arg( long, alias = "only-install-local", conflicts_with = "no_emit_local", hide = true )] pub only_emit_local: bool, /// Do not emit the given package(s). /// /// By default, all project's dependencies are included in the exported requirements /// file. The `--no-emit-package` option allows exclusion of specific packages. /// /// The inverse `--only-emit-package` can be used to emit _only_ the specified packages, /// excluding all others. #[arg( long, alias = "no-install-package", conflicts_with = "only_emit_package" )] pub no_emit_package: Vec, /// Only emit the given package(s). #[arg( long, alias = "only-install-package", conflicts_with = "no_emit_package", hide = true )] pub only_emit_package: Vec, /// Assert that the `uv.lock` will remain unchanged. /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, /// uv will exit with an error. #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, /// Do not update the `uv.lock` before exporting. /// /// If a `uv.lock` does not exist, uv will exit with an error. #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, #[command(flatten)] pub resolver: ResolverArgs, #[command(flatten)] pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, /// Export the dependencies for the specified PEP 723 Python script, rather than the current /// project. /// /// If provided, uv will resolve the dependencies based on its inline metadata table, in /// adherence with PEP 723. #[arg(long, conflicts_with_all = ["all_packages", "package", "no_emit_project", "no_emit_workspace"])] pub script: Option, /// The Python interpreter to use during resolution. /// /// A Python interpreter is required for building source distributions to determine package /// metadata when there are not wheels. /// /// The interpreter is also used as the fallback value for the minimum Python version if /// `requires-python` is not set. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, } #[derive(Args)] pub struct FormatArgs { /// Check if files are formatted without applying changes. #[arg(long)] pub check: bool, /// Show a diff of formatting changes without applying them. /// /// Implies `--check`. #[arg(long)] pub diff: bool, /// The version of Ruff to use for formatting. /// /// By default, a version of Ruff pinned by uv will be used. #[arg(long)] pub version: Option, /// Additional arguments to pass to Ruff. /// /// For example, use `uv format -- --line-length 100` to set the line length or /// `uv format -- src/module/foo.py` to format a specific file. #[arg(last = true)] pub extra_args: Vec, /// Avoid discovering a project or workspace. /// /// Instead of running the formatter in the context of the current project, run it in the /// context of the current directory. This is useful when the current directory is not a /// project. #[arg(long)] pub no_project: bool, } #[derive(Args)] pub struct AuthNamespace { #[command(subcommand)] pub command: AuthCommand, } #[derive(Subcommand)] pub enum AuthCommand { /// Login to a service Login(AuthLoginArgs), /// Logout of a service Logout(AuthLogoutArgs), /// Show the authentication token for a service Token(AuthTokenArgs), /// Show the path to the uv credentials directory. /// /// By default, credentials are stored in the uv data directory at /// `$XDG_DATA_HOME/uv/credentials` or `$HOME/.local/share/uv/credentials` on Unix and /// `%APPDATA%\uv\data\credentials` on Windows. /// /// The credentials directory may be overridden with `$UV_CREDENTIALS_DIR`. /// /// Credentials are only stored in this directory when the plaintext backend is used, as /// opposed to the native backend, which uses the system keyring. Dir(AuthDirArgs), /// Act as a credential helper for external tools. /// /// Implements the Bazel credential helper protocol to provide credentials /// to external tools via JSON over stdin/stdout. /// /// This command is typically invoked by external tools. #[command(hide = true)] Helper(AuthHelperArgs), } #[derive(Args)] pub struct ToolNamespace { #[command(subcommand)] pub command: ToolCommand, } #[derive(Subcommand)] pub enum ToolCommand { /// Run a command provided by a Python package. /// /// By default, the package to install is assumed to match the command name. /// /// The name of the command can include an exact version in the format `@`, /// e.g., `uv tool run ruff@0.3.0`. If more complex version specification is desired or if the /// command is provided by a different package, use `--from`. /// /// `uvx` can be used to invoke Python, e.g., with `uvx python` or `uvx python@`. A /// Python interpreter will be started in an isolated virtual environment. /// /// If the tool was previously installed, i.e., via `uv tool install`, the installed version /// will be used unless a version is requested or the `--isolated` flag is used. /// /// `uvx` is provided as a convenient alias for `uv tool run`, their behavior is identical. /// /// If no command is provided, the installed tools are displayed. /// /// Packages are installed into an ephemeral virtual environment in the uv cache directory. #[command( after_help = "Use `uvx` as a shortcut for `uv tool run`.\n\n\ Use `uv help tool run` for more details.", after_long_help = "" )] Run(ToolRunArgs), /// Hidden alias for `uv tool run` for the `uvx` command #[command( hide = true, override_usage = "uvx [OPTIONS] [COMMAND]", about = "Run a command provided by a Python package.", after_help = "Use `uv help tool run` for more details.", after_long_help = "", display_name = "uvx", long_version = crate::version::uv_self_version() )] Uvx(UvxArgs), /// Install commands provided by a Python package. /// /// Packages are installed into an isolated virtual environment in the uv tools directory. The /// executables are linked the tool executable directory, which is determined according to the /// XDG standard and can be retrieved with `uv tool dir --bin`. /// /// If the tool was previously installed, the existing tool will generally be replaced. Install(ToolInstallArgs), /// Upgrade installed tools. /// /// If a tool was installed with version constraints, they will be respected on upgrade — to /// upgrade a tool beyond the originally provided constraints, use `uv tool install` again. /// /// If a tool was installed with specific settings, they will be respected on upgraded. For /// example, if `--prereleases allow` was provided during installation, it will continue to be /// respected in upgrades. #[command(alias = "update")] Upgrade(ToolUpgradeArgs), /// List installed tools. #[command(alias = "ls")] List(ToolListArgs), /// Uninstall a tool. Uninstall(ToolUninstallArgs), /// Ensure that the tool executable directory is on the `PATH`. /// /// If the tool executable directory is not present on the `PATH`, uv will attempt to add it to /// the relevant shell configuration files. /// /// If the shell configuration files already include a blurb to add the executable directory to /// the path, but the directory is not present on the `PATH`, uv will exit with an error. /// /// The tool executable directory is determined according to the XDG standard and can be /// retrieved with `uv tool dir --bin`. #[command(alias = "ensurepath")] UpdateShell, /// Show the path to the uv tools directory. /// /// The tools directory is used to store environments and metadata for installed tools. /// /// By default, tools are stored in the uv data directory at `$XDG_DATA_HOME/uv/tools` or /// `$HOME/.local/share/uv/tools` on Unix and `%APPDATA%\uv\data\tools` on Windows. /// /// The tool installation directory may be overridden with `$UV_TOOL_DIR`. /// /// To instead view the directory uv installs executables into, use the `--bin` flag. Dir(ToolDirArgs), } #[derive(Args)] pub struct ToolRunArgs { /// The command to run. /// /// WARNING: The documentation for [`Self::command`] is not included in help output #[command(subcommand)] pub command: Option, /// Use the given package to provide the command. /// /// By default, the package name is assumed to match the command name. #[arg(long)] pub from: Option, /// Run with the given packages installed. #[arg(short = 'w', long)] pub with: Vec, /// Run with the given packages installed in editable mode /// /// When used in a project, these dependencies will be layered on top of the uv tool's /// environment in a separate, ephemeral environment. These dependencies are allowed to conflict /// with those specified. #[arg(long)] pub with_editable: Vec, /// Run with the packages listed in the given files. /// /// The following formats are supported: `requirements.txt`, `.py` files with inline metadata, /// and `pylock.toml`. #[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)] pub with_requirements: Vec>, /// Constrain versions using the given requirements files. /// /// Constraints files are `requirements.txt`-like files that only control the _version_ of a /// requirement that's installed. However, including a package in a constraints file will _not_ /// trigger the installation of that package. /// /// This is equivalent to pip's `--constraint` option. #[arg(long, short, alias = "constraint", env = EnvVars::UV_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub constraints: Vec>, /// Constrain build dependencies using the given requirements files when building source /// distributions. /// /// Constraints files are `requirements.txt`-like files that only control the _version_ of a /// requirement that's installed. However, including a package in a constraints file will _not_ /// trigger the installation of that package. #[arg(long, short, alias = "build-constraint", env = EnvVars::UV_BUILD_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub build_constraints: Vec>, /// Override versions using the given requirements files. /// /// Overrides files are `requirements.txt`-like files that force a specific version of a /// requirement to be installed, regardless of the requirements declared by any constituent /// package, and regardless of whether this would be considered an invalid resolution. /// /// While constraints are _additive_, in that they're combined with the requirements of the /// constituent packages, overrides are _absolute_, in that they completely replace the /// requirements of the constituent packages. #[arg(long, alias = "override", env = EnvVars::UV_OVERRIDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub overrides: Vec>, /// Run the tool in an isolated virtual environment, ignoring any already-installed tools. #[arg(long, env = EnvVars::UV_ISOLATED, value_parser = clap::builder::BoolishValueParser::new())] pub isolated: bool, /// Load environment variables from a `.env` file. /// /// Can be provided multiple times, with subsequent files overriding values defined in previous /// files. #[arg(long, value_delimiter = ' ', env = EnvVars::UV_ENV_FILE)] pub env_file: Vec, /// Avoid reading environment variables from a `.env` file. #[arg(long, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_ENV_FILE)] pub no_env_file: bool, #[command(flatten)] pub installer: ResolverInstallerArgs, #[command(flatten)] pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, /// Whether to use Git LFS when adding a dependency from Git. #[arg(long, env = EnvVars::UV_GIT_LFS, value_parser = clap::builder::BoolishValueParser::new())] pub lfs: bool, /// The Python interpreter to use to build the run environment. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// Whether to show resolver and installer output from any environment modifications. /// /// By default, environment modifications are omitted, but enabled under `--verbose`. #[arg(long, env = EnvVars::UV_SHOW_RESOLUTION, value_parser = clap::builder::BoolishValueParser::new(), hide = true)] pub show_resolution: bool, /// The platform for which requirements should be installed. /// /// Represented as a "target triple", a string that describes the target platform in terms of /// its CPU, vendor, and operating system name, like `x86_64-unknown-linux-gnu` or /// `aarch64-apple-darwin`. /// /// When targeting macOS (Darwin), the default minimum version is `13.0`. Use /// `MACOSX_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting iOS, the default minimum version is `13.0`. Use /// `IPHONEOS_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting Android, the default minimum Android API level is `24`. Use /// `ANDROID_API_LEVEL` to specify a different minimum version, e.g., `26`. /// /// WARNING: When specified, uv will select wheels that are compatible with the _target_ /// platform; as a result, the installed distributions may not be compatible with the _current_ /// platform. Conversely, any distributions that are built from source may be incompatible with /// the _target_ platform, as they will be built for the _current_ platform. The /// `--python-platform` option is intended for advanced use cases. #[arg(long)] pub python_platform: Option, #[arg(long, hide = true)] pub generate_shell_completion: Option, } #[derive(Args)] pub struct UvxArgs { #[command(flatten)] pub tool_run: ToolRunArgs, /// Display the uvx version. #[arg(short = 'V', long, action = clap::ArgAction::Version)] pub version: Option, } #[derive(Args)] pub struct ToolInstallArgs { /// The package to install commands from. pub package: String, /// The package to install commands from. /// /// This option is provided for parity with `uv tool run`, but is redundant with `package`. #[arg(long, hide = true)] pub from: Option, /// Include the following additional requirements. #[arg(short = 'w', long)] pub with: Vec, /// Run with the packages listed in the given files. /// /// The following formats are supported: `requirements.txt`, `.py` files with inline metadata, /// and `pylock.toml`. #[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)] pub with_requirements: Vec>, /// Install the target package in editable mode, such that changes in the package's source /// directory are reflected without reinstallation. #[arg(short, long)] pub editable: bool, /// Include the given packages in editable mode. #[arg(long)] pub with_editable: Vec, /// Install executables from the following packages. #[arg(long)] pub with_executables_from: Vec, /// Constrain versions using the given requirements files. /// /// Constraints files are `requirements.txt`-like files that only control the _version_ of a /// requirement that's installed. However, including a package in a constraints file will _not_ /// trigger the installation of that package. /// /// This is equivalent to pip's `--constraint` option. #[arg(long, short, alias = "constraint", env = EnvVars::UV_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub constraints: Vec>, /// Override versions using the given requirements files. /// /// Overrides files are `requirements.txt`-like files that force a specific version of a /// requirement to be installed, regardless of the requirements declared by any constituent /// package, and regardless of whether this would be considered an invalid resolution. /// /// While constraints are _additive_, in that they're combined with the requirements of the /// constituent packages, overrides are _absolute_, in that they completely replace the /// requirements of the constituent packages. #[arg(long, alias = "override", env = EnvVars::UV_OVERRIDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub overrides: Vec>, /// Exclude packages from resolution using the given requirements files. /// /// Excludes files are `requirements.txt`-like files that specify packages to exclude /// from the resolution. When a package is excluded, it will be omitted from the /// dependency list entirely and its own dependencies will be ignored during the resolution /// phase. Excludes are unconditional in that requirement specifiers and markers are ignored; /// any package listed in the provided file will be omitted from all resolved environments. #[arg(long, alias = "exclude", env = EnvVars::UV_EXCLUDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub excludes: Vec>, /// Constrain build dependencies using the given requirements files when building source /// distributions. /// /// Constraints files are `requirements.txt`-like files that only control the _version_ of a /// requirement that's installed. However, including a package in a constraints file will _not_ /// trigger the installation of that package. #[arg(long, short, alias = "build-constraint", env = EnvVars::UV_BUILD_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub build_constraints: Vec>, #[command(flatten)] pub installer: ResolverInstallerArgs, #[command(flatten)] pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, /// Force installation of the tool. /// /// Will replace any existing entry points with the same name in the executable directory. #[arg(long)] pub force: bool, /// Whether to use Git LFS when adding a dependency from Git. #[arg(long, env = EnvVars::UV_GIT_LFS, value_parser = clap::builder::BoolishValueParser::new())] pub lfs: bool, /// The Python interpreter to use to build the tool environment. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// The platform for which requirements should be installed. /// /// Represented as a "target triple", a string that describes the target platform in terms of /// its CPU, vendor, and operating system name, like `x86_64-unknown-linux-gnu` or /// `aarch64-apple-darwin`. /// /// When targeting macOS (Darwin), the default minimum version is `13.0`. Use /// `MACOSX_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting iOS, the default minimum version is `13.0`. Use /// `IPHONEOS_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting Android, the default minimum Android API level is `24`. Use /// `ANDROID_API_LEVEL` to specify a different minimum version, e.g., `26`. /// /// WARNING: When specified, uv will select wheels that are compatible with the _target_ /// platform; as a result, the installed distributions may not be compatible with the _current_ /// platform. Conversely, any distributions that are built from source may be incompatible with /// the _target_ platform, as they will be built for the _current_ platform. The /// `--python-platform` option is intended for advanced use cases. #[arg(long)] pub python_platform: Option, } #[derive(Args)] pub struct ToolListArgs { /// Whether to display the path to each tool environment and installed executable. #[arg(long)] pub show_paths: bool, /// Whether to display the version specifier(s) used to install each tool. #[arg(long)] pub show_version_specifiers: bool, /// Whether to display the additional requirements installed with each tool. #[arg(long)] pub show_with: bool, /// Whether to display the extra requirements installed with each tool. #[arg(long)] pub show_extras: bool, /// Whether to display the Python version associated with each tool. #[arg(long)] pub show_python: bool, // Hide unused global Python options. #[arg(long, hide = true)] pub python_preference: Option, #[arg(long, hide = true)] pub no_python_downloads: bool, } #[derive(Args)] pub struct ToolDirArgs { /// Show the directory into which `uv tool` will install executables. /// /// By default, `uv tool dir` shows the directory into which the tool Python environments /// themselves are installed, rather than the directory containing the linked executables. /// /// The tool executable directory is determined according to the XDG standard and is derived /// from the following environment variables, in order of preference: /// /// - `$UV_TOOL_BIN_DIR` /// - `$XDG_BIN_HOME` /// - `$XDG_DATA_HOME/../bin` /// - `$HOME/.local/bin` #[arg(long, verbatim_doc_comment)] pub bin: bool, } #[derive(Args)] pub struct ToolUninstallArgs { /// The name of the tool to uninstall. #[arg(required = true)] pub name: Vec, /// Uninstall all tools. #[arg(long, conflicts_with("name"))] pub all: bool, } #[derive(Args)] pub struct ToolUpgradeArgs { /// The name of the tool to upgrade, along with an optional version specifier. #[arg(required = true)] pub name: Vec, /// Upgrade all tools. #[arg(long, conflicts_with("name"))] pub all: bool, /// Upgrade a tool, and specify it to use the given Python interpreter to build its environment. /// Use with `--all` to apply to all tools. /// /// See `uv help python` for details on Python discovery and supported request formats. #[arg( long, short, env = EnvVars::UV_PYTHON, verbatim_doc_comment, help_heading = "Python options", value_parser = parse_maybe_string, )] pub python: Option>, /// The platform for which requirements should be installed. /// /// Represented as a "target triple", a string that describes the target platform in terms of /// its CPU, vendor, and operating system name, like `x86_64-unknown-linux-gnu` or /// `aarch64-apple-darwin`. /// /// When targeting macOS (Darwin), the default minimum version is `13.0`. Use /// `MACOSX_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting iOS, the default minimum version is `13.0`. Use /// `IPHONEOS_DEPLOYMENT_TARGET` to specify a different minimum version, e.g., `14.0`. /// /// When targeting Android, the default minimum Android API level is `24`. Use /// `ANDROID_API_LEVEL` to specify a different minimum version, e.g., `26`. /// /// WARNING: When specified, uv will select wheels that are compatible with the _target_ /// platform; as a result, the installed distributions may not be compatible with the _current_ /// platform. Conversely, any distributions that are built from source may be incompatible with /// the _target_ platform, as they will be built for the _current_ platform. The /// `--python-platform` option is intended for advanced use cases. #[arg(long)] pub python_platform: Option, // The following is equivalent to flattening `ResolverInstallerArgs`, with the `--upgrade`, and // `--upgrade-package` options hidden, and the `--no-upgrade` option removed. /// Allow package upgrades, ignoring pinned versions in any existing output file. Implies /// `--refresh`. #[arg(hide = true, long, short = 'U', help_heading = "Resolver options")] pub upgrade: bool, /// Allow upgrades for a specific package, ignoring pinned versions in any existing output /// file. Implies `--refresh-package`. #[arg(hide = true, long, short = 'P', help_heading = "Resolver options")] pub upgrade_package: Vec>, #[command(flatten)] pub index_args: IndexArgs, /// Reinstall all packages, regardless of whether they're already installed. Implies /// `--refresh`. #[arg( long, alias = "force-reinstall", overrides_with("no_reinstall"), help_heading = "Installer options" )] pub reinstall: bool, #[arg( long, overrides_with("reinstall"), hide = true, help_heading = "Installer options" )] pub no_reinstall: bool, /// Reinstall a specific package, regardless of whether it's already installed. Implies /// `--refresh-package`. #[arg(long, help_heading = "Installer options")] pub reinstall_package: Vec, /// The strategy to use when resolving against multiple index URLs. /// /// By default, uv will stop at the first index on which a given package is available, and limit /// resolutions to those present on that first index (`first-index`). This prevents "dependency /// confusion" attacks, whereby an attacker can upload a malicious package under the same name /// to an alternate index. #[arg( long, value_enum, env = EnvVars::UV_INDEX_STRATEGY, help_heading = "Index options" )] pub index_strategy: Option, /// Attempt to use `keyring` for authentication for index URLs. /// /// At present, only `--keyring-provider subprocess` is supported, which configures uv to use /// the `keyring` CLI to handle authentication. /// /// Defaults to `disabled`. #[arg( long, value_enum, env = EnvVars::UV_KEYRING_PROVIDER, help_heading = "Index options" )] pub keyring_provider: Option, /// The strategy to use when selecting between the different compatible versions for a given /// package requirement. /// /// By default, uv will use the latest compatible version of each package (`highest`). #[arg( long, value_enum, env = EnvVars::UV_RESOLUTION, help_heading = "Resolver options" )] pub resolution: Option, /// The strategy to use when considering pre-release versions. /// /// By default, uv will accept pre-releases for packages that _only_ publish pre-releases, along /// with first-party requirements that contain an explicit pre-release marker in the declared /// specifiers (`if-necessary-or-explicit`). #[arg( long, value_enum, env = EnvVars::UV_PRERELEASE, help_heading = "Resolver options" )] pub prerelease: Option, #[arg(long, hide = true)] pub pre: bool, /// The strategy to use when selecting multiple versions of a given package across Python /// versions and platforms. /// /// By default, uv will optimize for selecting the latest version of each package for each /// supported Python version (`requires-python`), while minimizing the number of selected /// versions across platforms. /// /// Under `fewest`, uv will minimize the number of selected versions for each package, /// preferring older versions that are compatible with a wider range of supported Python /// versions or platforms. #[arg( long, value_enum, env = EnvVars::UV_FORK_STRATEGY, help_heading = "Resolver options" )] pub fork_strategy: Option, /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. #[arg( long, short = 'C', alias = "config-settings", help_heading = "Build options" )] pub config_setting: Option>, /// Settings to pass to the PEP 517 build backend for a specific package, specified as `PACKAGE:KEY=VALUE` pairs. #[arg( long, alias = "config-settings-package", help_heading = "Build options" )] pub config_setting_package: Option>, /// Disable isolation when building source distributions. /// /// Assumes that build dependencies specified by PEP 518 are already installed. #[arg( long, overrides_with("build_isolation"), help_heading = "Build options", env = EnvVars::UV_NO_BUILD_ISOLATION, value_parser = clap::builder::BoolishValueParser::new(), )] pub no_build_isolation: bool, /// Disable isolation when building source distributions for a specific package. /// /// Assumes that the packages' build dependencies specified by PEP 518 are already installed. #[arg(long, help_heading = "Build options")] pub no_build_isolation_package: Vec, #[arg( long, overrides_with("no_build_isolation"), hide = true, help_heading = "Build options" )] pub build_isolation: bool, /// Limit candidate packages to those that were uploaded prior to the given date. /// /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format /// (e.g., `2006-12-02`) resolved based on your system's configured time zone, a "friendly" /// duration (e.g., `24 hours`, `1 week`, `30 days`), or an ISO 8601 duration (e.g., `PT24H`, /// `P7D`, `P30D`). /// /// Durations do not respect semantics of the local time zone and are always resolved to a fixed /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). /// Calendar units such as months and years are not allowed. #[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")] pub exclude_newer: Option, /// Limit candidate packages for specific packages to those that were uploaded prior to the /// given date. /// /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 /// timestamp (e.g., `2006-12-02T02:07:43Z`), a local date in the same format (e.g., /// `2006-12-02`) resolved based on your system's configured time zone, a "friendly" duration /// (e.g., `24 hours`, `1 week`, `30 days`), or an ISO 8601 duration (e.g., `PT24H`, `P7D`, /// `P30D`). /// /// Durations do not respect semantics of the local time zone and are always resolved to a fixed /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). /// Calendar units such as months and years are not allowed. /// /// Can be provided multiple times for different packages. #[arg(long, help_heading = "Resolver options")] pub exclude_newer_package: Option>, /// The method to use when installing packages from the global cache. /// /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and /// Windows. /// /// WARNING: The use of symlink link mode is discouraged, as they create tight coupling between /// the cache and the target environment. For example, clearing the cache (`uv cache clean`) /// will break all installed packages by way of removing the underlying source files. Use /// symlinks with caution. #[arg( long, value_enum, env = EnvVars::UV_LINK_MODE, help_heading = "Installer options" )] pub link_mode: Option, /// Compile Python files to bytecode after installation. /// /// By default, uv does not compile Python (`.py`) files to bytecode (`__pycache__/*.pyc`); /// instead, compilation is performed lazily the first time a module is imported. For use-cases /// in which start time is critical, such as CLI applications and Docker containers, this option /// can be enabled to trade longer installation times for faster start times. /// /// When enabled, uv will process the entire site-packages directory (including packages that /// are not being modified by the current operation) for consistency. Like pip, it will also /// ignore errors. #[arg( long, alias = "compile", overrides_with("no_compile_bytecode"), help_heading = "Installer options", env = EnvVars::UV_COMPILE_BYTECODE, value_parser = clap::builder::BoolishValueParser::new(), )] pub compile_bytecode: bool, #[arg( long, alias = "no-compile", overrides_with("compile_bytecode"), hide = true, help_heading = "Installer options" )] pub no_compile_bytecode: bool, /// Ignore the `tool.uv.sources` table when resolving dependencies. Used to lock against the /// standards-compliant, publishable package metadata, as opposed to using any workspace, Git, /// URL, or local path sources. #[arg( long, env = EnvVars::UV_NO_SOURCES, value_parser = clap::builder::BoolishValueParser::new(), help_heading = "Resolver options", )] pub no_sources: bool, #[command(flatten)] pub build: BuildOptionsArgs, } #[derive(Args)] pub struct PythonNamespace { #[command(subcommand)] pub command: PythonCommand, } #[derive(Subcommand)] pub enum PythonCommand { /// List the available Python installations. /// /// By default, installed Python versions and the downloads for latest available patch version /// of each supported Python major version are shown. /// /// Use `--managed-python` to view only managed Python versions. /// /// Use `--no-managed-python` to omit managed Python versions. /// /// Use `--all-versions` to view all available patch versions. /// /// Use `--only-installed` to omit available downloads. #[command(alias = "ls")] List(PythonListArgs), /// Download and install Python versions. /// /// Supports CPython and PyPy. CPython distributions are downloaded from the Astral /// `python-build-standalone` project. PyPy distributions are downloaded from `python.org`. The /// available Python versions are bundled with each uv release. To install new Python versions, /// you may need upgrade uv. /// /// Python versions are installed into the uv Python directory, which can be retrieved with `uv /// python dir`. /// /// By default, Python executables are added to a directory on the path with a minor version /// suffix, e.g., `python3.13`. To install `python3` and `python`, use the `--default` flag. Use /// `uv python dir --bin` to see the target directory. /// /// Multiple Python versions may be requested. /// /// See `uv help python` to view supported request formats. Install(PythonInstallArgs), /// Upgrade installed Python versions. /// /// Upgrades versions to the latest supported patch release. Requires the `python-upgrade` /// preview feature. /// /// A target Python minor version to upgrade may be provided, e.g., `3.13`. Multiple versions /// may be provided to perform more than one upgrade. /// /// If no target version is provided, then uv will upgrade all managed CPython versions. /// /// During an upgrade, uv will not uninstall outdated patch versions. /// /// When an upgrade is performed, virtual environments created by uv will automatically /// use the new version. However, if the virtual environment was created before the /// upgrade functionality was added, it will continue to use the old Python version; to enable /// upgrades, the environment must be recreated. /// /// Upgrades are not yet supported for alternative implementations, like PyPy. Upgrade(PythonUpgradeArgs), /// Search for a Python installation. /// /// Displays the path to the Python executable. /// /// See `uv help python` to view supported request formats and details on discovery behavior. Find(PythonFindArgs), /// Pin to a specific Python version. /// /// Writes the pinned Python version to a `.python-version` file, which is used by other uv /// commands to determine the required Python version. /// /// If no version is provided, uv will look for an existing `.python-version` file and display /// the currently pinned version. If no `.python-version` file is found, uv will exit with an /// error. /// /// See `uv help python` to view supported request formats. Pin(PythonPinArgs), /// Show the uv Python installation directory. /// /// By default, Python installations are stored in the uv data directory at /// `$XDG_DATA_HOME/uv/python` or `$HOME/.local/share/uv/python` on Unix and /// `%APPDATA%\uv\data\python` on Windows. /// /// The Python installation directory may be overridden with `$UV_PYTHON_INSTALL_DIR`. /// /// To view the directory where uv installs Python executables instead, use the `--bin` flag. /// The Python executable directory may be overridden with `$UV_PYTHON_BIN_DIR`. Note that /// Python executables are only installed when preview mode is enabled. Dir(PythonDirArgs), /// Uninstall Python versions. Uninstall(PythonUninstallArgs), /// Ensure that the Python executable directory is on the `PATH`. /// /// If the Python executable directory is not present on the `PATH`, uv will attempt to add it to /// the relevant shell configuration files. /// /// If the shell configuration files already include a blurb to add the executable directory to /// the path, but the directory is not present on the `PATH`, uv will exit with an error. /// /// The Python executable directory is determined according to the XDG standard and can be /// retrieved with `uv python dir --bin`. #[command(alias = "ensurepath")] UpdateShell, } #[derive(Args)] pub struct PythonListArgs { /// A Python request to filter by. /// /// See `uv help python` to view supported request formats. pub request: Option, /// List all Python versions, including old patch versions. /// /// By default, only the latest patch version is shown for each minor version. #[arg(long)] pub all_versions: bool, /// List Python downloads for all platforms. /// /// By default, only downloads for the current platform are shown. #[arg(long)] pub all_platforms: bool, /// List Python downloads for all architectures. /// /// By default, only downloads for the current architecture are shown. #[arg(long, alias = "all_architectures")] pub all_arches: bool, /// Only show installed Python versions. /// /// By default, installed distributions and available downloads for the current platform are shown. #[arg(long, conflicts_with("only_downloads"))] pub only_installed: bool, /// Only show available Python downloads. /// /// By default, installed distributions and available downloads for the current platform are shown. #[arg(long, conflicts_with("only_installed"))] pub only_downloads: bool, /// Show the URLs of available Python downloads. /// /// By default, these display as ``. #[arg(long)] pub show_urls: bool, /// Select the output format. #[arg(long, value_enum, default_value_t = PythonListFormat::default())] pub output_format: PythonListFormat, /// URL pointing to JSON of custom Python installations. #[arg(long)] pub python_downloads_json_url: Option, } #[derive(Args)] pub struct PythonDirArgs { /// Show the directory into which `uv python` will install Python executables. /// /// Note that this directory is only used when installing Python with preview mode enabled. /// /// The Python executable directory is determined according to the XDG standard and is derived /// from the following environment variables, in order of preference: /// /// - `$UV_PYTHON_BIN_DIR` /// - `$XDG_BIN_HOME` /// - `$XDG_DATA_HOME/../bin` /// - `$HOME/.local/bin` #[arg(long, verbatim_doc_comment)] pub bin: bool, } #[derive(Args)] pub struct PythonInstallArgs { /// The directory to store the Python installation in. /// /// If provided, `UV_PYTHON_INSTALL_DIR` will need to be set for subsequent operations for uv to /// discover the Python installation. /// /// See `uv python dir` to view the current Python installation directory. Defaults to /// `~/.local/share/uv/python`. #[arg(long, short, env = EnvVars::UV_PYTHON_INSTALL_DIR)] pub install_dir: Option, /// Install a Python executable into the `bin` directory. /// /// This is the default behavior. If this flag is provided explicitly, uv will error if the /// executable cannot be installed. /// /// This can also be set with `UV_PYTHON_INSTALL_BIN=1`. /// /// See `UV_PYTHON_BIN_DIR` to customize the target directory. #[arg(long, overrides_with("no_bin"), hide = true)] pub bin: bool, /// Do not install a Python executable into the `bin` directory. /// /// This can also be set with `UV_PYTHON_INSTALL_BIN=0`. #[arg(long, overrides_with("bin"), conflicts_with("default"))] pub no_bin: bool, /// Register the Python installation in the Windows registry. /// /// This is the default behavior on Windows. If this flag is provided explicitly, uv will error if the /// registry entry cannot be created. /// /// This can also be set with `UV_PYTHON_INSTALL_REGISTRY=1`. #[arg(long, overrides_with("no_registry"), hide = true)] pub registry: bool, /// Do not register the Python installation in the Windows registry. /// /// This can also be set with `UV_PYTHON_INSTALL_REGISTRY=0`. #[arg(long, overrides_with("registry"))] pub no_registry: bool, /// The Python version(s) to install. /// /// If not provided, the requested Python version(s) will be read from the `UV_PYTHON` /// environment variable then `.python-versions` or `.python-version` files. If none of the /// above are present, uv will check if it has installed any Python versions. If not, it will /// install the latest stable version of Python. /// /// See `uv help python` to view supported request formats. #[arg(env = EnvVars::UV_PYTHON)] pub targets: Vec, /// Set the URL to use as the source for downloading Python installations. /// /// The provided URL will replace /// `https://github.com/astral-sh/python-build-standalone/releases/download` in, e.g., /// `https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz`. /// /// Distributions can be read from a local directory by using the `file://` URL scheme. #[arg(long)] pub mirror: Option, /// Set the URL to use as the source for downloading PyPy installations. /// /// The provided URL will replace `https://downloads.python.org/pypy` in, e.g., /// `https://downloads.python.org/pypy/pypy3.8-v7.3.7-osx64.tar.bz2`. /// /// Distributions can be read from a local directory by using the `file://` URL scheme. #[arg(long)] pub pypy_mirror: Option, /// URL pointing to JSON of custom Python installations. #[arg(long)] pub python_downloads_json_url: Option, /// Reinstall the requested Python version, if it's already installed. /// /// By default, uv will exit successfully if the version is already /// installed. #[arg(long, short)] pub reinstall: bool, /// Replace existing Python executables during installation. /// /// By default, uv will refuse to replace executables that it does not manage. /// /// Implies `--reinstall`. #[arg(long, short)] pub force: bool, /// Upgrade existing Python installations to the latest patch version. /// /// By default, uv will not upgrade already-installed Python versions to newer patch releases. /// With `--upgrade`, uv will upgrade to the latest available patch version for the specified /// minor version(s). /// /// If the requested versions are not yet installed, uv will install them. /// /// This option is only supported for minor version requests, e.g., `3.12`; uv will exit with an /// error if a patch version, e.g., `3.12.2`, is requested. #[arg(long, short = 'U')] pub upgrade: bool, /// Use as the default Python version. /// /// By default, only a `python{major}.{minor}` executable is installed, e.g., `python3.10`. When /// the `--default` flag is used, `python{major}`, e.g., `python3`, and `python` executables are /// also installed. /// /// Alternative Python variants will still include their tag. For example, installing /// 3.13+freethreaded with `--default` will include in `python3t` and `pythont`, not `python3` /// and `python`. /// /// If multiple Python versions are requested, uv will exit with an error. #[arg(long, conflicts_with("no_bin"))] pub default: bool, } impl PythonInstallArgs { #[must_use] pub fn install_mirrors(&self) -> PythonInstallMirrors { PythonInstallMirrors { python_install_mirror: self.mirror.clone(), pypy_install_mirror: self.pypy_mirror.clone(), python_downloads_json_url: self.python_downloads_json_url.clone(), } } } #[derive(Args)] pub struct PythonUpgradeArgs { /// The directory Python installations are stored in. /// /// If provided, `UV_PYTHON_INSTALL_DIR` will need to be set for subsequent operations for uv to /// discover the Python installation. /// /// See `uv python dir` to view the current Python installation directory. Defaults to /// `~/.local/share/uv/python`. #[arg(long, short, env = EnvVars::UV_PYTHON_INSTALL_DIR)] pub install_dir: Option, /// The Python minor version(s) to upgrade. /// /// If no target version is provided, then uv will upgrade all managed CPython versions. #[arg(env = EnvVars::UV_PYTHON)] pub targets: Vec, /// Set the URL to use as the source for downloading Python installations. /// /// The provided URL will replace /// `https://github.com/astral-sh/python-build-standalone/releases/download` in, e.g., /// `https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz`. /// /// Distributions can be read from a local directory by using the `file://` URL scheme. #[arg(long)] pub mirror: Option, /// Set the URL to use as the source for downloading PyPy installations. /// /// The provided URL will replace `https://downloads.python.org/pypy` in, e.g., /// `https://downloads.python.org/pypy/pypy3.8-v7.3.7-osx64.tar.bz2`. /// /// Distributions can be read from a local directory by using the `file://` URL scheme. #[arg(long)] pub pypy_mirror: Option, /// Reinstall the latest Python patch, if it's already installed. /// /// By default, uv will exit successfully if the latest patch is already /// installed. #[arg(long, short)] pub reinstall: bool, /// URL pointing to JSON of custom Python installations. #[arg(long)] pub python_downloads_json_url: Option, } impl PythonUpgradeArgs { #[must_use] pub fn install_mirrors(&self) -> PythonInstallMirrors { PythonInstallMirrors { python_install_mirror: self.mirror.clone(), pypy_install_mirror: self.pypy_mirror.clone(), python_downloads_json_url: self.python_downloads_json_url.clone(), } } } #[derive(Args)] pub struct PythonUninstallArgs { /// The directory where the Python was installed. #[arg(long, short, env = EnvVars::UV_PYTHON_INSTALL_DIR)] pub install_dir: Option, /// The Python version(s) to uninstall. /// /// See `uv help python` to view supported request formats. #[arg(required = true)] pub targets: Vec, /// Uninstall all managed Python versions. #[arg(long, conflicts_with("targets"))] pub all: bool, } #[derive(Args)] pub struct PythonFindArgs { /// The Python request. /// /// See `uv help python` to view supported request formats. pub request: Option, /// Avoid discovering a project or workspace. /// /// Otherwise, when no request is provided, the Python requirement of a project in the current /// directory or parent directories will be used. #[arg(long, alias = "no_workspace")] pub no_project: bool, /// Only find system Python interpreters. /// /// By default, uv will report the first Python interpreter it would use, including those in an /// active virtual environment or a virtual environment in the current working directory or any /// parent directory. /// /// The `--system` option instructs uv to skip virtual environment Python interpreters and /// restrict its search to the system path. #[arg( long, env = EnvVars::UV_SYSTEM_PYTHON, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_system") )] pub system: bool, #[arg(long, overrides_with("system"), hide = true)] pub no_system: bool, /// Find the environment for a Python script, rather than the current project. #[arg( long, conflicts_with = "request", conflicts_with = "no_project", conflicts_with = "system", conflicts_with = "no_system" )] pub script: Option, /// Show the Python version that would be used instead of the path to the interpreter. #[arg(long)] pub show_version: bool, /// URL pointing to JSON of custom Python installations. #[arg(long)] pub python_downloads_json_url: Option, } #[derive(Args)] pub struct PythonPinArgs { /// The Python version request. /// /// uv supports more formats than other tools that read `.python-version` files, i.e., `pyenv`. /// If compatibility with those tools is needed, only use version numbers instead of complex /// requests such as `cpython@3.10`. /// /// If no request is provided, the currently pinned version will be shown. /// /// See `uv help python` to view supported request formats. pub request: Option, /// Write the resolved Python interpreter path instead of the request. /// /// Ensures that the exact same interpreter is used. /// /// This option is usually not safe to use when committing the `.python-version` file to version /// control. #[arg(long, overrides_with("resolved"))] pub resolved: bool, #[arg(long, overrides_with("no_resolved"), hide = true)] pub no_resolved: bool, /// Avoid validating the Python pin is compatible with the project or workspace. /// /// By default, a project or workspace is discovered in the current directory or any parent /// directory. If a workspace is found, the Python pin is validated against the workspace's /// `requires-python` constraint. #[arg(long, alias = "no-workspace")] pub no_project: bool, /// Update the global Python version pin. /// /// Writes the pinned Python version to a `.python-version` file in the uv user configuration /// directory: `XDG_CONFIG_HOME/uv` on Linux/macOS and `%APPDATA%/uv` on Windows. /// /// When a local Python version pin is not found in the working directory or an ancestor /// directory, this version will be used instead. #[arg(long)] pub global: bool, /// Remove the Python version pin. #[arg(long, conflicts_with = "request", conflicts_with = "resolved")] pub rm: bool, } #[derive(Args)] pub struct AuthLogoutArgs { /// The domain or URL of the service to logout from. pub service: Service, /// The username to logout. #[arg(long, short)] pub username: Option, /// The keyring provider to use for storage of credentials. /// /// Only `--keyring-provider native` is supported for `logout`, which uses the system keyring /// via an integration built into uv. #[arg( long, value_enum, env = EnvVars::UV_KEYRING_PROVIDER, )] pub keyring_provider: Option, } #[derive(Args)] pub struct AuthLoginArgs { /// The domain or URL of the service to log into. pub service: Service, /// The username to use for the service. #[arg(long, short, conflicts_with = "token")] pub username: Option, /// The password to use for the service. /// /// Use `-` to read the password from stdin. #[arg(long, conflicts_with = "token")] pub password: Option, /// The token to use for the service. /// /// The username will be set to `__token__`. /// /// Use `-` to read the token from stdin. #[arg(long, short, conflicts_with = "username", conflicts_with = "password")] pub token: Option, /// The keyring provider to use for storage of credentials. /// /// Only `--keyring-provider native` is supported for `login`, which uses the system keyring via /// an integration built into uv. #[arg( long, value_enum, env = EnvVars::UV_KEYRING_PROVIDER, )] pub keyring_provider: Option, } #[derive(Args)] pub struct AuthTokenArgs { /// The domain or URL of the service to lookup. pub service: Service, /// The username to lookup. #[arg(long, short)] pub username: Option, /// The keyring provider to use for reading credentials. #[arg( long, value_enum, env = EnvVars::UV_KEYRING_PROVIDER, )] pub keyring_provider: Option, } #[derive(Args)] pub struct AuthDirArgs { /// The domain or URL of the service to lookup. pub service: Option, } #[derive(Args)] pub struct AuthHelperArgs { #[command(subcommand)] pub command: AuthHelperCommand, /// The credential helper protocol to use #[arg(long, value_enum, required = true)] pub protocol: AuthHelperProtocol, } /// Credential helper protocols supported by uv #[derive(Debug, Copy, Clone, PartialEq, Eq, clap::ValueEnum)] pub enum AuthHelperProtocol { /// Bazel credential helper protocol as described in [the /// spec](https://github.com/bazelbuild/proposals/blob/main/designs/2022-06-07-bazel-credential-helpers.md) Bazel, } #[derive(Subcommand)] pub enum AuthHelperCommand { /// Retrieve credentials for a URI Get, } #[derive(Args)] pub struct GenerateShellCompletionArgs { /// The shell to generate the completion script for pub shell: clap_complete_command::Shell, // Hide unused global options. #[arg(long, short, hide = true)] pub no_cache: bool, #[arg(long, hide = true)] pub cache_dir: Option, #[arg(long, hide = true)] pub python_preference: Option, #[arg(long, hide = true)] pub no_python_downloads: bool, #[arg(long, short, action = clap::ArgAction::Count, conflicts_with = "verbose", hide = true)] pub quiet: u8, #[arg(long, short, action = clap::ArgAction::Count, conflicts_with = "quiet", hide = true)] pub verbose: u8, #[arg(long, conflicts_with = "no_color", hide = true)] pub color: Option, #[arg(long, hide = true)] pub native_tls: bool, #[arg(long, hide = true)] pub offline: bool, #[arg(long, hide = true)] pub no_progress: bool, #[arg(long, hide = true)] pub config_file: Option, #[arg(long, hide = true)] pub no_config: bool, #[arg(long, short, action = clap::ArgAction::HelpShort, hide = true)] pub help: Option, #[arg(short = 'V', long, hide = true)] pub version: bool, } #[derive(Args)] pub struct IndexArgs { /// The URLs to use when resolving dependencies, in addition to the default index. /// /// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local /// directory laid out in the same format. /// /// All indexes provided via this flag take priority over the index specified by /// `--default-index` (which defaults to PyPI). When multiple `--index` flags are provided, /// earlier values take priority. /// /// Index names are not supported as values. Relative paths must be disambiguated from index /// names with `./` or `../` on Unix or `.\\`, `..\\`, `./` or `../` on Windows. // // The nested Vec structure (`Vec>>`) is required for clap's // value parsing mechanism, which processes one value at a time, in order to handle // `UV_INDEX` the same way pip handles `PIP_EXTRA_INDEX_URL`. #[arg(long, env = EnvVars::UV_INDEX, value_parser = parse_indices, help_heading = "Index options")] pub index: Option>>>, /// The URL of the default package index (by default: ). /// /// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local /// directory laid out in the same format. /// /// The index given by this flag is given lower priority than all other indexes specified via /// the `--index` flag. #[arg(long, env = EnvVars::UV_DEFAULT_INDEX, value_parser = parse_default_index, help_heading = "Index options")] pub default_index: Option>, /// (Deprecated: use `--default-index` instead) The URL of the Python package index (by default: /// ). /// /// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local /// directory laid out in the same format. /// /// The index given by this flag is given lower priority than all other indexes specified via /// the `--extra-index-url` flag. #[arg(long, short, env = EnvVars::UV_INDEX_URL, value_parser = parse_index_url, help_heading = "Index options")] pub index_url: Option>, /// (Deprecated: use `--index` instead) Extra URLs of package indexes to use, in addition to /// `--index-url`. /// /// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local /// directory laid out in the same format. /// /// All indexes provided via this flag take priority over the index specified by `--index-url` /// (which defaults to PyPI). When multiple `--extra-index-url` flags are provided, earlier /// values take priority. #[arg(long, env = EnvVars::UV_EXTRA_INDEX_URL, value_delimiter = ' ', value_parser = parse_extra_index_url, help_heading = "Index options")] pub extra_index_url: Option>>, /// Locations to search for candidate distributions, in addition to those found in the registry /// indexes. /// /// If a path, the target must be a directory that contains packages as wheel files (`.whl`) or /// source distributions (e.g., `.tar.gz` or `.zip`) at the top level. /// /// If a URL, the page must contain a flat list of links to package files adhering to the /// formats described above. #[arg( long, short, env = EnvVars::UV_FIND_LINKS, value_delimiter = ',', value_parser = parse_find_links, help_heading = "Index options" )] pub find_links: Option>>, /// Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those /// provided via `--find-links`. #[arg(long, help_heading = "Index options")] pub no_index: bool, } #[derive(Args)] pub struct RefreshArgs { /// Refresh all cached data. #[arg( long, conflicts_with("offline"), overrides_with("no_refresh"), help_heading = "Cache options" )] pub refresh: bool, #[arg( long, conflicts_with("offline"), overrides_with("refresh"), hide = true, help_heading = "Cache options" )] pub no_refresh: bool, /// Refresh cached data for a specific package. #[arg(long, help_heading = "Cache options")] pub refresh_package: Vec, } #[derive(Args)] pub struct BuildOptionsArgs { /// Don't build source distributions. /// /// When enabled, resolving will not run arbitrary Python code. The cached wheels of /// already-built source distributions will be reused, but operations that require building /// distributions will exit with an error. #[arg( long, env = EnvVars::UV_NO_BUILD, overrides_with("build"), value_parser = clap::builder::BoolishValueParser::new(), help_heading = "Build options", )] pub no_build: bool, #[arg( long, overrides_with("no_build"), hide = true, help_heading = "Build options" )] pub build: bool, /// Don't build source distributions for a specific package. #[arg(long, help_heading = "Build options", env = EnvVars::UV_NO_BUILD_PACKAGE, value_delimiter = ' ')] pub no_build_package: Vec, /// Don't install pre-built wheels. /// /// The given packages will be built and installed from source. The resolver will still use /// pre-built wheels to extract package metadata, if available. #[arg( long, env = EnvVars::UV_NO_BINARY, overrides_with("binary"), value_parser = clap::builder::BoolishValueParser::new(), help_heading = "Build options" )] pub no_binary: bool, #[arg( long, overrides_with("no_binary"), hide = true, help_heading = "Build options" )] pub binary: bool, /// Don't install pre-built wheels for a specific package. #[arg(long, help_heading = "Build options", env = EnvVars::UV_NO_BINARY_PACKAGE, value_delimiter = ' ')] pub no_binary_package: Vec, } /// Arguments that are used by commands that need to install (but not resolve) packages. #[derive(Args)] pub struct InstallerArgs { #[command(flatten)] pub index_args: IndexArgs, /// Reinstall all packages, regardless of whether they're already installed. Implies /// `--refresh`. #[arg( long, alias = "force-reinstall", overrides_with("no_reinstall"), help_heading = "Installer options" )] pub reinstall: bool, #[arg( long, overrides_with("reinstall"), hide = true, help_heading = "Installer options" )] pub no_reinstall: bool, /// Reinstall a specific package, regardless of whether it's already installed. Implies /// `--refresh-package`. #[arg(long, help_heading = "Installer options")] pub reinstall_package: Vec, /// The strategy to use when resolving against multiple index URLs. /// /// By default, uv will stop at the first index on which a given package is available, and limit /// resolutions to those present on that first index (`first-index`). This prevents "dependency /// confusion" attacks, whereby an attacker can upload a malicious package under the same name /// to an alternate index. #[arg( long, value_enum, env = EnvVars::UV_INDEX_STRATEGY, help_heading = "Index options" )] pub index_strategy: Option, /// Attempt to use `keyring` for authentication for index URLs. /// /// At present, only `--keyring-provider subprocess` is supported, which configures uv to use /// the `keyring` CLI to handle authentication. /// /// Defaults to `disabled`. #[arg( long, value_enum, env = EnvVars::UV_KEYRING_PROVIDER, help_heading = "Index options" )] pub keyring_provider: Option, /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. #[arg( long, short = 'C', alias = "config-settings", help_heading = "Build options" )] pub config_setting: Option>, /// Settings to pass to the PEP 517 build backend for a specific package, specified as `PACKAGE:KEY=VALUE` pairs. #[arg( long, alias = "config-settings-package", help_heading = "Build options" )] pub config_settings_package: Option>, /// Disable isolation when building source distributions. /// /// Assumes that build dependencies specified by PEP 518 are already installed. #[arg( long, overrides_with("build_isolation"), help_heading = "Build options", env = EnvVars::UV_NO_BUILD_ISOLATION, value_parser = clap::builder::BoolishValueParser::new(), )] pub no_build_isolation: bool, #[arg( long, overrides_with("no_build_isolation"), hide = true, help_heading = "Build options" )] pub build_isolation: bool, /// Limit candidate packages to those that were uploaded prior to the given date. /// /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format /// (e.g., `2006-12-02`) resolved based on your system's configured time zone, a "friendly" /// duration (e.g., `24 hours`, `1 week`, `30 days`), or an ISO 8601 duration (e.g., `PT24H`, /// `P7D`, `P30D`). /// /// Durations do not respect semantics of the local time zone and are always resolved to a fixed /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). /// Calendar units such as months and years are not allowed. #[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")] pub exclude_newer: Option, /// Limit candidate packages for specific packages to those that were uploaded prior to the /// given date. /// /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 /// timestamp (e.g., `2006-12-02T02:07:43Z`), a local date in the same format (e.g., /// `2006-12-02`) resolved based on your system's configured time zone, a "friendly" duration /// (e.g., `24 hours`, `1 week`, `30 days`), or an ISO 8601 duration (e.g., `PT24H`, `P7D`, /// `P30D`). /// /// Durations do not respect semantics of the local time zone and are always resolved to a fixed /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). /// Calendar units such as months and years are not allowed. /// /// Can be provided multiple times for different packages. #[arg(long, help_heading = "Resolver options")] pub exclude_newer_package: Option>, /// The method to use when installing packages from the global cache. /// /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and /// Windows. /// /// WARNING: The use of symlink link mode is discouraged, as they create tight coupling between /// the cache and the target environment. For example, clearing the cache (`uv cache clean`) /// will break all installed packages by way of removing the underlying source files. Use /// symlinks with caution. #[arg( long, value_enum, env = EnvVars::UV_LINK_MODE, help_heading = "Installer options" )] pub link_mode: Option, /// Compile Python files to bytecode after installation. /// /// By default, uv does not compile Python (`.py`) files to bytecode (`__pycache__/*.pyc`); /// instead, compilation is performed lazily the first time a module is imported. For use-cases /// in which start time is critical, such as CLI applications and Docker containers, this option /// can be enabled to trade longer installation times for faster start times. /// /// When enabled, uv will process the entire site-packages directory (including packages that /// are not being modified by the current operation) for consistency. Like pip, it will also /// ignore errors. #[arg( long, alias = "compile", overrides_with("no_compile_bytecode"), help_heading = "Installer options", env = EnvVars::UV_COMPILE_BYTECODE, value_parser = clap::builder::BoolishValueParser::new(), )] pub compile_bytecode: bool, #[arg( long, alias = "no-compile", overrides_with("compile_bytecode"), hide = true, help_heading = "Installer options" )] pub no_compile_bytecode: bool, /// Ignore the `tool.uv.sources` table when resolving dependencies. Used to lock against the /// standards-compliant, publishable package metadata, as opposed to using any workspace, Git, /// URL, or local path sources. #[arg( long, env = EnvVars::UV_NO_SOURCES, value_parser = clap::builder::BoolishValueParser::new(), help_heading = "Resolver options" )] pub no_sources: bool, } /// Arguments that are used by commands that need to resolve (but not install) packages. #[derive(Args)] pub struct ResolverArgs { #[command(flatten)] pub index_args: IndexArgs, /// Allow package upgrades, ignoring pinned versions in any existing output file. Implies /// `--refresh`. #[arg( long, short = 'U', overrides_with("no_upgrade"), help_heading = "Resolver options" )] pub upgrade: bool, #[arg( long, overrides_with("upgrade"), hide = true, help_heading = "Resolver options" )] pub no_upgrade: bool, /// Allow upgrades for a specific package, ignoring pinned versions in any existing output /// file. Implies `--refresh-package`. #[arg(long, short = 'P', help_heading = "Resolver options")] pub upgrade_package: Vec>, /// The strategy to use when resolving against multiple index URLs. /// /// By default, uv will stop at the first index on which a given package is available, and limit /// resolutions to those present on that first index (`first-index`). This prevents "dependency /// confusion" attacks, whereby an attacker can upload a malicious package under the same name /// to an alternate index. #[arg( long, value_enum, env = EnvVars::UV_INDEX_STRATEGY, help_heading = "Index options" )] pub index_strategy: Option, /// Attempt to use `keyring` for authentication for index URLs. /// /// At present, only `--keyring-provider subprocess` is supported, which configures uv to use /// the `keyring` CLI to handle authentication. /// /// Defaults to `disabled`. #[arg( long, value_enum, env = EnvVars::UV_KEYRING_PROVIDER, help_heading = "Index options" )] pub keyring_provider: Option, /// The strategy to use when selecting between the different compatible versions for a given /// package requirement. /// /// By default, uv will use the latest compatible version of each package (`highest`). #[arg( long, value_enum, env = EnvVars::UV_RESOLUTION, help_heading = "Resolver options" )] pub resolution: Option, /// The strategy to use when considering pre-release versions. /// /// By default, uv will accept pre-releases for packages that _only_ publish pre-releases, along /// with first-party requirements that contain an explicit pre-release marker in the declared /// specifiers (`if-necessary-or-explicit`). #[arg( long, value_enum, env = EnvVars::UV_PRERELEASE, help_heading = "Resolver options" )] pub prerelease: Option, #[arg(long, hide = true, help_heading = "Resolver options")] pub pre: bool, /// The strategy to use when selecting multiple versions of a given package across Python /// versions and platforms. /// /// By default, uv will optimize for selecting the latest version of each package for each /// supported Python version (`requires-python`), while minimizing the number of selected /// versions across platforms. /// /// Under `fewest`, uv will minimize the number of selected versions for each package, /// preferring older versions that are compatible with a wider range of supported Python /// versions or platforms. #[arg( long, value_enum, env = EnvVars::UV_FORK_STRATEGY, help_heading = "Resolver options" )] pub fork_strategy: Option, /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. #[arg( long, short = 'C', alias = "config-settings", help_heading = "Build options" )] pub config_setting: Option>, /// Settings to pass to the PEP 517 build backend for a specific package, specified as `PACKAGE:KEY=VALUE` pairs. #[arg( long, alias = "config-settings-package", help_heading = "Build options" )] pub config_settings_package: Option>, /// Disable isolation when building source distributions. /// /// Assumes that build dependencies specified by PEP 518 are already installed. #[arg( long, overrides_with("build_isolation"), help_heading = "Build options", env = EnvVars::UV_NO_BUILD_ISOLATION, value_parser = clap::builder::BoolishValueParser::new(), )] pub no_build_isolation: bool, /// Disable isolation when building source distributions for a specific package. /// /// Assumes that the packages' build dependencies specified by PEP 518 are already installed. #[arg(long, help_heading = "Build options")] pub no_build_isolation_package: Vec, #[arg( long, overrides_with("no_build_isolation"), hide = true, help_heading = "Build options" )] pub build_isolation: bool, /// Limit candidate packages to those that were uploaded prior to the given date. /// /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format /// (e.g., `2006-12-02`) resolved based on your system's configured time zone, a "friendly" /// duration (e.g., `24 hours`, `1 week`, `30 days`), or an ISO 8601 duration (e.g., `PT24H`, /// `P7D`, `P30D`). /// /// Durations do not respect semantics of the local time zone and are always resolved to a fixed /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). /// Calendar units such as months and years are not allowed. #[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")] pub exclude_newer: Option, /// Limit candidate packages for specific packages to those that were uploaded prior to the /// given date. /// /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 /// timestamp (e.g., `2006-12-02T02:07:43Z`), a local date in the same format (e.g., /// `2006-12-02`) resolved based on your system's configured time zone, a "friendly" duration /// (e.g., `24 hours`, `1 week`, `30 days`), or an ISO 8601 duration (e.g., `PT24H`, `P7D`, /// `P30D`). /// /// Durations do not respect semantics of the local time zone and are always resolved to a fixed /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). /// Calendar units such as months and years are not allowed. /// /// Can be provided multiple times for different packages. #[arg(long, help_heading = "Resolver options")] pub exclude_newer_package: Option>, /// The method to use when installing packages from the global cache. /// /// This option is only used when building source distributions. /// /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and /// Windows. /// /// WARNING: The use of symlink link mode is discouraged, as they create tight coupling between /// the cache and the target environment. For example, clearing the cache (`uv cache clean`) /// will break all installed packages by way of removing the underlying source files. Use /// symlinks with caution. #[arg( long, value_enum, env = EnvVars::UV_LINK_MODE, help_heading = "Installer options" )] pub link_mode: Option, /// Ignore the `tool.uv.sources` table when resolving dependencies. Used to lock against the /// standards-compliant, publishable package metadata, as opposed to using any workspace, Git, /// URL, or local path sources. #[arg( long, env = EnvVars::UV_NO_SOURCES, value_parser = clap::builder::BoolishValueParser::new(), help_heading = "Resolver options", )] pub no_sources: bool, } /// Arguments that are used by commands that need to resolve and install packages. #[derive(Args)] pub struct ResolverInstallerArgs { #[command(flatten)] pub index_args: IndexArgs, /// Allow package upgrades, ignoring pinned versions in any existing output file. Implies /// `--refresh`. #[arg( long, short = 'U', overrides_with("no_upgrade"), help_heading = "Resolver options" )] pub upgrade: bool, #[arg( long, overrides_with("upgrade"), hide = true, help_heading = "Resolver options" )] pub no_upgrade: bool, /// Allow upgrades for a specific package, ignoring pinned versions in any existing output file. /// Implies `--refresh-package`. #[arg(long, short = 'P', help_heading = "Resolver options")] pub upgrade_package: Vec>, /// Reinstall all packages, regardless of whether they're already installed. Implies /// `--refresh`. #[arg( long, alias = "force-reinstall", overrides_with("no_reinstall"), help_heading = "Installer options" )] pub reinstall: bool, #[arg( long, overrides_with("reinstall"), hide = true, help_heading = "Installer options" )] pub no_reinstall: bool, /// Reinstall a specific package, regardless of whether it's already installed. Implies /// `--refresh-package`. #[arg(long, help_heading = "Installer options")] pub reinstall_package: Vec, /// The strategy to use when resolving against multiple index URLs. /// /// By default, uv will stop at the first index on which a given package is available, and limit /// resolutions to those present on that first index (`first-index`). This prevents "dependency /// confusion" attacks, whereby an attacker can upload a malicious package under the same name /// to an alternate index. #[arg( long, value_enum, env = EnvVars::UV_INDEX_STRATEGY, help_heading = "Index options" )] pub index_strategy: Option, /// Attempt to use `keyring` for authentication for index URLs. /// /// At present, only `--keyring-provider subprocess` is supported, which configures uv to use /// the `keyring` CLI to handle authentication. /// /// Defaults to `disabled`. #[arg( long, value_enum, env = EnvVars::UV_KEYRING_PROVIDER, help_heading = "Index options" )] pub keyring_provider: Option, /// The strategy to use when selecting between the different compatible versions for a given /// package requirement. /// /// By default, uv will use the latest compatible version of each package (`highest`). #[arg( long, value_enum, env = EnvVars::UV_RESOLUTION, help_heading = "Resolver options" )] pub resolution: Option, /// The strategy to use when considering pre-release versions. /// /// By default, uv will accept pre-releases for packages that _only_ publish pre-releases, along /// with first-party requirements that contain an explicit pre-release marker in the declared /// specifiers (`if-necessary-or-explicit`). #[arg( long, value_enum, env = EnvVars::UV_PRERELEASE, help_heading = "Resolver options" )] pub prerelease: Option, #[arg(long, hide = true)] pub pre: bool, /// The strategy to use when selecting multiple versions of a given package across Python /// versions and platforms. /// /// By default, uv will optimize for selecting the latest version of each package for each /// supported Python version (`requires-python`), while minimizing the number of selected /// versions across platforms. /// /// Under `fewest`, uv will minimize the number of selected versions for each package, /// preferring older versions that are compatible with a wider range of supported Python /// versions or platforms. #[arg( long, value_enum, env = EnvVars::UV_FORK_STRATEGY, help_heading = "Resolver options" )] pub fork_strategy: Option, /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. #[arg( long, short = 'C', alias = "config-settings", help_heading = "Build options" )] pub config_setting: Option>, /// Settings to pass to the PEP 517 build backend for a specific package, specified as `PACKAGE:KEY=VALUE` pairs. #[arg( long, alias = "config-settings-package", help_heading = "Build options" )] pub config_settings_package: Option>, /// Disable isolation when building source distributions. /// /// Assumes that build dependencies specified by PEP 518 are already installed. #[arg( long, overrides_with("build_isolation"), help_heading = "Build options", env = EnvVars::UV_NO_BUILD_ISOLATION, value_parser = clap::builder::BoolishValueParser::new(), )] pub no_build_isolation: bool, /// Disable isolation when building source distributions for a specific package. /// /// Assumes that the packages' build dependencies specified by PEP 518 are already installed. #[arg(long, help_heading = "Build options")] pub no_build_isolation_package: Vec, #[arg( long, overrides_with("no_build_isolation"), hide = true, help_heading = "Build options" )] pub build_isolation: bool, /// Limit candidate packages to those that were uploaded prior to the given date. /// /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format /// (e.g., `2006-12-02`) resolved based on your system's configured time zone, a "friendly" /// duration (e.g., `24 hours`, `1 week`, `30 days`), or an ISO 8601 duration (e.g., `PT24H`, /// `P7D`, `P30D`). /// /// Durations do not respect semantics of the local time zone and are always resolved to a fixed /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). /// Calendar units such as months and years are not allowed. #[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")] pub exclude_newer: Option, /// Limit candidate packages for specific packages to those that were uploaded prior to the /// given date. /// /// Accepts package-date pairs in the format `PACKAGE=DATE`, where `DATE` is an RFC 3339 /// timestamp (e.g., `2006-12-02T02:07:43Z`), a local date in the same format (e.g., /// `2006-12-02`) resolved based on your system's configured time zone, a "friendly" duration /// (e.g., `24 hours`, `1 week`, `30 days`), or an ISO 8601 duration (e.g., `PT24H`, `P7D`, /// `P30D`). /// /// Durations do not respect semantics of the local time zone and are always resolved to a fixed /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). /// Calendar units such as months and years are not allowed. /// /// Can be provided multiple times for different packages. #[arg(long, help_heading = "Resolver options")] pub exclude_newer_package: Option>, /// The method to use when installing packages from the global cache. /// /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and /// Windows. /// /// WARNING: The use of symlink link mode is discouraged, as they create tight coupling between /// the cache and the target environment. For example, clearing the cache (`uv cache clean`) /// will break all installed packages by way of removing the underlying source files. Use /// symlinks with caution. #[arg( long, value_enum, env = EnvVars::UV_LINK_MODE, help_heading = "Installer options" )] pub link_mode: Option, /// Compile Python files to bytecode after installation. /// /// By default, uv does not compile Python (`.py`) files to bytecode (`__pycache__/*.pyc`); /// instead, compilation is performed lazily the first time a module is imported. For use-cases /// in which start time is critical, such as CLI applications and Docker containers, this option /// can be enabled to trade longer installation times for faster start times. /// /// When enabled, uv will process the entire site-packages directory (including packages that /// are not being modified by the current operation) for consistency. Like pip, it will also /// ignore errors. #[arg( long, alias = "compile", overrides_with("no_compile_bytecode"), help_heading = "Installer options", env = EnvVars::UV_COMPILE_BYTECODE, value_parser = clap::builder::BoolishValueParser::new(), )] pub compile_bytecode: bool, #[arg( long, alias = "no-compile", overrides_with("compile_bytecode"), hide = true, help_heading = "Installer options" )] pub no_compile_bytecode: bool, /// Ignore the `tool.uv.sources` table when resolving dependencies. Used to lock against the /// standards-compliant, publishable package metadata, as opposed to using any workspace, Git, /// URL, or local path sources. #[arg( long, env = EnvVars::UV_NO_SOURCES, value_parser = clap::builder::BoolishValueParser::new(), help_heading = "Resolver options", )] pub no_sources: bool, } /// Arguments that are used by commands that need to fetch from the Simple API. #[derive(Args)] pub struct FetchArgs { #[command(flatten)] pub index_args: IndexArgs, /// The strategy to use when resolving against multiple index URLs. /// /// By default, uv will stop at the first index on which a given package is available, and limit /// resolutions to those present on that first index (`first-index`). This prevents "dependency /// confusion" attacks, whereby an attacker can upload a malicious package under the same name /// to an alternate index. #[arg( long, value_enum, env = EnvVars::UV_INDEX_STRATEGY, help_heading = "Index options" )] pub index_strategy: Option, /// Attempt to use `keyring` for authentication for index URLs. /// /// At present, only `--keyring-provider subprocess` is supported, which configures uv to use /// the `keyring` CLI to handle authentication. /// /// Defaults to `disabled`. #[arg( long, value_enum, env = EnvVars::UV_KEYRING_PROVIDER, help_heading = "Index options" )] pub keyring_provider: Option, /// Limit candidate packages to those that were uploaded prior to the given date. /// /// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format /// (e.g., `2006-12-02`) resolved based on your system's configured time zone, a "friendly" /// duration (e.g., `24 hours`, `1 week`, `30 days`), or an ISO 8601 duration (e.g., `PT24H`, /// `P7D`, `P30D`). /// /// Durations do not respect semantics of the local time zone and are always resolved to a fixed /// number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). /// Calendar units such as months and years are not allowed. #[arg(long, env = EnvVars::UV_EXCLUDE_NEWER, help_heading = "Resolver options")] pub exclude_newer: Option, } #[derive(Args)] pub struct DisplayTreeArgs { /// Maximum display depth of the dependency tree #[arg(long, short, default_value_t = 255)] pub depth: u8, /// Prune the given package from the display of the dependency tree. #[arg(long)] pub prune: Vec, /// Display only the specified packages. #[arg(long)] pub package: Vec, /// Do not de-duplicate repeated dependencies. Usually, when a package has already displayed its /// dependencies, further occurrences will not re-display its dependencies, and will include a /// (*) to indicate it has already been shown. This flag will cause those duplicates to be /// repeated. #[arg(long)] pub no_dedupe: bool, /// Show the reverse dependencies for the given package. This flag will invert the tree and /// display the packages that depend on the given package. #[arg(long, alias = "reverse")] pub invert: bool, /// Show the latest available version of each package in the tree. #[arg(long)] pub outdated: bool, /// Show compressed wheel sizes for packages in the tree. #[arg(long)] pub show_sizes: bool, } #[derive(Args, Debug)] pub struct PublishArgs { /// Paths to the files to upload. Accepts glob expressions. /// /// Defaults to the `dist` directory. Selects only wheels and source distributions /// and their attestations, while ignoring other files. #[arg(default_value = "dist/*")] pub files: Vec, /// The name of an index in the configuration to use for publishing. /// /// The index must have a `publish-url` setting, for example: /// /// ```toml /// [[tool.uv.index]] /// name = "pypi" /// url = "https://pypi.org/simple" /// publish-url = "https://upload.pypi.org/legacy/" /// ``` /// /// The index `url` will be used to check for existing files to skip duplicate uploads. /// /// With these settings, the following two calls are equivalent: /// /// ```shell /// uv publish --index pypi /// uv publish --publish-url https://upload.pypi.org/legacy/ --check-url https://pypi.org/simple /// ``` #[arg( long, verbatim_doc_comment, env = EnvVars::UV_PUBLISH_INDEX, conflicts_with = "publish_url", conflicts_with = "check_url" )] pub index: Option, /// The username for the upload. #[arg(short, long, env = EnvVars::UV_PUBLISH_USERNAME)] pub username: Option, /// The password for the upload. #[arg(short, long, env = EnvVars::UV_PUBLISH_PASSWORD)] pub password: Option, /// The token for the upload. /// /// Using a token is equivalent to passing `__token__` as `--username` and the token as /// `--password` password. #[arg( short, long, env = EnvVars::UV_PUBLISH_TOKEN, conflicts_with = "username", conflicts_with = "password" )] pub token: Option, /// Configure trusted publishing. /// /// By default, uv checks for trusted publishing when running in a supported environment, but /// ignores it if it isn't configured. /// /// uv's supported environments for trusted publishing include GitHub Actions and GitLab CI/CD. #[arg(long)] pub trusted_publishing: Option, /// Attempt to use `keyring` for authentication for remote requirements files. /// /// At present, only `--keyring-provider subprocess` is supported, which configures uv to use /// the `keyring` CLI to handle authentication. /// /// Defaults to `disabled`. #[arg(long, value_enum, env = EnvVars::UV_KEYRING_PROVIDER)] pub keyring_provider: Option, /// The URL of the upload endpoint (not the index URL). /// /// Note that there are typically different URLs for index access (e.g., `https:://.../simple`) /// and index upload. /// /// Defaults to PyPI's publish URL (). #[arg(long, env = EnvVars::UV_PUBLISH_URL)] pub publish_url: Option, /// Check an index URL for existing files to skip duplicate uploads. /// /// This option allows retrying publishing that failed after only some, but not all files have /// been uploaded, and handles errors due to parallel uploads of the same file. /// /// Before uploading, the index is checked. If the exact same file already exists in the index, /// the file will not be uploaded. If an error occurred during the upload, the index is checked /// again, to handle cases where the identical file was uploaded twice in parallel. /// /// The exact behavior will vary based on the index. When uploading to PyPI, uploading the same /// file succeeds even without `--check-url`, while most other indexes error. When uploading to /// pyx, the index URL can be inferred automatically from the publish URL. /// /// The index must provide one of the supported hashes (SHA-256, SHA-384, or SHA-512). #[arg(long, env = EnvVars::UV_PUBLISH_CHECK_URL)] pub check_url: Option, #[arg(long, hide = true)] pub skip_existing: bool, /// Perform a dry run without uploading files. /// /// When enabled, the command will check for existing files if `--check-url` is provided, /// and will perform validation against the index if supported, but will not upload any files. #[arg(long)] pub dry_run: bool, /// Do not upload attestations for the published files. /// /// By default, uv attempts to upload matching PEP 740 attestations with each distribution /// that is published. #[arg(long, env = EnvVars::UV_PUBLISH_NO_ATTESTATIONS)] pub no_attestations: bool, } #[derive(Args)] pub struct WorkspaceNamespace { #[command(subcommand)] pub command: WorkspaceCommand, } #[derive(Subcommand)] pub enum WorkspaceCommand { /// View metadata about the current workspace. /// /// The output of this command is not yet stable. Metadata(MetadataArgs), /// Display the path of a workspace member. /// /// By default, the path to the workspace root directory is displayed. /// The `--package` option can be used to display the path to a workspace member instead. /// /// If used outside of a workspace, i.e., if a `pyproject.toml` cannot be found, uv will exit with an error. Dir(WorkspaceDirArgs), /// List the members of a workspace. /// /// Displays newline separated names of workspace members. #[command(hide = true)] List(WorkspaceListArgs), } #[derive(Args, Debug)] pub struct MetadataArgs; #[derive(Args, Debug)] pub struct WorkspaceDirArgs { /// Display the path to a specific package in the workspace. #[arg(long)] pub package: Option, } #[derive(Args, Debug)] pub struct WorkspaceListArgs { /// Show paths instead of names. #[arg(long)] pub paths: bool, } /// See [PEP 517](https://peps.python.org/pep-0517/) and /// [PEP 660](https://peps.python.org/pep-0660/) for specifications of the parameters. #[derive(Subcommand)] pub enum BuildBackendCommand { /// PEP 517 hook `build_sdist`. BuildSdist { sdist_directory: PathBuf }, /// PEP 517 hook `build_wheel`. BuildWheel { wheel_directory: PathBuf, #[arg(long)] metadata_directory: Option, }, /// PEP 660 hook `build_editable`. BuildEditable { wheel_directory: PathBuf, #[arg(long)] metadata_directory: Option, }, /// PEP 517 hook `get_requires_for_build_sdist`. GetRequiresForBuildSdist, /// PEP 517 hook `get_requires_for_build_wheel`. GetRequiresForBuildWheel, /// PEP 517 hook `prepare_metadata_for_build_wheel`. PrepareMetadataForBuildWheel { wheel_directory: PathBuf }, /// PEP 660 hook `get_requires_for_build_editable`. GetRequiresForBuildEditable, /// PEP 660 hook `prepare_metadata_for_build_editable`. PrepareMetadataForBuildEditable { wheel_directory: PathBuf }, } uv-0.9.17+ds1/crates/uv-cli/src/options.rs000066400000000000000000000377311520155276700202620ustar00rootroot00000000000000use anstream::eprintln; use uv_cache::Refresh; use uv_configuration::{BuildIsolation, Reinstall, Upgrade}; use uv_distribution_types::{ConfigSettings, PackageConfigSettings, Requirement}; use uv_resolver::{ExcludeNewer, ExcludeNewerPackage, PrereleaseMode}; use uv_settings::{Combine, PipOptions, ResolverInstallerOptions, ResolverOptions}; use uv_warnings::owo_colors::OwoColorize; use crate::{ BuildOptionsArgs, FetchArgs, IndexArgs, InstallerArgs, Maybe, RefreshArgs, ResolverArgs, ResolverInstallerArgs, }; /// Given a boolean flag pair (like `--upgrade` and `--no-upgrade`), resolve the value of the flag. pub fn flag(yes: bool, no: bool, name: &str) -> Option { match (yes, no) { (true, false) => Some(true), (false, true) => Some(false), (false, false) => None, (..) => { eprintln!( "{}{} `{}` and `{}` cannot be used together. \ Boolean flags on different levels are currently not supported \ (https://github.com/clap-rs/clap/issues/6049)", "error".bold().red(), ":".bold(), format!("--{name}").green(), format!("--no-{name}").green(), ); // No error forwarding since should eventually be solved on the clap side. #[allow(clippy::exit)] { std::process::exit(2); } } } } impl From for Refresh { fn from(value: RefreshArgs) -> Self { let RefreshArgs { refresh, no_refresh, refresh_package, } = value; Self::from_args(flag(refresh, no_refresh, "no-refresh"), refresh_package) } } impl From for PipOptions { fn from(args: ResolverArgs) -> Self { let ResolverArgs { index_args, upgrade, no_upgrade, upgrade_package, index_strategy, keyring_provider, resolution, prerelease, pre, fork_strategy, config_setting, config_settings_package, no_build_isolation, no_build_isolation_package, build_isolation, exclude_newer, link_mode, no_sources, exclude_newer_package, } = args; Self { upgrade: flag(upgrade, no_upgrade, "no-upgrade"), upgrade_package: Some(upgrade_package), index_strategy, keyring_provider, resolution, fork_strategy, prerelease: if pre { Some(PrereleaseMode::Allow) } else { prerelease }, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), config_settings_package: config_settings_package.map(|config_settings| { config_settings .into_iter() .collect::() }), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation_package: Some(no_build_isolation_package), exclude_newer, exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter), link_mode, no_sources: if no_sources { Some(true) } else { None }, ..Self::from(index_args) } } } impl From for PipOptions { fn from(args: InstallerArgs) -> Self { let InstallerArgs { index_args, reinstall, no_reinstall, reinstall_package, index_strategy, keyring_provider, config_setting, config_settings_package, no_build_isolation, build_isolation, exclude_newer, link_mode, compile_bytecode, no_compile_bytecode, no_sources, exclude_newer_package, } = args; Self { reinstall: flag(reinstall, no_reinstall, "reinstall"), reinstall_package: Some(reinstall_package), index_strategy, keyring_provider, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), config_settings_package: config_settings_package.map(|config_settings| { config_settings .into_iter() .collect::() }), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), exclude_newer, exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter), link_mode, compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"), no_sources: if no_sources { Some(true) } else { None }, ..Self::from(index_args) } } } impl From for PipOptions { fn from(args: ResolverInstallerArgs) -> Self { let ResolverInstallerArgs { index_args, upgrade, no_upgrade, upgrade_package, reinstall, no_reinstall, reinstall_package, index_strategy, keyring_provider, resolution, prerelease, pre, fork_strategy, config_setting, config_settings_package, no_build_isolation, no_build_isolation_package, build_isolation, exclude_newer, link_mode, compile_bytecode, no_compile_bytecode, no_sources, exclude_newer_package, } = args; Self { upgrade: flag(upgrade, no_upgrade, "upgrade"), upgrade_package: Some(upgrade_package), reinstall: flag(reinstall, no_reinstall, "reinstall"), reinstall_package: Some(reinstall_package), index_strategy, keyring_provider, resolution, prerelease: if pre { Some(PrereleaseMode::Allow) } else { prerelease }, fork_strategy, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), config_settings_package: config_settings_package.map(|config_settings| { config_settings .into_iter() .collect::() }), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation_package: Some(no_build_isolation_package), exclude_newer, exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter), link_mode, compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"), no_sources: if no_sources { Some(true) } else { None }, ..Self::from(index_args) } } } impl From for PipOptions { fn from(args: FetchArgs) -> Self { let FetchArgs { index_args, index_strategy, keyring_provider, exclude_newer, } = args; Self { index_strategy, keyring_provider, exclude_newer, ..Self::from(index_args) } } } impl From for PipOptions { fn from(args: IndexArgs) -> Self { let IndexArgs { default_index, index, index_url, extra_index_url, no_index, find_links, } = args; Self { index: default_index .and_then(Maybe::into_option) .map(|default_index| vec![default_index]) .combine(index.map(|index| { index .iter() .flat_map(std::clone::Clone::clone) .filter_map(Maybe::into_option) .collect() })), index_url: index_url.and_then(Maybe::into_option), extra_index_url: extra_index_url.map(|extra_index_urls| { extra_index_urls .into_iter() .filter_map(Maybe::into_option) .collect() }), no_index: if no_index { Some(true) } else { None }, find_links: find_links.map(|find_links| { find_links .into_iter() .filter_map(Maybe::into_option) .collect() }), ..Self::default() } } } /// Construct the [`ResolverOptions`] from the [`ResolverArgs`] and [`BuildOptionsArgs`]. pub fn resolver_options( resolver_args: ResolverArgs, build_args: BuildOptionsArgs, ) -> ResolverOptions { let ResolverArgs { index_args, upgrade, no_upgrade, upgrade_package, index_strategy, keyring_provider, resolution, prerelease, pre, fork_strategy, config_setting, config_settings_package, no_build_isolation, no_build_isolation_package, build_isolation, exclude_newer, link_mode, no_sources, exclude_newer_package, } = resolver_args; let BuildOptionsArgs { no_build, build, no_build_package, no_binary, binary, no_binary_package, } = build_args; ResolverOptions { index: index_args .default_index .and_then(Maybe::into_option) .map(|default_index| vec![default_index]) .combine(index_args.index.map(|index| { index .into_iter() .flat_map(|v| v.clone()) .filter_map(Maybe::into_option) .collect() })), index_url: index_args.index_url.and_then(Maybe::into_option), extra_index_url: index_args.extra_index_url.map(|extra_index_url| { extra_index_url .into_iter() .filter_map(Maybe::into_option) .collect() }), no_index: if index_args.no_index { Some(true) } else { None }, find_links: index_args.find_links.map(|find_links| { find_links .into_iter() .filter_map(Maybe::into_option) .collect() }), upgrade: Upgrade::from_args( flag(upgrade, no_upgrade, "no-upgrade"), upgrade_package.into_iter().map(Requirement::from).collect(), ), index_strategy, keyring_provider, resolution, prerelease: if pre { Some(PrereleaseMode::Allow) } else { prerelease }, fork_strategy, dependency_metadata: None, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), config_settings_package: config_settings_package.map(|config_settings| { config_settings .into_iter() .collect::() }), build_isolation: BuildIsolation::from_args( flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation_package, ), extra_build_dependencies: None, extra_build_variables: None, exclude_newer: ExcludeNewer::from_args( exclude_newer, exclude_newer_package.unwrap_or_default(), ), link_mode, no_build: flag(no_build, build, "build"), no_build_package: Some(no_build_package), no_binary: flag(no_binary, binary, "binary"), no_binary_package: Some(no_binary_package), no_sources: if no_sources { Some(true) } else { None }, } } /// Construct the [`ResolverInstallerOptions`] from the [`ResolverInstallerArgs`] and [`BuildOptionsArgs`]. pub fn resolver_installer_options( resolver_installer_args: ResolverInstallerArgs, build_args: BuildOptionsArgs, ) -> ResolverInstallerOptions { let ResolverInstallerArgs { index_args, upgrade, no_upgrade, upgrade_package, reinstall, no_reinstall, reinstall_package, index_strategy, keyring_provider, resolution, prerelease, pre, fork_strategy, config_setting, config_settings_package, no_build_isolation, no_build_isolation_package, build_isolation, exclude_newer, exclude_newer_package, link_mode, compile_bytecode, no_compile_bytecode, no_sources, } = resolver_installer_args; let BuildOptionsArgs { no_build, build, no_build_package, no_binary, binary, no_binary_package, } = build_args; let default_index = index_args .default_index .and_then(Maybe::into_option) .map(|default_index| vec![default_index]); let index = index_args.index.map(|index| { index .into_iter() .flat_map(|v| v.clone()) .filter_map(Maybe::into_option) .collect() }); ResolverInstallerOptions { index: default_index.combine(index), index_url: index_args.index_url.and_then(Maybe::into_option), extra_index_url: index_args.extra_index_url.map(|extra_index_url| { extra_index_url .into_iter() .filter_map(Maybe::into_option) .collect() }), no_index: if index_args.no_index { Some(true) } else { None }, find_links: index_args.find_links.map(|find_links| { find_links .into_iter() .filter_map(Maybe::into_option) .collect() }), upgrade: Upgrade::from_args( flag(upgrade, no_upgrade, "upgrade"), upgrade_package.into_iter().map(Requirement::from).collect(), ), reinstall: Reinstall::from_args( flag(reinstall, no_reinstall, "reinstall"), reinstall_package, ), index_strategy, keyring_provider, resolution, prerelease: if pre { Some(PrereleaseMode::Allow) } else { prerelease }, fork_strategy, dependency_metadata: None, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), config_settings_package: config_settings_package.map(|config_settings| { config_settings .into_iter() .collect::() }), build_isolation: BuildIsolation::from_args( flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation_package, ), extra_build_dependencies: None, extra_build_variables: None, exclude_newer, exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter), link_mode, compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"), no_build: flag(no_build, build, "build"), no_build_package: if no_build_package.is_empty() { None } else { Some(no_build_package) }, no_binary: flag(no_binary, binary, "binary"), no_binary_package: if no_binary_package.is_empty() { None } else { Some(no_binary_package) }, no_sources: if no_sources { Some(true) } else { None }, } } uv-0.9.17+ds1/crates/uv-cli/src/version.rs000066400000000000000000000131621520155276700202440ustar00rootroot00000000000000//! Code for representing uv's release version number. // See also use std::fmt; use serde::Serialize; use uv_normalize::PackageName; use uv_pep508::uv_pep440::Version; /// Information about the git repository where uv was built from. #[derive(Serialize)] pub(crate) struct CommitInfo { short_commit_hash: String, commit_hash: String, commit_date: String, last_tag: Option, commits_since_last_tag: u32, } /// uv's version. #[derive(Serialize)] pub struct VersionInfo { /// Name of the package (or "uv" if printing uv's own version) pub package_name: Option, /// version, such as "0.5.1" version: String, /// Information about the git commit we may have been built from. /// /// `None` if not built from a git repo or if retrieval failed. commit_info: Option, } impl VersionInfo { pub fn new(package_name: Option<&PackageName>, version: &Version) -> Self { Self { package_name: package_name.map(ToString::to_string), version: version.to_string(), commit_info: None, } } } impl fmt::Display for VersionInfo { /// Formatted version information: "[+] ( )" /// /// This is intended for consumption by `clap` to provide `uv --version`, /// and intentionally omits the name of the package fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.version)?; if let Some(ci) = &self.commit_info { write!(f, "{ci}")?; } Ok(()) } } impl fmt::Display for CommitInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.commits_since_last_tag > 0 { write!(f, "+{}", self.commits_since_last_tag)?; } write!(f, " ({} {})", self.short_commit_hash, self.commit_date)?; Ok(()) } } impl From for clap::builder::Str { fn from(val: VersionInfo) -> Self { val.to_string().into() } } /// Returns information about uv's version. pub fn uv_self_version() -> VersionInfo { // Environment variables are only read at compile-time macro_rules! option_env_str { ($name:expr) => { option_env!($name).map(|s| s.to_string()) }; } // This version is pulled from Cargo.toml and set by Cargo let version = uv_version::version().to_string(); // Commit info is pulled from git and set by `build.rs` let commit_info = option_env_str!("UV_COMMIT_HASH").map(|commit_hash| CommitInfo { short_commit_hash: option_env_str!("UV_COMMIT_SHORT_HASH").unwrap(), commit_hash, commit_date: option_env_str!("UV_COMMIT_DATE").unwrap(), last_tag: option_env_str!("UV_LAST_TAG"), commits_since_last_tag: option_env_str!("UV_LAST_TAG_DISTANCE") .as_deref() .map_or(0, |value| value.parse::().unwrap_or(0)), }); VersionInfo { package_name: Some("uv".to_owned()), version, commit_info, } } #[cfg(test)] mod tests { use insta::{assert_json_snapshot, assert_snapshot}; use super::{CommitInfo, VersionInfo}; #[test] fn version_formatting() { let version = VersionInfo { package_name: Some("uv".to_string()), version: "0.0.0".to_string(), commit_info: None, }; assert_snapshot!(version, @"0.0.0"); } #[test] fn version_formatting_with_commit_info() { let version = VersionInfo { package_name: Some("uv".to_string()), version: "0.0.0".to_string(), commit_info: Some(CommitInfo { short_commit_hash: "53b0f5d92".to_string(), commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), last_tag: Some("v0.0.1".to_string()), commit_date: "2023-10-19".to_string(), commits_since_last_tag: 0, }), }; assert_snapshot!(version, @"0.0.0 (53b0f5d92 2023-10-19)"); } #[test] fn version_formatting_with_commits_since_last_tag() { let version = VersionInfo { package_name: Some("uv".to_string()), version: "0.0.0".to_string(), commit_info: Some(CommitInfo { short_commit_hash: "53b0f5d92".to_string(), commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), last_tag: Some("v0.0.1".to_string()), commit_date: "2023-10-19".to_string(), commits_since_last_tag: 24, }), }; assert_snapshot!(version, @"0.0.0+24 (53b0f5d92 2023-10-19)"); } #[test] fn version_serializable() { let version = VersionInfo { package_name: Some("uv".to_string()), version: "0.0.0".to_string(), commit_info: Some(CommitInfo { short_commit_hash: "53b0f5d92".to_string(), commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), last_tag: Some("v0.0.1".to_string()), commit_date: "2023-10-19".to_string(), commits_since_last_tag: 0, }), }; assert_json_snapshot!(version, @r#" { "package_name": "uv", "version": "0.0.0", "commit_info": { "short_commit_hash": "53b0f5d92", "commit_hash": "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7", "commit_date": "2023-10-19", "last_tag": "v0.0.1", "commits_since_last_tag": 0 } } "#); } } uv-0.9.17+ds1/crates/uv-client/000077500000000000000000000000001520155276700161265ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-client/Cargo.toml000066400000000000000000000045501520155276700200620ustar00rootroot00000000000000[package] name = "uv-client" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-auth = { workspace = true } uv-cache = { workspace = true } uv-cache-key = { workspace = true } uv-configuration = { workspace = true } uv-distribution-filename = { workspace = true } uv-distribution-types = { workspace = true } uv-fs = { workspace = true, features = ["tokio"] } uv-metadata = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-platform-tags = { workspace = true } uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-small-str = { workspace = true } uv-redacted = { workspace = true } uv-static = { workspace = true } uv-torch = { workspace = true } uv-version = { workspace = true } uv-warnings = { workspace = true } anyhow = { workspace = true } astral-tl = { workspace = true } async-trait = { workspace = true } async_http_range_reader = { workspace = true } async_zip = { workspace = true } bytecheck = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } h2 = { workspace = true } html-escape = { workspace = true } http = { workspace = true } itertools = { workspace = true } jiff = { workspace = true } percent-encoding = { workspace = true } reqwest = { workspace = true } reqwest-middleware = { workspace = true } reqwest-retry = { workspace = true } rkyv = { workspace = true } rmp-serde = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sys-info = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } tracing = { workspace = true } url = { workspace = true } [dev-dependencies] anyhow = { workspace = true } http-body-util = { workspace = true } hyper = { workspace = true } hyper-util = { workspace = true } insta = { workspace = true } rcgen = { workspace = true } rustls = { workspace = true } tokio = { workspace = true } tokio-rustls = { workspace = true } wiremock = { workspace = true } tempfile = { workspace = true } uv-0.9.17+ds1/crates/uv-client/README.md000066400000000000000000000010251520155276700174030ustar00rootroot00000000000000 # uv-client This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-client). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-client/src/000077500000000000000000000000001520155276700167155ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-client/src/base_client.rs000066400000000000000000001452701520155276700215440ustar00rootroot00000000000000use std::error::Error; use std::fmt::Debug; use std::fmt::Write; use std::num::ParseIntError; use std::path::Path; use std::sync::Arc; use std::time::Duration; use std::{env, io, iter}; use anyhow::anyhow; use http::{ HeaderMap, HeaderName, HeaderValue, Method, StatusCode, header::{ AUTHORIZATION, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, COOKIE, LOCATION, PROXY_AUTHORIZATION, REFERER, TRANSFER_ENCODING, WWW_AUTHENTICATE, }, }; use itertools::Itertools; use reqwest::{Client, ClientBuilder, IntoUrl, Proxy, Request, Response, multipart}; use reqwest_middleware::{ClientWithMiddleware, Middleware}; use reqwest_retry::policies::ExponentialBackoff; use reqwest_retry::{ DefaultRetryableStrategy, RetryTransientMiddleware, Retryable, RetryableStrategy, default_on_request_error, }; use thiserror::Error; use tracing::{debug, trace}; use url::ParseError; use url::Url; use uv_auth::{AuthMiddleware, Credentials, CredentialsCache, Indexes, PyxTokenStore}; use uv_configuration::{KeyringProviderType, TrustedHost}; use uv_fs::Simplified; use uv_pep508::MarkerEnvironment; use uv_platform_tags::Platform; use uv_preview::Preview; use uv_redacted::DisplaySafeUrl; use uv_redacted::DisplaySafeUrlError; use uv_static::EnvVars; use uv_version::version; use uv_warnings::warn_user_once; use crate::linehaul::LineHaul; use crate::middleware::OfflineMiddleware; use crate::tls::read_identity; use crate::{Connectivity, WrappedReqwestError}; pub const DEFAULT_RETRIES: u32 = 3; /// Maximum number of redirects to follow before giving up. /// /// This is the default used by [`reqwest`]. const DEFAULT_MAX_REDIRECTS: u32 = 10; /// Selectively skip parts or the entire auth middleware. #[derive(Debug, Clone, Copy, Default)] pub enum AuthIntegration { /// Run the full auth middleware, including sending an unauthenticated request first. #[default] Default, /// Send only an authenticated request without cloning and sending an unauthenticated request /// first. Errors if no credentials were found. OnlyAuthenticated, /// Skip the auth middleware entirely. The caller is responsible for managing authentication. NoAuthMiddleware, } /// A builder for an [`BaseClient`]. #[derive(Debug, Clone)] pub struct BaseClientBuilder<'a> { keyring: KeyringProviderType, preview: Preview, allow_insecure_host: Vec, native_tls: bool, built_in_root_certs: bool, retries: u32, pub connectivity: Connectivity, markers: Option<&'a MarkerEnvironment>, platform: Option<&'a Platform>, auth_integration: AuthIntegration, /// Global authentication cache for a uv invocation to share credentials across uv clients. credentials_cache: Arc, indexes: Indexes, timeout: Duration, extra_middleware: Option, proxies: Vec, redirect_policy: RedirectPolicy, /// Whether credentials should be propagated during cross-origin redirects. /// /// A policy allowing propagation is insecure and should only be available for test code. cross_origin_credential_policy: CrossOriginCredentialsPolicy, /// Optional custom reqwest client to use instead of creating a new one. custom_client: Option, /// uv subcommand in which this client is being used subcommand: Option>, } /// The policy for handling HTTP redirects. #[derive(Debug, Default, Clone, Copy)] pub enum RedirectPolicy { /// Use reqwest's built-in redirect handling. This bypasses our custom middleware /// on redirect. #[default] BypassMiddleware, /// Handle redirects manually, re-triggering our custom middleware for each request. RetriggerMiddleware, } impl RedirectPolicy { pub fn reqwest_policy(self) -> reqwest::redirect::Policy { match self { Self::BypassMiddleware => reqwest::redirect::Policy::default(), Self::RetriggerMiddleware => reqwest::redirect::Policy::none(), } } } /// A list of user-defined middlewares to be applied to the client. #[derive(Clone)] pub struct ExtraMiddleware(pub Vec>); impl Debug for ExtraMiddleware { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ExtraMiddleware") .field("0", &format!("{} middlewares", self.0.len())) .finish() } } impl Default for BaseClientBuilder<'_> { fn default() -> Self { Self { keyring: KeyringProviderType::default(), preview: Preview::default(), allow_insecure_host: vec![], native_tls: false, built_in_root_certs: false, connectivity: Connectivity::Online, retries: DEFAULT_RETRIES, markers: None, platform: None, auth_integration: AuthIntegration::default(), credentials_cache: Arc::new(CredentialsCache::default()), indexes: Indexes::new(), timeout: Duration::from_secs(30), extra_middleware: None, proxies: vec![], redirect_policy: RedirectPolicy::default(), cross_origin_credential_policy: CrossOriginCredentialsPolicy::Secure, custom_client: None, subcommand: None, } } } impl<'a> BaseClientBuilder<'a> { pub fn new( connectivity: Connectivity, native_tls: bool, allow_insecure_host: Vec, preview: Preview, timeout: Duration, retries: u32, ) -> Self { Self { preview, allow_insecure_host, native_tls, retries, connectivity, timeout, ..Self::default() } } /// Use a custom reqwest client instead of creating a new one. /// /// This allows you to provide your own reqwest client with custom configuration. /// Note that some configuration options from this builder will still be applied /// to the client via middleware. #[must_use] pub fn custom_client(mut self, client: Client) -> Self { self.custom_client = Some(client); self } #[must_use] pub fn keyring(mut self, keyring_type: KeyringProviderType) -> Self { self.keyring = keyring_type; self } #[must_use] pub fn allow_insecure_host(mut self, allow_insecure_host: Vec) -> Self { self.allow_insecure_host = allow_insecure_host; self } #[must_use] pub fn connectivity(mut self, connectivity: Connectivity) -> Self { self.connectivity = connectivity; self } #[must_use] pub fn retries(mut self, retries: u32) -> Self { self.retries = retries; self } #[must_use] pub fn native_tls(mut self, native_tls: bool) -> Self { self.native_tls = native_tls; self } #[must_use] pub fn built_in_root_certs(mut self, built_in_root_certs: bool) -> Self { self.built_in_root_certs = built_in_root_certs; self } #[must_use] pub fn markers(mut self, markers: &'a MarkerEnvironment) -> Self { self.markers = Some(markers); self } #[must_use] pub fn platform(mut self, platform: &'a Platform) -> Self { self.platform = Some(platform); self } #[must_use] pub fn auth_integration(mut self, auth_integration: AuthIntegration) -> Self { self.auth_integration = auth_integration; self } #[must_use] pub fn indexes(mut self, indexes: Indexes) -> Self { self.indexes = indexes; self } #[must_use] pub fn timeout(mut self, timeout: Duration) -> Self { self.timeout = timeout; self } #[must_use] pub fn extra_middleware(mut self, middleware: ExtraMiddleware) -> Self { self.extra_middleware = Some(middleware); self } #[must_use] pub fn proxy(mut self, proxy: Proxy) -> Self { self.proxies.push(proxy); self } #[must_use] pub fn redirect(mut self, policy: RedirectPolicy) -> Self { self.redirect_policy = policy; self } /// Allows credentials to be propagated on cross-origin redirects. /// /// WARNING: This should only be available for tests. In production code, propagating credentials /// during cross-origin redirects can lead to security vulnerabilities including credential /// leakage to untrusted domains. #[cfg(test)] #[must_use] pub fn allow_cross_origin_credentials(mut self) -> Self { self.cross_origin_credential_policy = CrossOriginCredentialsPolicy::Insecure; self } #[must_use] pub fn subcommand(mut self, subcommand: Vec) -> Self { self.subcommand = Some(subcommand); self } pub fn credentials_cache(&self) -> &CredentialsCache { &self.credentials_cache } /// See [`CredentialsCache::store_credentials_from_url`]. pub fn store_credentials_from_url(&self, url: &DisplaySafeUrl) -> bool { self.credentials_cache.store_credentials_from_url(url) } /// See [`CredentialsCache::store_credentials`]. pub fn store_credentials(&self, url: &DisplaySafeUrl, credentials: Credentials) { self.credentials_cache.store_credentials(url, credentials); } pub fn is_native_tls(&self) -> bool { self.native_tls } pub fn is_offline(&self) -> bool { matches!(self.connectivity, Connectivity::Offline) } /// Create a [`RetryPolicy`] for the client. pub fn retry_policy(&self) -> ExponentialBackoff { let mut builder = ExponentialBackoff::builder(); if env::var_os(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY).is_some() { builder = builder.retry_bounds(Duration::from_millis(0), Duration::from_millis(0)); } builder.build_with_max_retries(self.retries) } pub fn build(&self) -> BaseClient { let timeout = self.timeout; debug!("Using request timeout of {}s", timeout.as_secs()); // Use the custom client if provided, otherwise create a new one let (raw_client, raw_dangerous_client) = match &self.custom_client { Some(client) => (client.clone(), client.clone()), None => self.create_secure_and_insecure_clients(timeout), }; // Wrap in any relevant middleware and handle connectivity. let client = RedirectClientWithMiddleware { client: self.apply_middleware(raw_client.clone()), redirect_policy: self.redirect_policy, cross_origin_credentials_policy: self.cross_origin_credential_policy, }; let dangerous_client = RedirectClientWithMiddleware { client: self.apply_middleware(raw_dangerous_client.clone()), redirect_policy: self.redirect_policy, cross_origin_credentials_policy: self.cross_origin_credential_policy, }; BaseClient { connectivity: self.connectivity, allow_insecure_host: self.allow_insecure_host.clone(), retries: self.retries, client, raw_client, dangerous_client, raw_dangerous_client, timeout, credentials_cache: self.credentials_cache.clone(), } } /// Share the underlying client between two different middleware configurations. pub fn wrap_existing(&self, existing: &BaseClient) -> BaseClient { // Wrap in any relevant middleware and handle connectivity. let client = RedirectClientWithMiddleware { client: self.apply_middleware(existing.raw_client.clone()), redirect_policy: self.redirect_policy, cross_origin_credentials_policy: self.cross_origin_credential_policy, }; let dangerous_client = RedirectClientWithMiddleware { client: self.apply_middleware(existing.raw_dangerous_client.clone()), redirect_policy: self.redirect_policy, cross_origin_credentials_policy: self.cross_origin_credential_policy, }; BaseClient { connectivity: self.connectivity, allow_insecure_host: self.allow_insecure_host.clone(), retries: self.retries, client, dangerous_client, raw_client: existing.raw_client.clone(), raw_dangerous_client: existing.raw_dangerous_client.clone(), timeout: existing.timeout, credentials_cache: existing.credentials_cache.clone(), } } fn create_secure_and_insecure_clients(&self, timeout: Duration) -> (Client, Client) { // Create user agent. let mut user_agent_string = format!("uv/{}", version()); // Add linehaul metadata. let linehaul = LineHaul::new(self.markers, self.platform, self.subcommand.clone()); if let Ok(output) = serde_json::to_string(&linehaul) { let _ = write!(user_agent_string, " {output}"); } // Checks for the presence of `SSL_CERT_FILE`. // Certificate loading support is delegated to `rustls-native-certs`. // See https://github.com/rustls/rustls-native-certs/blob/813790a297ad4399efe70a8e5264ca1b420acbec/src/lib.rs#L118-L125 let ssl_cert_file_exists = env::var_os(EnvVars::SSL_CERT_FILE).is_some_and(|path| { let path_exists = Path::new(&path).exists(); if !path_exists { warn_user_once!( "Ignoring invalid `SSL_CERT_FILE`. File does not exist: {}.", path.simplified_display().cyan() ); } path_exists }); // Checks for the presence of `SSL_CERT_DIR`. // Certificate loading support is delegated to `rustls-native-certs`. // See https://github.com/rustls/rustls-native-certs/blob/813790a297ad4399efe70a8e5264ca1b420acbec/src/lib.rs#L118-L125 let ssl_cert_dir_exists = env::var_os(EnvVars::SSL_CERT_DIR) .filter(|v| !v.is_empty()) .is_some_and(|dirs| { // Parse `SSL_CERT_DIR`, with support for multiple entries using // a platform-specific delimiter (`:` on Unix, `;` on Windows) let (existing, missing): (Vec<_>, Vec<_>) = env::split_paths(&dirs).partition(|p| p.exists()); if existing.is_empty() { let end_note = if missing.len() == 1 { "The directory does not exist." } else { "The entries do not exist." }; warn_user_once!( "Ignoring invalid `SSL_CERT_DIR`. {end_note}: {}.", missing .iter() .map(Simplified::simplified_display) .join(", ") .cyan() ); return false; } // Warn on any missing entries if !missing.is_empty() { let end_note = if missing.len() == 1 { "The following directory does not exist:" } else { "The following entries do not exist:" }; warn_user_once!( "Invalid entries in `SSL_CERT_DIR`. {end_note}: {}.", missing .iter() .map(Simplified::simplified_display) .join(", ") .cyan() ); } // Proceed while ignoring missing entries true }); // Create a secure client that validates certificates. let raw_client = self.create_client( &user_agent_string, timeout, ssl_cert_file_exists, ssl_cert_dir_exists, Security::Secure, self.redirect_policy, ); // Create an insecure client that accepts invalid certificates. let raw_dangerous_client = self.create_client( &user_agent_string, timeout, ssl_cert_file_exists, ssl_cert_dir_exists, Security::Insecure, self.redirect_policy, ); (raw_client, raw_dangerous_client) } fn create_client( &self, user_agent: &str, timeout: Duration, ssl_cert_file_exists: bool, ssl_cert_dir_exists: bool, security: Security, redirect_policy: RedirectPolicy, ) -> Client { // Configure the builder. let client_builder = ClientBuilder::new() .http1_title_case_headers() .user_agent(user_agent) .pool_max_idle_per_host(20) .read_timeout(timeout) .tls_built_in_root_certs(self.built_in_root_certs) .redirect(redirect_policy.reqwest_policy()); // If necessary, accept invalid certificates. let client_builder = match security { Security::Secure => client_builder, Security::Insecure => client_builder.danger_accept_invalid_certs(true), }; let client_builder = if self.native_tls || ssl_cert_file_exists || ssl_cert_dir_exists { client_builder.tls_built_in_native_certs(true) } else { client_builder.tls_built_in_webpki_certs(true) }; // Configure mTLS. let client_builder = if let Some(ssl_client_cert) = env::var_os(EnvVars::SSL_CLIENT_CERT) { match read_identity(&ssl_client_cert) { Ok(identity) => client_builder.identity(identity), Err(err) => { warn_user_once!("Ignoring invalid `SSL_CLIENT_CERT`: {err}"); client_builder } } } else { client_builder }; // apply proxies let mut client_builder = client_builder; for p in &self.proxies { client_builder = client_builder.proxy(p.clone()); } let client_builder = client_builder; client_builder .build() .expect("Failed to build HTTP client.") } fn apply_middleware(&self, client: Client) -> ClientWithMiddleware { match self.connectivity { Connectivity::Online => { // Create a base client to using in the authentication middleware. let base_client = { let mut client = reqwest_middleware::ClientBuilder::new(client.clone()); // Avoid uncloneable errors with a streaming body during publish. if self.retries > 0 { // Initialize the retry strategy. let retry_strategy = RetryTransientMiddleware::new_with_policy_and_strategy( self.retry_policy(), UvRetryableStrategy, ); client = client.with(retry_strategy); } // When supplied, add the extra middleware. if let Some(extra_middleware) = &self.extra_middleware { for middleware in &extra_middleware.0 { client = client.with_arc(middleware.clone()); } } client.build() }; let mut client = reqwest_middleware::ClientBuilder::new(client); // Avoid uncloneable errors with a streaming body during publish. if self.retries > 0 { // Initialize the retry strategy. let retry_strategy = RetryTransientMiddleware::new_with_policy_and_strategy( self.retry_policy(), UvRetryableStrategy, ); client = client.with(retry_strategy); } // When supplied, add the extra middleware. if let Some(extra_middleware) = &self.extra_middleware { for middleware in &extra_middleware.0 { client = client.with_arc(middleware.clone()); } } // Initialize the authentication middleware to set headers. match self.auth_integration { AuthIntegration::Default => { let mut auth_middleware = AuthMiddleware::new() .with_cache_arc(self.credentials_cache.clone()) .with_base_client(base_client) .with_indexes(self.indexes.clone()) .with_keyring(self.keyring.to_provider()) .with_preview(self.preview); if let Ok(token_store) = PyxTokenStore::from_settings() { auth_middleware = auth_middleware.with_pyx_token_store(token_store); } client = client.with(auth_middleware); } AuthIntegration::OnlyAuthenticated => { let mut auth_middleware = AuthMiddleware::new() .with_cache_arc(self.credentials_cache.clone()) .with_base_client(base_client) .with_indexes(self.indexes.clone()) .with_keyring(self.keyring.to_provider()) .with_preview(self.preview) .with_only_authenticated(true); if let Ok(token_store) = PyxTokenStore::from_settings() { auth_middleware = auth_middleware.with_pyx_token_store(token_store); } client = client.with(auth_middleware); } AuthIntegration::NoAuthMiddleware => { // The downstream code uses custom auth logic. } } client.build() } Connectivity::Offline => reqwest_middleware::ClientBuilder::new(client) .with(OfflineMiddleware) .build(), } } } /// A base client for HTTP requests #[derive(Debug, Clone)] pub struct BaseClient { /// The underlying HTTP client that enforces valid certificates. client: RedirectClientWithMiddleware, /// The underlying HTTP client that accepts invalid certificates. dangerous_client: RedirectClientWithMiddleware, /// The HTTP client without middleware. raw_client: Client, /// The HTTP client that accepts invalid certificates without middleware. raw_dangerous_client: Client, /// The connectivity mode to use. connectivity: Connectivity, /// Configured client timeout, in seconds. timeout: Duration, /// Hosts that are trusted to use the insecure client. allow_insecure_host: Vec, /// The number of retries to attempt on transient errors. retries: u32, /// Global authentication cache for a uv invocation to share credentials across uv clients. credentials_cache: Arc, } #[derive(Debug, Clone, Copy)] enum Security { /// The client should use secure settings, i.e., valid certificates. Secure, /// The client should use insecure settings, i.e., skip certificate validation. Insecure, } impl BaseClient { /// Selects the appropriate client based on the host's trustworthiness. pub fn for_host(&self, url: &DisplaySafeUrl) -> &RedirectClientWithMiddleware { if self.disable_ssl(url) { &self.dangerous_client } else { &self.client } } /// Executes a request, applying redirect policy. pub async fn execute(&self, req: Request) -> reqwest_middleware::Result { let client = self.for_host(&DisplaySafeUrl::from_url(req.url().clone())); client.execute(req).await } /// Returns `true` if the host is trusted to use the insecure client. pub fn disable_ssl(&self, url: &DisplaySafeUrl) -> bool { self.allow_insecure_host .iter() .any(|allow_insecure_host| allow_insecure_host.matches(url)) } /// The configured client timeout, in seconds. pub fn timeout(&self) -> Duration { self.timeout } /// The configured connectivity mode. pub fn connectivity(&self) -> Connectivity { self.connectivity } /// The [`RetryPolicy`] for the client. pub fn retry_policy(&self) -> ExponentialBackoff { let mut builder = ExponentialBackoff::builder(); if env::var_os(EnvVars::UV_TEST_NO_HTTP_RETRY_DELAY).is_some() { builder = builder.retry_bounds(Duration::from_millis(0), Duration::from_millis(0)); } builder.build_with_max_retries(self.retries) } pub fn credentials_cache(&self) -> &CredentialsCache { &self.credentials_cache } } /// Wrapper around [`ClientWithMiddleware`] that manages redirects. #[derive(Debug, Clone)] pub struct RedirectClientWithMiddleware { client: ClientWithMiddleware, redirect_policy: RedirectPolicy, /// Whether credentials should be preserved during cross-origin redirects. /// /// WARNING: This should only be available for tests. In production code, preserving credentials /// during cross-origin redirects can lead to security vulnerabilities including credential /// leakage to untrusted domains. cross_origin_credentials_policy: CrossOriginCredentialsPolicy, } impl RedirectClientWithMiddleware { /// Convenience method to make a `GET` request to a URL. pub fn get(&self, url: U) -> RequestBuilder<'_> { RequestBuilder::new(self.client.get(url), self) } /// Convenience method to make a `POST` request to a URL. pub fn post(&self, url: U) -> RequestBuilder<'_> { RequestBuilder::new(self.client.post(url), self) } /// Convenience method to make a `HEAD` request to a URL. pub fn head(&self, url: U) -> RequestBuilder<'_> { RequestBuilder::new(self.client.head(url), self) } /// Executes a request, applying the redirect policy. pub async fn execute(&self, req: Request) -> reqwest_middleware::Result { match self.redirect_policy { RedirectPolicy::BypassMiddleware => self.client.execute(req).await, RedirectPolicy::RetriggerMiddleware => self.execute_with_redirect_handling(req).await, } } /// Executes a request. If the response is a redirect (one of HTTP 301, 302, 303, 307, or 308), the /// request is executed again with the redirect location URL (up to a maximum number of /// redirects). /// /// Unlike the built-in reqwest redirect policies, this sends the redirect request through the /// entire middleware pipeline again. /// /// See RFC 7231 7.1.2 for details on /// redirect semantics. async fn execute_with_redirect_handling( &self, req: Request, ) -> reqwest_middleware::Result { let mut request = req; let mut redirects = 0; let max_redirects = DEFAULT_MAX_REDIRECTS; loop { let result = self .client .execute(request.try_clone().expect("HTTP request must be cloneable")) .await; let Ok(response) = result else { return result; }; if redirects >= max_redirects { return Ok(response); } let Some(redirect_request) = request_into_redirect(request, &response, self.cross_origin_credentials_policy)? else { return Ok(response); }; redirects += 1; request = redirect_request; } } pub fn raw_client(&self) -> &ClientWithMiddleware { &self.client } } impl From for ClientWithMiddleware { fn from(item: RedirectClientWithMiddleware) -> Self { item.client } } /// Check if this is should be a redirect and, if so, return a new redirect request. /// /// This implementation is based on the [`reqwest`] crate redirect implementation. /// It takes ownership of the original [`Request`] and mutates it to create the new /// redirect [`Request`]. fn request_into_redirect( mut req: Request, res: &Response, cross_origin_credentials_policy: CrossOriginCredentialsPolicy, ) -> reqwest_middleware::Result> { let original_req_url = DisplaySafeUrl::from_url(req.url().clone()); let status = res.status(); let should_redirect = match status { StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND | StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT => true, StatusCode::SEE_OTHER => { // Per RFC 7231, HTTP 303 is intended for the user agent // to perform a GET or HEAD request to the redirect target. // Historically, some browsers also changed method from POST // to GET on 301 or 302, but this is not required by RFC 7231 // and was not intended by the HTTP spec. *req.body_mut() = None; for header in &[ TRANSFER_ENCODING, CONTENT_ENCODING, CONTENT_TYPE, CONTENT_LENGTH, ] { req.headers_mut().remove(header); } match *req.method() { Method::GET | Method::HEAD => {} _ => { *req.method_mut() = Method::GET; } } true } _ => false, }; if !should_redirect { return Ok(None); } let location = res .headers() .get(LOCATION) .ok_or(reqwest_middleware::Error::Middleware(anyhow!( "Server returned redirect (HTTP {status}) without destination URL. This may indicate a server configuration issue" )))? .to_str() .map_err(|_| { reqwest_middleware::Error::Middleware(anyhow!( "Invalid HTTP {status} 'Location' value: must only contain visible ascii characters" )) })?; let mut redirect_url = match DisplaySafeUrl::parse(location) { Ok(url) => url, // Per RFC 7231, URLs should be resolved against the request URL. Err(DisplaySafeUrlError::Url(ParseError::RelativeUrlWithoutBase)) => original_req_url.join(location).map_err(|err| { reqwest_middleware::Error::Middleware(anyhow!( "Invalid HTTP {status} 'Location' value `{location}` relative to `{original_req_url}`: {err}" )) })?, Err(err) => { return Err(reqwest_middleware::Error::Middleware(anyhow!( "Invalid HTTP {status} 'Location' value `{location}`: {err}" ))); } }; // Per RFC 7231, fragments must be propagated if let Some(fragment) = original_req_url.fragment() { redirect_url.set_fragment(Some(fragment)); } // Ensure the URL is a valid HTTP URI. if let Err(err) = redirect_url.as_str().parse::() { return Err(reqwest_middleware::Error::Middleware(anyhow!( "HTTP {status} 'Location' value `{redirect_url}` is not a valid HTTP URI: {err}" ))); } if redirect_url.scheme() != "http" && redirect_url.scheme() != "https" { return Err(reqwest_middleware::Error::Middleware(anyhow!( "Invalid HTTP {status} 'Location' value `{redirect_url}`: scheme needs to be https or http" ))); } let mut headers = HeaderMap::new(); std::mem::swap(req.headers_mut(), &mut headers); let cross_host = redirect_url.host_str() != original_req_url.host_str() || redirect_url.port_or_known_default() != original_req_url.port_or_known_default(); if cross_host { if cross_origin_credentials_policy == CrossOriginCredentialsPolicy::Secure { debug!("Received a cross-origin redirect. Removing sensitive headers."); headers.remove(AUTHORIZATION); headers.remove(COOKIE); headers.remove(PROXY_AUTHORIZATION); headers.remove(WWW_AUTHENTICATE); } // If the redirect request is not a cross-origin request and the original request already // had a Referer header, attempt to set the Referer header for the redirect request. } else if headers.contains_key(REFERER) { if let Some(referer) = make_referer(&redirect_url, &original_req_url) { headers.insert(REFERER, referer); } } // Check if there are credentials on the redirect location itself. // If so, move them to Authorization header. if !redirect_url.username().is_empty() { if let Some(credentials) = Credentials::from_url(&redirect_url) { let _ = redirect_url.set_username(""); let _ = redirect_url.set_password(None); headers.insert(AUTHORIZATION, credentials.to_header_value()); } } std::mem::swap(req.headers_mut(), &mut headers); *req.url_mut() = Url::from(redirect_url); debug!( "Received HTTP {status}. Redirecting to {}", DisplaySafeUrl::ref_cast(req.url()) ); Ok(Some(req)) } /// Return a Referer [`HeaderValue`] according to RFC 7231. /// /// Return [`None`] if https has been downgraded in the redirect location. fn make_referer( redirect_url: &DisplaySafeUrl, original_url: &DisplaySafeUrl, ) -> Option { if redirect_url.scheme() == "http" && original_url.scheme() == "https" { return None; } let mut referer = original_url.clone(); referer.remove_credentials(); referer.set_fragment(None); referer.as_str().parse().ok() } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub(crate) enum CrossOriginCredentialsPolicy { /// Do not propagate credentials on cross-origin requests. #[default] Secure, /// Propagate credentials on cross-origin requests. /// /// WARNING: This should only be available for tests. In production code, preserving credentials /// during cross-origin redirects can lead to security vulnerabilities including credential /// leakage to untrusted domains. #[cfg(test)] Insecure, } /// A builder to construct the properties of a `Request`. /// /// This wraps [`reqwest_middleware::RequestBuilder`] to ensure that the [`BaseClient`] /// redirect policy is respected if `send()` is called. #[derive(Debug)] #[must_use] pub struct RequestBuilder<'a> { builder: reqwest_middleware::RequestBuilder, client: &'a RedirectClientWithMiddleware, } impl<'a> RequestBuilder<'a> { pub fn new( builder: reqwest_middleware::RequestBuilder, client: &'a RedirectClientWithMiddleware, ) -> Self { Self { builder, client } } /// Add a `Header` to this Request. pub fn header(mut self, key: K, value: V) -> Self where HeaderName: TryFrom, >::Error: Into, HeaderValue: TryFrom, >::Error: Into, { self.builder = self.builder.header(key, value); self } /// Add a set of Headers to the existing ones on this Request. /// /// The headers will be merged in to any already set. pub fn headers(mut self, headers: HeaderMap) -> Self { self.builder = self.builder.headers(headers); self } #[cfg(not(target_arch = "wasm32"))] pub fn version(mut self, version: reqwest::Version) -> Self { self.builder = self.builder.version(version); self } #[cfg_attr(docsrs, doc(cfg(feature = "multipart")))] pub fn multipart(mut self, multipart: multipart::Form) -> Self { self.builder = self.builder.multipart(multipart); self } /// Build a `Request`. pub fn build(self) -> reqwest::Result { self.builder.build() } /// Constructs the Request and sends it to the target URL, returning a /// future Response. pub async fn send(self) -> reqwest_middleware::Result { self.client.execute(self.build()?).await } pub fn raw_builder(&self) -> &reqwest_middleware::RequestBuilder { &self.builder } } /// Extends [`DefaultRetryableStrategy`], to log transient request failures and additional retry cases. pub struct UvRetryableStrategy; impl RetryableStrategy for UvRetryableStrategy { fn handle(&self, res: &Result) -> Option { // Use the default strategy and check for additional transient error cases. let retryable = match DefaultRetryableStrategy.handle(res) { None | Some(Retryable::Fatal) if res .as_ref() .is_err_and(|err| is_transient_network_error(err)) => { Some(Retryable::Transient) } default => default, }; // Log on transient errors if retryable == Some(Retryable::Transient) { match res { Ok(response) => { debug!("Transient request failure for: {}", response.url()); } Err(err) => { let context = iter::successors(err.source(), |&err| err.source()) .map(|err| format!(" Caused by: {err}")) .join("\n"); debug!( "Transient request failure for {}, retrying: {err}\n{context}", err.url().map(Url::as_str).unwrap_or("unknown URL") ); } } } retryable } } /// Whether the error looks like a network error that should be retried. /// /// There are two cases that the default retry strategy is missing: /// * Inside the reqwest or reqwest-middleware error is an `io::Error` such as a broken pipe /// * When streaming a response, a reqwest error may be hidden several layers behind errors /// of different crates processing the stream, including `io::Error` layers. pub fn is_transient_network_error(err: &(dyn Error + 'static)) -> bool { // First, try to show a nice trace log if let Some((Some(status), Some(url))) = find_source::(&err) .map(|request_err| (request_err.status(), request_err.url())) { trace!("Considering retry of response HTTP {status} for {url}"); } else { trace!("Considering retry of error: {err:?}"); } let mut has_known_error = false; // IO Errors or reqwest errors may be nested through custom IO errors or stream processing // crates let mut current_source = Some(err); while let Some(source) = current_source { if let Some(reqwest_err) = source.downcast_ref::() { has_known_error = true; if let reqwest_middleware::Error::Reqwest(reqwest_err) = &**reqwest_err { if default_on_request_error(reqwest_err) == Some(Retryable::Transient) { trace!("Retrying nested reqwest middleware error"); return true; } if is_retryable_status_error(reqwest_err) { trace!("Retrying nested reqwest middleware status code error"); return true; } } trace!("Cannot retry nested reqwest middleware error"); } else if let Some(reqwest_err) = source.downcast_ref::() { has_known_error = true; if default_on_request_error(reqwest_err) == Some(Retryable::Transient) { trace!("Retrying nested reqwest error"); return true; } if is_retryable_status_error(reqwest_err) { trace!("Retrying nested reqwest status code error"); return true; } trace!("Cannot retry nested reqwest error"); } else if source.downcast_ref::().is_some() { // All h2 errors look like errors that should be retried // https://github.com/astral-sh/uv/issues/15916 trace!("Retrying nested h2 error"); return true; } else if let Some(io_err) = source.downcast_ref::() { has_known_error = true; let retryable_io_err_kinds = [ // https://github.com/astral-sh/uv/issues/12054 io::ErrorKind::BrokenPipe, // From reqwest-middleware io::ErrorKind::ConnectionAborted, // https://github.com/astral-sh/uv/issues/3514 io::ErrorKind::ConnectionReset, // https://github.com/astral-sh/uv/issues/14699 io::ErrorKind::InvalidData, // https://github.com/astral-sh/uv/issues/9246 io::ErrorKind::UnexpectedEof, ]; if retryable_io_err_kinds.contains(&io_err.kind()) { trace!("Retrying error: `{}`", io_err.kind()); return true; } trace!( "Cannot retry IO error `{}`, not a retryable IO error kind", io_err.kind() ); } current_source = source.source(); } if !has_known_error { trace!("Cannot retry error: Neither an IO error nor a reqwest error"); } false } /// Whether the error is a status code error that is retryable. /// /// Port of `reqwest_retry::default_on_request_success`. fn is_retryable_status_error(reqwest_err: &reqwest::Error) -> bool { let Some(status) = reqwest_err.status() else { return false; }; status.is_server_error() || status == StatusCode::REQUEST_TIMEOUT || status == StatusCode::TOO_MANY_REQUESTS } /// Find the first source error of a specific type. /// /// See fn find_source(orig: &dyn Error) -> Option<&E> { let mut cause = orig.source(); while let Some(err) = cause { if let Some(typed) = err.downcast_ref() { return Some(typed); } cause = err.source(); } None } // TODO(konsti): Remove once we find a native home for `retries_from_env` #[derive(Debug, Error)] pub enum RetryParsingError { #[error("Failed to parse `UV_HTTP_RETRIES`")] ParseInt(#[from] ParseIntError), } #[cfg(test)] mod tests { use super::*; use anyhow::Result; use insta::assert_debug_snapshot; use reqwest::{Client, Method}; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; use crate::base_client::request_into_redirect; #[tokio::test] async fn test_redirect_preserves_authorization_header_on_same_origin() -> Result<()> { for status in &[301, 302, 303, 307, 308] { let server = MockServer::start().await; Mock::given(method("GET")) .respond_with( ResponseTemplate::new(*status) .insert_header("location", format!("{}/redirect", server.uri())), ) .mount(&server) .await; let request = Client::new() .get(server.uri()) .basic_auth("username", Some("password")) .build() .unwrap(); assert!(request.headers().contains_key(AUTHORIZATION)); let response = Client::builder() .redirect(reqwest::redirect::Policy::none()) .build() .unwrap() .execute(request.try_clone().unwrap()) .await .unwrap(); let redirect_request = request_into_redirect(request, &response, CrossOriginCredentialsPolicy::Secure)? .unwrap(); assert!(redirect_request.headers().contains_key(AUTHORIZATION)); } Ok(()) } #[tokio::test] async fn test_redirect_preserves_fragment() -> Result<()> { for status in &[301, 302, 303, 307, 308] { let server = MockServer::start().await; Mock::given(method("GET")) .respond_with( ResponseTemplate::new(*status) .insert_header("location", format!("{}/redirect", server.uri())), ) .mount(&server) .await; let request = Client::new() .get(format!("{}#fragment", server.uri())) .build() .unwrap(); let response = Client::builder() .redirect(reqwest::redirect::Policy::none()) .build() .unwrap() .execute(request.try_clone().unwrap()) .await .unwrap(); let redirect_request = request_into_redirect(request, &response, CrossOriginCredentialsPolicy::Secure)? .unwrap(); assert!( redirect_request .url() .fragment() .is_some_and(|fragment| fragment == "fragment") ); } Ok(()) } #[tokio::test] async fn test_redirect_removes_authorization_header_on_cross_origin() -> Result<()> { for status in &[301, 302, 303, 307, 308] { let server = MockServer::start().await; Mock::given(method("GET")) .respond_with( ResponseTemplate::new(*status) .insert_header("location", "https://cross-origin.com/simple"), ) .mount(&server) .await; let request = Client::new() .get(server.uri()) .basic_auth("username", Some("password")) .build() .unwrap(); assert!(request.headers().contains_key(AUTHORIZATION)); let response = Client::builder() .redirect(reqwest::redirect::Policy::none()) .build() .unwrap() .execute(request.try_clone().unwrap()) .await .unwrap(); let redirect_request = request_into_redirect(request, &response, CrossOriginCredentialsPolicy::Secure)? .unwrap(); assert!(!redirect_request.headers().contains_key(AUTHORIZATION)); } Ok(()) } #[tokio::test] async fn test_redirect_303_changes_post_to_get() -> Result<()> { let server = MockServer::start().await; Mock::given(method("POST")) .respond_with( ResponseTemplate::new(303) .insert_header("location", format!("{}/redirect", server.uri())), ) .mount(&server) .await; let request = Client::new() .post(server.uri()) .basic_auth("username", Some("password")) .build() .unwrap(); assert_eq!(request.method(), Method::POST); let response = Client::builder() .redirect(reqwest::redirect::Policy::none()) .build() .unwrap() .execute(request.try_clone().unwrap()) .await .unwrap(); let redirect_request = request_into_redirect(request, &response, CrossOriginCredentialsPolicy::Secure)? .unwrap(); assert_eq!(redirect_request.method(), Method::GET); Ok(()) } #[tokio::test] async fn test_redirect_no_referer_if_disabled() -> Result<()> { for status in &[301, 302, 303, 307, 308] { let server = MockServer::start().await; Mock::given(method("GET")) .respond_with( ResponseTemplate::new(*status) .insert_header("location", format!("{}/redirect", server.uri())), ) .mount(&server) .await; let request = Client::builder() .referer(false) .build() .unwrap() .get(server.uri()) .basic_auth("username", Some("password")) .build() .unwrap(); assert!(!request.headers().contains_key(REFERER)); let response = Client::builder() .redirect(reqwest::redirect::Policy::none()) .build() .unwrap() .execute(request.try_clone().unwrap()) .await .unwrap(); let redirect_request = request_into_redirect(request, &response, CrossOriginCredentialsPolicy::Secure)? .unwrap(); assert!(!redirect_request.headers().contains_key(REFERER)); } Ok(()) } /// Enumerate which status codes we are retrying. #[tokio::test] async fn retried_status_codes() -> Result<()> { let server = MockServer::start().await; let client = Client::default(); let middleware_client = ClientWithMiddleware::default(); let mut retried = Vec::new(); for status in 100..599 { // Test all standard status codes and and example for a non-RFC code used in the wild. if StatusCode::from_u16(status)?.canonical_reason().is_none() && status != 420 { continue; } Mock::given(path(format!("/{status}"))) .respond_with(ResponseTemplate::new(status)) .mount(&server) .await; let response = middleware_client .get(format!("{}/{}", server.uri(), status)) .send() .await; let middleware_retry = DefaultRetryableStrategy.handle(&response) == Some(Retryable::Transient); let response = client .get(format!("{}/{}", server.uri(), status)) .send() .await?; let uv_retry = match response.error_for_status() { Ok(_) => false, Err(err) => is_transient_network_error(&err), }; // Ensure we're retrying the same status code as the reqwest_retry crate. We may choose // to deviate from this later. assert_eq!(middleware_retry, uv_retry); if uv_retry { retried.push(status); } } assert_debug_snapshot!(retried, @r" [ 100, 102, 408, 429, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, ] "); Ok(()) } } uv-0.9.17+ds1/crates/uv-client/src/cached_client.rs000066400000000000000000001225311520155276700220340ustar00rootroot00000000000000use std::time::{Duration, SystemTime}; use std::{borrow::Cow, path::Path}; use futures::FutureExt; use reqwest::{Request, Response}; use reqwest_retry::RetryPolicy; use rkyv::util::AlignedVec; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use tracing::{Instrument, debug, info_span, instrument, trace, warn}; use uv_cache::{CacheEntry, Freshness}; use uv_fs::write_atomic; use uv_redacted::DisplaySafeUrl; use crate::BaseClient; use crate::base_client::is_transient_network_error; use crate::error::ProblemDetails; use crate::{ Error, ErrorKind, httpcache::{AfterResponse, BeforeRequest, CachePolicy, CachePolicyBuilder}, rkyvutil::OwnedArchive, }; /// Extract problem details from an HTTP response if it has the correct content type /// /// Note: This consumes the response body, so it should only be called when there's an error status. async fn extract_problem_details(response: Response) -> Option { match response.bytes().await { Ok(bytes) => match serde_json::from_slice(&bytes) { Ok(details) => Some(details), Err(err) => { warn!("Failed to parse problem details: {err}"); None } }, Err(err) => { warn!("Failed to read response body for problem details: {err}"); None } } } /// A trait the generalizes (de)serialization at a high level. /// /// The main purpose of this trait is to make the `CachedClient` work for /// either serde or other mechanisms of serialization such as `rkyv`. /// /// If you're using Serde, then unless you want to control the format, callers /// should just use `CachedClient::get_serde`. This will use a default /// implementation of `Cacheable` internally. /// /// Alternatively, callers using `rkyv` should use /// `CachedClient::get_cacheable`. If your types fit into the /// `rkyvutil::OwnedArchive` mold, then an implementation of `Cacheable` is /// already provided for that type. pub trait Cacheable: Sized { /// This associated type permits customizing what the "output" type of /// deserialization is. It can be identical to `Self`. /// /// Typical use of this is for wrapper types used to provide blanket trait /// impls without hitting overlapping impl problems. type Target: Send + 'static; /// Deserialize a value from bytes aligned to a 16-byte boundary. fn from_aligned_bytes(bytes: AlignedVec) -> Result; /// Serialize bytes to a possibly owned byte buffer. fn to_bytes(&self) -> Result, Error>; /// Convert this type into its final form. fn into_target(self) -> Self::Target; } /// A wrapper type that makes anything with Serde support automatically /// implement `Cacheable`. #[derive(Debug, Deserialize, Serialize)] #[serde(transparent)] pub(crate) struct SerdeCacheable { inner: T, } impl Cacheable for SerdeCacheable { type Target = T; fn from_aligned_bytes(bytes: AlignedVec) -> Result { Ok(rmp_serde::from_slice::(&bytes).map_err(ErrorKind::Decode)?) } fn to_bytes(&self) -> Result, Error> { Ok(Cow::from( rmp_serde::to_vec(&self.inner).map_err(ErrorKind::Encode)?, )) } fn into_target(self) -> Self::Target { self.inner } } /// All `OwnedArchive` values are cacheable. impl Cacheable for OwnedArchive where A: rkyv::Archive + for<'a> rkyv::Serialize> + Send + 'static, A::Archived: rkyv::Portable + rkyv::Deserialize + for<'a> rkyv::bytecheck::CheckBytes>, { type Target = Self; fn from_aligned_bytes(bytes: AlignedVec) -> Result { Self::new(bytes) } fn to_bytes(&self) -> Result, Error> { Ok(Cow::from(Self::as_bytes(self))) } fn into_target(self) -> Self::Target { self } } /// Dispatch type: Either a cached client error or a (user specified) error from the callback pub enum CachedClientError { Client { retries: Option, err: Error, }, Callback { retries: Option, err: CallbackError, }, } impl CachedClientError { /// Attach the number of retries to the error context. /// /// Adds to existing errors if any, in case different layers retried. fn with_retries(self, retries: u32) -> Self { match self { Self::Client { retries: existing_retries, err, } => Self::Client { retries: Some(existing_retries.unwrap_or_default() + retries), err, }, Self::Callback { retries: existing_retries, err, } => Self::Callback { retries: Some(existing_retries.unwrap_or_default() + retries), err, }, } } fn retries(&self) -> Option { match self { Self::Client { retries, .. } => *retries, Self::Callback { retries, .. } => *retries, } } fn error(&self) -> &(dyn std::error::Error + 'static) { match self { Self::Client { err, .. } => err, Self::Callback { err, .. } => err, } } } impl From for CachedClientError { fn from(error: Error) -> Self { Self::Client { retries: None, err: error, } } } impl From for CachedClientError { fn from(error: ErrorKind) -> Self { Self::Client { retries: None, err: error.into(), } } } impl + std::error::Error + 'static> From> for Error { /// Attach retry error context, if there were retries. fn from(error: CachedClientError) -> Self { match error { CachedClientError::Client { retries: Some(retries), err, } => Self::new(err.into_kind(), retries), CachedClientError::Client { retries: None, err } => err, CachedClientError::Callback { retries: Some(retries), err, } => Self::new(err.into().into_kind(), retries), CachedClientError::Callback { retries: None, err } => err.into(), } } } #[derive(Debug, Clone, Copy)] pub enum CacheControl<'a> { /// Respect the `cache-control` header from the response. None, /// Apply `max-age=0, must-revalidate` to the request. MustRevalidate, /// Allow the client to return stale responses. AllowStale, /// Override the cache control header with a custom value. Override(&'a str), } impl From for CacheControl<'_> { fn from(value: Freshness) -> Self { match value { Freshness::Fresh => Self::None, Freshness::Stale => Self::MustRevalidate, Freshness::Missing => Self::None, } } } /// Custom caching layer over [`reqwest::Client`]. /// /// The implementation takes inspiration from the `http-cache` crate, but adds support for running /// an async callback on the response before caching. We use this to e.g. store a /// parsed version of the wheel metadata and for our remote zip reader. In the latter case, we want /// to read a single file from a remote zip using range requests (so we don't have to download the /// entire file). We send a HEAD request in the caching layer to check if the remote file has /// changed (and if range requests are supported), and in the callback we make the actual range /// requests if required. /// /// Unlike `http-cache`, all outputs must be serializable/deserializable in some way, by /// implementing the `Cacheable` trait. /// /// Again unlike `http-cache`, the caller gets full control over the cache key with the assumption /// that it's a file. #[derive(Debug, Clone)] pub struct CachedClient(BaseClient); impl CachedClient { pub fn new(client: BaseClient) -> Self { Self(client) } /// The underlying [`BaseClient`] without caching. pub fn uncached(&self) -> &BaseClient { &self.0 } /// Make a cached request with a custom response transformation /// while using serde to (de)serialize cached responses. /// /// If a new response was received (no prior cached response or modified /// on the remote), the response is passed through `response_callback` and /// only the result is cached and returned. The `response_callback` is /// allowed to make subsequent requests, e.g. through the uncached client. #[instrument(skip_all)] pub async fn get_serde< Payload: Serialize + DeserializeOwned + Send + 'static, CallBackError: std::error::Error + 'static, Callback: AsyncFn(Response) -> Result, >( &self, req: Request, cache_entry: &CacheEntry, cache_control: CacheControl<'_>, response_callback: Callback, ) -> Result> { let payload = self .get_cacheable(req, cache_entry, cache_control, async |resp| { let payload = response_callback(resp).await?; Ok(SerdeCacheable { inner: payload }) }) .await?; Ok(payload) } /// Make a cached request with a custom response transformation while using /// the `Cacheable` trait to (de)serialize cached responses. /// /// The purpose of this routine is the use of `Cacheable`. Namely, it /// generalizes over (de)serialization such that mechanisms other than /// serde (such as rkyv) can be used to manage (de)serialization of cached /// data. /// /// If a new response was received (no prior cached response or modified /// on the remote), the response is passed through `response_callback` and /// only the result is cached and returned. The `response_callback` is /// allowed to make subsequent requests, e.g. through the uncached client. #[instrument(skip_all)] pub async fn get_cacheable< Payload: Cacheable, CallBackError: std::error::Error + 'static, Callback: AsyncFn(Response) -> Result, >( &self, req: Request, cache_entry: &CacheEntry, cache_control: CacheControl<'_>, response_callback: Callback, ) -> Result> { let fresh_req = req.try_clone().expect("HTTP request must be cloneable"); let cached_response = if let Some(cached) = Self::read_cache(cache_entry).await { self.send_cached(req, cache_control, cached) .boxed_local() .await? } else { debug!("No cache entry for: {}", req.url()); let (response, cache_policy) = self.fresh_request(req, cache_control).await?; CachedResponse::ModifiedOrNew { response, cache_policy, } }; match cached_response { CachedResponse::FreshCache(cached) => match Payload::from_aligned_bytes(cached.data) { Ok(payload) => Ok(payload), Err(err) => { warn!( "Broken fresh cache entry (for payload) at {}, removing: {err}", cache_entry.path().display() ); self.resend_and_heal_cache( fresh_req, cache_entry, cache_control, response_callback, ) .await } }, CachedResponse::NotModified { cached, new_policy } => { let refresh_cache = info_span!("refresh_cache", file = %cache_entry.path().display()); async { let data_with_cache_policy_bytes = DataWithCachePolicy::serialize(&new_policy, &cached.data)?; write_atomic(cache_entry.path(), data_with_cache_policy_bytes) .await .map_err(ErrorKind::CacheWrite)?; match Payload::from_aligned_bytes(cached.data) { Ok(payload) => Ok(payload), Err(err) => { warn!( "Broken fresh cache entry after revalidation \ (for payload) at {}, removing: {err}", cache_entry.path().display() ); self.resend_and_heal_cache( fresh_req, cache_entry, cache_control, response_callback, ) .await } } } .instrument(refresh_cache) .await } CachedResponse::ModifiedOrNew { response, cache_policy, } => { // If we got a modified response, but it's a 304, then a validator failed (e.g., the // ETag didn't match). We need to make a fresh request. if response.status() == http::StatusCode::NOT_MODIFIED { warn!("Server returned unusable 304 for: {}", fresh_req.url()); self.resend_and_heal_cache( fresh_req, cache_entry, cache_control, response_callback, ) .await } else { self.run_response_callback( cache_entry, cache_policy, response, response_callback, ) .await } } } } /// Make a request without checking whether the cache is fresh. pub async fn skip_cache< Payload: Serialize + DeserializeOwned + Send + 'static, CallBackError: std::error::Error + 'static, Callback: AsyncFnOnce(Response) -> Result, >( &self, req: Request, cache_entry: &CacheEntry, cache_control: CacheControl<'_>, response_callback: Callback, ) -> Result> { let (response, cache_policy) = self.fresh_request(req, cache_control).await?; let payload = self .run_response_callback(cache_entry, cache_policy, response, async |resp| { let payload = response_callback(resp).await?; Ok(SerdeCacheable { inner: payload }) }) .await?; Ok(payload) } async fn resend_and_heal_cache< Payload: Cacheable, CallBackError: std::error::Error + 'static, Callback: AsyncFnOnce(Response) -> Result, >( &self, req: Request, cache_entry: &CacheEntry, cache_control: CacheControl<'_>, response_callback: Callback, ) -> Result> { let _ = fs_err::tokio::remove_file(&cache_entry.path()).await; let (response, cache_policy) = self.fresh_request(req, cache_control).await?; self.run_response_callback(cache_entry, cache_policy, response, response_callback) .await } async fn run_response_callback< Payload: Cacheable, CallBackError: std::error::Error + 'static, Callback: AsyncFnOnce(Response) -> Result, >( &self, cache_entry: &CacheEntry, cache_policy: Option>, response: Response, response_callback: Callback, ) -> Result> { let new_cache = info_span!("new_cache", file = %cache_entry.path().display()); let data = response_callback(response) .boxed_local() .await .map_err(|err| CachedClientError::Callback { retries: None, err })?; let Some(cache_policy) = cache_policy else { return Ok(data.into_target()); }; async { fs_err::tokio::create_dir_all(cache_entry.dir()) .await .map_err(ErrorKind::CacheWrite)?; let data_with_cache_policy_bytes = DataWithCachePolicy::serialize(&cache_policy, &data.to_bytes()?)?; write_atomic(cache_entry.path(), data_with_cache_policy_bytes) .await .map_err(ErrorKind::CacheWrite)?; Ok(data.into_target()) } .instrument(new_cache) .await } #[instrument(name = "read_and_parse_cache", skip_all, fields(file = %cache_entry.path().display() ))] async fn read_cache(cache_entry: &CacheEntry) -> Option { match DataWithCachePolicy::from_path_async(cache_entry.path()).await { Ok(data) => Some(data), Err(err) => { // When we know the cache entry doesn't exist, then things are // normal and we shouldn't emit a WARN. if err.is_file_not_exists() { trace!("No cache entry exists for {}", cache_entry.path().display()); } else { warn!( "Broken cache policy entry at {}, removing: {err}", cache_entry.path().display() ); let _ = fs_err::tokio::remove_file(&cache_entry.path()).await; } None } } } /// Send a request given that we have a (possibly) stale cached response. /// /// If the cached response is valid but stale, then this will attempt a /// revalidation request. async fn send_cached( &self, mut req: Request, cache_control: CacheControl<'_>, cached: DataWithCachePolicy, ) -> Result { // Apply the cache control header, if necessary. match cache_control { CacheControl::None | CacheControl::AllowStale | CacheControl::Override(..) => {} CacheControl::MustRevalidate => { req.headers_mut().insert( http::header::CACHE_CONTROL, http::HeaderValue::from_static("no-cache"), ); } } Ok(match cached.cache_policy.before_request(&mut req) { BeforeRequest::Fresh => { debug!("Found fresh response for: {}", req.url()); CachedResponse::FreshCache(cached) } BeforeRequest::Stale(new_cache_policy_builder) => match cache_control { CacheControl::None | CacheControl::MustRevalidate | CacheControl::Override(_) => { debug!("Found stale response for: {}", req.url()); self.send_cached_handle_stale( req, cache_control, cached, new_cache_policy_builder, ) .await? } CacheControl::AllowStale => { debug!("Found stale (but allowed) response for: {}", req.url()); CachedResponse::FreshCache(cached) } }, BeforeRequest::NoMatch => { // This shouldn't happen; if it does, we'll override the cache. warn!( "Cached response doesn't match current request for: {}", req.url() ); let (response, cache_policy) = self.fresh_request(req, cache_control).await?; CachedResponse::ModifiedOrNew { response, cache_policy, } } }) } async fn send_cached_handle_stale( &self, req: Request, cache_control: CacheControl<'_>, cached: DataWithCachePolicy, new_cache_policy_builder: CachePolicyBuilder, ) -> Result { let url = DisplaySafeUrl::from_url(req.url().clone()); debug!("Sending revalidation request for: {url}"); let mut response = self .0 .execute(req) .instrument(info_span!("revalidation_request", url = url.as_str())) .await .map_err(|err| ErrorKind::from_reqwest_middleware(url.clone(), err))?; // Check for HTTP error status and extract problem details if available if let Err(status_error) = response.error_for_status_ref() { // Clone the response to extract problem details before the error consumes it let problem_details = if response .headers() .get("content-type") .and_then(|ct| ct.to_str().ok()) .map(|ct| ct == "application/problem+json") .unwrap_or(false) { extract_problem_details(response).await } else { None }; return Err(ErrorKind::from_reqwest_with_problem_details( url.clone(), status_error, problem_details, ) .into()); } // If the user set a custom `Cache-Control` header, override it. if let CacheControl::Override(header) = cache_control { response.headers_mut().insert( http::header::CACHE_CONTROL, http::HeaderValue::from_str(header) .expect("Cache-Control header must be valid UTF-8"), ); } match cached .cache_policy .after_response(new_cache_policy_builder, &response) { AfterResponse::NotModified(new_policy) => { debug!("Found not-modified response for: {url}"); Ok(CachedResponse::NotModified { cached, new_policy: Box::new(new_policy), }) } AfterResponse::Modified(new_policy) => { debug!("Found modified response for: {url}"); Ok(CachedResponse::ModifiedOrNew { response, cache_policy: new_policy .to_archived() .is_storable() .then(|| Box::new(new_policy)), }) } } } #[instrument(skip_all, fields(url = req.url().as_str()))] async fn fresh_request( &self, req: Request, cache_control: CacheControl<'_>, ) -> Result<(Response, Option>), Error> { let url = DisplaySafeUrl::from_url(req.url().clone()); trace!("Sending fresh {} request for {}", req.method(), url); let cache_policy_builder = CachePolicyBuilder::new(&req); let mut response = self .0 .execute(req) .await .map_err(|err| ErrorKind::from_reqwest_middleware(url.clone(), err))?; // If the user set a custom `Cache-Control` header, override it. if let CacheControl::Override(header) = cache_control { response.headers_mut().insert( http::header::CACHE_CONTROL, http::HeaderValue::from_str(header) .expect("Cache-Control header must be valid UTF-8"), ); } let retry_count = response .extensions() .get::() .map(|retries| retries.value()); if let Err(status_error) = response.error_for_status_ref() { let problem_details = if response .headers() .get("content-type") .and_then(|ct| ct.to_str().ok()) .map(|ct| ct.starts_with("application/problem+json")) .unwrap_or(false) { extract_problem_details(response).await } else { None }; return Err(CachedClientError::::Client { retries: retry_count, err: ErrorKind::from_reqwest_with_problem_details( url, status_error, problem_details, ) .into(), } .into()); } let cache_policy = cache_policy_builder.build(&response); let cache_policy = if cache_policy.to_archived().is_storable() { Some(Box::new(cache_policy)) } else { None }; Ok((response, cache_policy)) } /// Perform a [`CachedClient::get_serde`] request with a default retry strategy. #[instrument(skip_all)] pub async fn get_serde_with_retry< Payload: Serialize + DeserializeOwned + Send + 'static, CallBackError: std::error::Error + 'static, Callback: AsyncFn(Response) -> Result, >( &self, req: Request, cache_entry: &CacheEntry, cache_control: CacheControl<'_>, response_callback: Callback, ) -> Result> { let payload = self .get_cacheable_with_retry(req, cache_entry, cache_control, async |resp| { let payload = response_callback(resp).await?; Ok(SerdeCacheable { inner: payload }) }) .await?; Ok(payload) } /// Perform a [`CachedClient::get_cacheable`] request with a default retry strategy. /// /// See: #[instrument(skip_all)] pub async fn get_cacheable_with_retry< Payload: Cacheable, CallBackError: std::error::Error + 'static, Callback: AsyncFn(Response) -> Result, >( &self, req: Request, cache_entry: &CacheEntry, cache_control: CacheControl<'_>, response_callback: Callback, ) -> Result> { let mut past_retries = 0; let start_time = SystemTime::now(); let retry_policy = self.uncached().retry_policy(); loop { let fresh_req = req.try_clone().expect("HTTP request must be cloneable"); let result = self .get_cacheable(fresh_req, cache_entry, cache_control, &response_callback) .await; // Check if the middleware already performed retries let middleware_retries = match &result { Err(err) => err.retries().unwrap_or_default(), Ok(_) => 0, }; if result .as_ref() .is_err_and(|err| is_transient_network_error(err.error())) { // If middleware already retried, consider that in our retry budget let total_retries = past_retries + middleware_retries; let retry_decision = retry_policy.should_retry(start_time, total_retries); if let reqwest_retry::RetryDecision::Retry { execute_after } = retry_decision { let duration = execute_after .duration_since(SystemTime::now()) .unwrap_or_else(|_| Duration::default()); debug!( "Transient failure while handling response from {}; retrying after {:.1}s...", req.url(), duration.as_secs_f32(), ); tokio::time::sleep(duration).await; past_retries += 1; continue; } } if past_retries > 0 { return result.map_err(|err| err.with_retries(past_retries)); } return result; } } /// Perform a [`CachedClient::skip_cache`] request with a default retry strategy. /// /// See: pub async fn skip_cache_with_retry< Payload: Serialize + DeserializeOwned + Send + 'static, CallBackError: std::error::Error + 'static, Callback: AsyncFn(Response) -> Result, >( &self, req: Request, cache_entry: &CacheEntry, cache_control: CacheControl<'_>, response_callback: Callback, ) -> Result> { let mut past_retries = 0; let start_time = SystemTime::now(); let retry_policy = self.uncached().retry_policy(); loop { let fresh_req = req.try_clone().expect("HTTP request must be cloneable"); let result = self .skip_cache(fresh_req, cache_entry, cache_control, &response_callback) .await; // Check if the middleware already performed retries let middleware_retries = match &result { Err(err) => err.retries().unwrap_or_default(), _ => 0, }; if result .as_ref() .err() .is_some_and(|err| is_transient_network_error(err.error())) { let total_retries = past_retries + middleware_retries; let retry_decision = retry_policy.should_retry(start_time, total_retries); if let reqwest_retry::RetryDecision::Retry { execute_after } = retry_decision { let duration = execute_after .duration_since(SystemTime::now()) .unwrap_or_else(|_| Duration::default()); debug!( "Transient failure while handling response from {}; retrying after {}s...", req.url(), duration.as_secs(), ); tokio::time::sleep(duration).await; past_retries += 1; continue; } } if past_retries > 0 { return result.map_err(|err| err.with_retries(past_retries)); } return result; } } } #[derive(Debug)] enum CachedResponse { /// The cached response is fresh without an HTTP request (e.g. age < max-age). FreshCache(DataWithCachePolicy), /// The cached response is fresh after an HTTP request (e.g. 304 not modified) NotModified { /// The cached response (with its old cache policy). cached: DataWithCachePolicy, /// The new [`CachePolicy`] is used to determine if the response /// is fresh or stale when making subsequent requests for the same /// resource. This policy should overwrite the old policy associated /// with the cached response. In particular, this new policy is derived /// from data received in a revalidation response, which might change /// the parameters of cache behavior. /// /// The policy is large (352 bytes at time of writing), so we reduce /// the stack size by boxing it. new_policy: Box, }, /// There was no prior cached response or the cache was outdated /// /// The cache policy is `None` if it isn't storable ModifiedOrNew { /// The response received from the server. response: Response, /// The [`CachePolicy`] is used to determine if the response is fresh or /// stale when making subsequent requests for the same resource. /// /// The policy is large (352 bytes at time of writing), so we reduce /// the stack size by boxing it. cache_policy: Option>, }, } /// Represents an arbitrary data blob with an associated HTTP cache policy. /// /// The cache policy is used to determine whether the data blob is stale or /// not. /// /// # Format /// /// This type encapsulates the format for how blobs of data are stored on /// disk. The format is very simple. First, the blob of data is written as-is. /// Second, the archived representation of a `CachePolicy` is written. Thirdly, /// the length, in bytes, of the archived `CachePolicy` is written as a 64-bit /// little endian integer. /// /// Reading the format is done via an `AlignedVec` so that `rkyv` can correctly /// read the archived representation of the data blob. The cache policy is /// split into its own `AlignedVec` allocation. /// /// # Future ideas /// /// This format was also chosen because it should in theory permit rewriting /// the cache policy without needing to rewrite the data blob if the blob has /// not changed. For example, this case occurs when a revalidation request /// responds with HTTP 304 NOT MODIFIED. At time of writing, this is not yet /// implemented because 1) the synchronization specifics of mutating a cache /// file have not been worked out and 2) it's not clear if it's a win. /// /// An alternative format would be to write the cache policy and the /// blob in two distinct files. This would avoid needing to worry about /// synchronization, but it means reading two files instead of one for every /// cached response in the fast path. It's unclear whether it's worth it. /// (Experiments have not yet been done.) /// /// Another approach here would be to memory map the file and rejigger /// `OwnedArchive` (or create a new type) that works with a memory map instead /// of an `AlignedVec`. This will require care to ensure alignment is handled /// correctly. This approach has not been litigated yet. I did not start with /// it because experiments with ripgrep have tended to show that (on Linux) /// memory mapping a bunch of small files ends up being quite a bit slower than /// just reading them on to the heap. #[derive(Debug)] pub struct DataWithCachePolicy { pub data: AlignedVec, cache_policy: OwnedArchive, } impl DataWithCachePolicy { /// Loads cached data and its associated HTTP cache policy from the given /// file path in an asynchronous fashion (via `spawn_blocking`). /// /// # Errors /// /// If the given byte buffer is not in a valid format or if reading the /// file given fails, then this returns an error. async fn from_path_async(path: &Path) -> Result { let path = path.to_path_buf(); tokio::task::spawn_blocking(move || Self::from_path_sync(&path)) .await // This just forwards panics from the closure. .unwrap() } /// Loads cached data and its associated HTTP cache policy from the given /// file path in a synchronous fashion. /// /// # Errors /// /// If the given byte buffer is not in a valid format or if reading the /// file given fails, then this returns an error. #[instrument] fn from_path_sync(path: &Path) -> Result { let file = fs_err::File::open(path).map_err(ErrorKind::Io)?; // Note that we don't wrap our file in a buffer because it will just // get passed to AlignedVec::extend_from_reader, which doesn't benefit // from an intermediary buffer. In effect, the AlignedVec acts as the // buffer. Self::from_reader(file) } /// Loads cached data and its associated HTTP cache policy from the given /// reader. /// /// # Errors /// /// If the given byte buffer is not in a valid format or if the reader /// fails, then this returns an error. pub fn from_reader(mut rdr: impl std::io::Read) -> Result { let mut aligned_bytes = AlignedVec::new(); aligned_bytes .extend_from_reader(&mut rdr) .map_err(ErrorKind::Io)?; Self::from_aligned_bytes(aligned_bytes) } /// Loads cached data and its associated HTTP cache policy form an in /// memory byte buffer. /// /// # Errors /// /// If the given byte buffer is not in a valid format, then this /// returns an error. fn from_aligned_bytes(mut bytes: AlignedVec) -> Result { let cache_policy = Self::deserialize_cache_policy(&mut bytes)?; Ok(Self { data: bytes, cache_policy, }) } /// Serializes the given cache policy and arbitrary data blob to an in /// memory byte buffer. /// /// # Errors /// /// If there was a problem converting the given cache policy to its /// serialized representation, then this routine will return an error. fn serialize(cache_policy: &CachePolicy, data: &[u8]) -> Result, Error> { let mut buf = vec![]; Self::serialize_to_writer(cache_policy, data, &mut buf)?; Ok(buf) } /// Serializes the given cache policy and arbitrary data blob to the given /// writer. /// /// # Errors /// /// If there was a problem converting the given cache policy to its /// serialized representation or if the writer returns an error, then /// this routine will return an error. fn serialize_to_writer( cache_policy: &CachePolicy, data: &[u8], mut wtr: impl std::io::Write, ) -> Result<(), Error> { let cache_policy_archived = OwnedArchive::from_unarchived(cache_policy)?; let cache_policy_bytes = OwnedArchive::as_bytes(&cache_policy_archived); wtr.write_all(data).map_err(ErrorKind::Io)?; wtr.write_all(cache_policy_bytes).map_err(ErrorKind::Io)?; let len = u64::try_from(cache_policy_bytes.len()).map_err(|_| { let msg = format!( "failed to represent {} (length of cache policy) in a u64", cache_policy_bytes.len() ); ErrorKind::Io(std::io::Error::other(msg)) })?; wtr.write_all(&len.to_le_bytes()).map_err(ErrorKind::Io)?; Ok(()) } /// Deserializes a `OwnedArchive` off the end of the given /// aligned bytes. Upon success, the given bytes will only contain the /// data itself. The bytes representing the cached policy will have been /// removed. /// /// # Errors /// /// This returns an error if the cache policy could not be deserialized /// from the end of the given bytes. fn deserialize_cache_policy( bytes: &mut AlignedVec, ) -> Result, Error> { let len = Self::deserialize_cache_policy_len(bytes)?; let cache_policy_bytes_start = bytes.len() - (len + 8); let cache_policy_bytes = &bytes[cache_policy_bytes_start..][..len]; let mut cache_policy_bytes_aligned = AlignedVec::with_capacity(len); cache_policy_bytes_aligned.extend_from_slice(cache_policy_bytes); assert!( cache_policy_bytes_start <= bytes.len(), "slicing cache policy should result in a truncation" ); // Technically this will keep the extra capacity used to store the // cache policy around. But it should be pretty small, and it saves a // realloc. (It's unclear whether that matters more or less than the // extra memory usage.) bytes.resize(cache_policy_bytes_start, 0); OwnedArchive::new(cache_policy_bytes_aligned) } /// Deserializes the length, in bytes, of the cache policy given a complete /// serialized byte buffer of a `DataWithCachePolicy`. /// /// Upon success, callers are guaranteed that /// `&bytes[bytes.len() - (len + 8)..][..len]` will not panic. /// /// # Errors /// /// This returns an error if the length could not be read as a `usize` or is /// otherwise known to be invalid. (For example, it is a length that is bigger /// than `bytes.len()`.) fn deserialize_cache_policy_len(bytes: &[u8]) -> Result { let Some(cache_policy_len_start) = bytes.len().checked_sub(8) else { let msg = format!( "data-with-cache-policy buffer should be at least 8 bytes \ in length, but is {} bytes", bytes.len(), ); return Err(ErrorKind::ArchiveRead(msg).into()); }; let cache_policy_len_bytes = <[u8; 8]>::try_from(&bytes[cache_policy_len_start..]) .expect("cache policy length is 8 bytes"); let len_u64 = u64::from_le_bytes(cache_policy_len_bytes); let Ok(len_usize) = usize::try_from(len_u64) else { let msg = format!( "data-with-cache-policy has cache policy length of {len_u64}, \ but overflows usize", ); return Err(ErrorKind::ArchiveRead(msg).into()); }; if bytes.len() < len_usize + 8 { let msg = format!( "invalid cache entry: data-with-cache-policy has cache policy length of {}, \ but total buffer size is {}", len_usize, bytes.len(), ); return Err(ErrorKind::ArchiveRead(msg).into()); } Ok(len_usize) } } uv-0.9.17+ds1/crates/uv-client/src/error.rs000066400000000000000000000534541520155276700204270ustar00rootroot00000000000000use async_http_range_reader::AsyncHttpRangeReaderError; use async_zip::error::ZipError; use serde::Deserialize; use std::fmt::{Display, Formatter}; use std::ops::Deref; use std::path::PathBuf; use uv_distribution_filename::{WheelFilename, WheelFilenameError}; use uv_fs::LockedFileError; use uv_normalize::PackageName; use uv_redacted::DisplaySafeUrl; use crate::middleware::OfflineError; use crate::{FlatIndexError, html}; /// RFC 9457 Problem Details for HTTP APIs /// /// This structure represents the standard format for machine-readable details /// of errors in HTTP response bodies as defined in RFC 9457. #[derive(Debug, Clone, Deserialize)] pub struct ProblemDetails { /// A URI reference that identifies the problem type. /// When dereferenced, it SHOULD provide human-readable documentation for the problem type. #[serde(rename = "type", default = "default_problem_type")] pub problem_type: String, /// A short, human-readable summary of the problem type. pub title: Option, /// The HTTP status code generated by the origin server for this occurrence of the problem. pub status: Option, /// A human-readable explanation specific to this occurrence of the problem. pub detail: Option, /// A URI reference that identifies the specific occurrence of the problem. pub instance: Option, } /// Default problem type URI as per RFC 9457 #[inline] fn default_problem_type() -> String { "about:blank".to_string() } impl ProblemDetails { /// Get a human-readable description of the problem pub fn description(&self) -> Option { match self { Self { title: Some(title), detail: Some(detail), .. } => Some(format!("Server message: {title}, {detail}")), Self { title: Some(title), .. } => Some(format!("Server message: {title}")), Self { detail: Some(detail), .. } => Some(format!("Server message: {detail}")), Self { status: Some(status), .. } => Some(format!("HTTP error {status}")), _ => None, } } } #[derive(Debug)] pub struct Error { kind: Box, retries: u32, } impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if self.retries > 0 { write!( f, "Request failed after {retries} {subject}", retries = self.retries, subject = if self.retries > 1 { "retries" } else { "retry" } ) } else { Display::fmt(&self.kind, f) } } } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { if self.retries > 0 { Some(&self.kind) } else { self.kind.source() } } } impl Error { /// Create a new [`Error`] with the given [`ErrorKind`] and number of retries. pub fn new(kind: ErrorKind, retries: u32) -> Self { Self { kind: Box::new(kind), retries, } } /// Return the number of retries that were attempted before this error was returned. pub fn retries(&self) -> u32 { self.retries } /// Convert this error into an [`ErrorKind`]. pub fn into_kind(self) -> ErrorKind { *self.kind } /// Return the [`ErrorKind`] of this error. pub fn kind(&self) -> &ErrorKind { &self.kind } /// Create a new error from a JSON parsing error. pub(crate) fn from_json_err(err: serde_json::Error, url: DisplaySafeUrl) -> Self { ErrorKind::BadJson { source: err, url }.into() } /// Create a new error from an HTML parsing error. pub(crate) fn from_html_err(err: html::Error, url: DisplaySafeUrl) -> Self { ErrorKind::BadHtml { source: err, url }.into() } /// Create a new error from a `MessagePack` parsing error. pub(crate) fn from_msgpack_err(err: rmp_serde::decode::Error, url: DisplaySafeUrl) -> Self { ErrorKind::BadMessagePack { source: err, url }.into() } /// Returns `true` if this error corresponds to an offline error. pub(crate) fn is_offline(&self) -> bool { matches!(&*self.kind, ErrorKind::Offline(_)) } /// Returns `true` if this error corresponds to an I/O "not found" error. pub(crate) fn is_file_not_exists(&self) -> bool { let ErrorKind::Io(err) = &*self.kind else { return false; }; matches!(err.kind(), std::io::ErrorKind::NotFound) } /// Returns `true` if the error is due to an SSL error. pub fn is_ssl(&self) -> bool { matches!(&*self.kind, ErrorKind::WrappedReqwestError(.., err) if err.is_ssl()) } /// Returns `true` if the error is due to the server not supporting HTTP range requests. pub fn is_http_range_requests_unsupported(&self) -> bool { match &*self.kind { // The server doesn't support range requests (as reported by the `HEAD` check). ErrorKind::AsyncHttpRangeReader( _, AsyncHttpRangeReaderError::HttpRangeRequestUnsupported, ) => { return true; } // The server doesn't support range requests (it doesn't return the necessary headers). ErrorKind::AsyncHttpRangeReader( _, AsyncHttpRangeReaderError::ContentLengthMissing | AsyncHttpRangeReaderError::ContentRangeMissing, ) => { return true; } // The server returned a "Method Not Allowed" error, indicating it doesn't support // HEAD requests, so we can't check for range requests. ErrorKind::WrappedReqwestError(_, err) => { if let Some(status) = err.status() { // If the server doesn't support HEAD requests, we can't check for range // requests. if status == reqwest::StatusCode::METHOD_NOT_ALLOWED { return true; } // In some cases, registries return a 404 for HEAD requests when they're not // supported. In the worst case, we'll now just proceed to attempt to stream the // entire file, so it's fine to be somewhat lenient here. if status == reqwest::StatusCode::NOT_FOUND { return true; } // In some cases, registries (like PyPICloud) return a 403 for HEAD requests // when they're not supported. Again, it's better to be lenient here. if status == reqwest::StatusCode::FORBIDDEN { return true; } // In some cases, registries (like Alibaba Cloud) return a 400 for HEAD requests // when they're not supported. Again, it's better to be lenient here. if status == reqwest::StatusCode::BAD_REQUEST { return true; } } } // The server doesn't support range requests, but we only discovered this while // unzipping due to erroneous server behavior. ErrorKind::Zip(_, ZipError::UpstreamReadError(err)) => { if let Some(inner) = err.get_ref() { if let Some(inner) = inner.downcast_ref::() { if matches!( inner, AsyncHttpRangeReaderError::HttpRangeRequestUnsupported ) { return true; } } } } _ => {} } false } /// Returns `true` if the error is due to the server not supporting HTTP streaming. Most /// commonly, this is due to serving ZIP files with features that are incompatible with /// streaming, like data descriptors. pub fn is_http_streaming_unsupported(&self) -> bool { matches!( &*self.kind, ErrorKind::Zip(_, ZipError::FeatureNotSupported(_)) ) } } impl From for Error { fn from(kind: ErrorKind) -> Self { Self { kind: Box::new(kind), retries: 0, } } } #[derive(Debug, thiserror::Error)] pub enum ErrorKind { #[error(transparent)] InvalidUrl(#[from] uv_distribution_types::ToUrlError), #[error(transparent)] Flat(#[from] FlatIndexError), #[error("Expected a file URL, but received: {0}")] NonFileUrl(DisplaySafeUrl), #[error("Expected an index URL, but received non-base URL: {0}")] CannotBeABase(DisplaySafeUrl), #[error("Failed to read metadata: `{0}`")] Metadata(String, #[source] uv_metadata::Error), #[error("{0} isn't available locally, but making network requests to registries was banned")] NoIndex(String), /// The package was not found in the registry. /// /// Make sure the package name is spelled correctly and that you've /// configured the right registry to fetch it from. #[error("Package `{0}` was not found in the registry")] RemotePackageNotFound(PackageName), /// The package was not found in the local (file-based) index. #[error("Package `{0}` was not found in the local index")] LocalPackageNotFound(PackageName), /// The root was not found in the local (file-based) index. #[error("Local index not found at: `{}`", _0.display())] LocalIndexNotFound(PathBuf), /// The metadata file could not be parsed. #[error("Couldn't parse metadata of {0} from {1}")] MetadataParseError( WheelFilename, String, #[source] Box, ), /// An error that happened while making a request or in a reqwest middleware. #[error("Failed to fetch: `{0}`")] WrappedReqwestError(DisplaySafeUrl, #[source] WrappedReqwestError), /// Add the number of failed retries to the error. #[error("Request failed after {retries} {subject}", subject = if *retries > 1 { "retries" } else { "retry" })] RequestWithRetries { source: Box, retries: u32, }, #[error("Received some unexpected JSON from {}", url)] BadJson { source: serde_json::Error, url: DisplaySafeUrl, }, #[error("Received some unexpected HTML from {}", url)] BadHtml { source: html::Error, url: DisplaySafeUrl, }, #[error("Received some unexpected MessagePack from {}", url)] BadMessagePack { source: rmp_serde::decode::Error, url: DisplaySafeUrl, }, #[error("Failed to read zip with range requests: `{0}`")] AsyncHttpRangeReader(DisplaySafeUrl, #[source] AsyncHttpRangeReaderError), #[error("{0} is not a valid wheel filename")] WheelFilename(#[source] WheelFilenameError), #[error("Package metadata name `{metadata}` does not match given name `{given}`")] NameMismatch { given: PackageName, metadata: PackageName, }, #[error("Failed to unzip wheel: {0}")] Zip(WheelFilename, #[source] ZipError), #[error("Failed to write to the client cache")] CacheWrite(#[source] std::io::Error), #[error("Failed to acquire lock on the client cache")] CacheLock(#[source] LockedFileError), #[error(transparent)] Io(std::io::Error), #[error("Cache deserialization failed")] Decode(#[source] rmp_serde::decode::Error), #[error("Cache serialization failed")] Encode(#[source] rmp_serde::encode::Error), #[error("Missing `Content-Type` header for {0}")] MissingContentType(DisplaySafeUrl), #[error("Invalid `Content-Type` header for {0}")] InvalidContentTypeHeader(DisplaySafeUrl, #[source] http::header::ToStrError), #[error("Unsupported `Content-Type` \"{1}\" for {0}. Expected JSON or HTML.")] UnsupportedMediaType(DisplaySafeUrl, String), #[error("Reading from cache archive failed: {0}")] ArchiveRead(String), #[error("Writing to cache archive failed: {0}")] ArchiveWrite(String), #[error( "Network connectivity is disabled, but the requested data wasn't found in the cache for: `{0}`" )] Offline(String), #[error("Invalid cache control header: `{0}`")] InvalidCacheControl(String), } impl ErrorKind { /// Create an [`ErrorKind`] from a [`reqwest::Error`]. pub(crate) fn from_reqwest(url: DisplaySafeUrl, error: reqwest::Error) -> Self { Self::WrappedReqwestError(url, WrappedReqwestError::from(error)) } /// Create an [`ErrorKind`] from a [`reqwest_middleware::Error`]. pub(crate) fn from_reqwest_middleware( url: DisplaySafeUrl, err: reqwest_middleware::Error, ) -> Self { if let reqwest_middleware::Error::Middleware(ref underlying) = err { if let Some(err) = underlying.downcast_ref::() { return Self::Offline(err.url().to_string()); } } Self::WrappedReqwestError(url, WrappedReqwestError::from(err)) } /// Create an [`ErrorKind`] from a [`reqwest::Error`] with problem details. pub(crate) fn from_reqwest_with_problem_details( url: DisplaySafeUrl, error: reqwest::Error, problem_details: Option, ) -> Self { Self::WrappedReqwestError( url, WrappedReqwestError::with_problem_details(error.into(), problem_details), ) } } /// Handle the case with no internet by explicitly telling the user instead of showing an obscure /// DNS error. /// /// Wraps a [`reqwest_middleware::Error`] instead of an [`reqwest::Error`] since the actual reqwest /// error may be below some context in the [`anyhow::Error`]. #[derive(Debug)] pub struct WrappedReqwestError { error: reqwest_middleware::Error, problem_details: Option>, } impl WrappedReqwestError { /// Create a new `WrappedReqwestError` with optional problem details pub fn with_problem_details( error: reqwest_middleware::Error, problem_details: Option, ) -> Self { Self { error, problem_details: problem_details.map(Box::new), } } /// Return the inner [`reqwest::Error`] from the error chain, if it exists. fn inner(&self) -> Option<&reqwest::Error> { match &self.error { reqwest_middleware::Error::Reqwest(err) => Some(err), reqwest_middleware::Error::Middleware(err) => err.chain().find_map(|err| { if let Some(err) = err.downcast_ref::() { Some(err) } else if let Some(reqwest_middleware::Error::Reqwest(err)) = err.downcast_ref::() { Some(err) } else { None } }), } } /// Check if the error chain contains a `reqwest` error that looks like this: /// * error sending request for url (...) /// * client error (Connect) /// * dns error: failed to lookup address information: Name or service not known /// * failed to lookup address information: Name or service not known fn is_likely_offline(&self) -> bool { if let Some(reqwest_err) = self.inner() { if !reqwest_err.is_connect() { return false; } // Self is "error sending request for url", the first source is "error trying to connect", // the second source is "dns error". We have to check for the string because hyper errors // are opaque. if std::error::Error::source(&reqwest_err) .and_then(|err| err.source()) .is_some_and(|err| err.to_string().starts_with("dns error: ")) { return true; } } false } /// Check if the error chain contains a `reqwest` error that looks like this: /// * invalid peer certificate: `UnknownIssuer` fn is_ssl(&self) -> bool { if let Some(reqwest_err) = self.inner() { if !reqwest_err.is_connect() { return false; } // Self is "error sending request for url", the first source is "error trying to connect", // the second source is "dns error". We have to check for the string because hyper errors // are opaque. if std::error::Error::source(&reqwest_err) .and_then(|err| err.source()) .is_some_and(|err| err.to_string().starts_with("invalid peer certificate: ")) { return true; } } false } } impl From for WrappedReqwestError { fn from(error: reqwest::Error) -> Self { Self { error: error.into(), problem_details: None, } } } impl From for WrappedReqwestError { fn from(error: reqwest_middleware::Error) -> Self { Self { error, problem_details: None, } } } impl Deref for WrappedReqwestError { type Target = reqwest_middleware::Error; fn deref(&self) -> &Self::Target { &self.error } } impl Display for WrappedReqwestError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if self.is_likely_offline() { // Insert an extra hint, we'll show the wrapped error through `source` f.write_str("Could not connect, are you offline?") } else if let Some(problem_details) = &self.problem_details { // Show problem details if available match problem_details.description() { None => Display::fmt(&self.error, f), Some(message) => f.write_str(&message), } } else { // Show the wrapped error Display::fmt(&self.error, f) } } } impl std::error::Error for WrappedReqwestError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { if self.is_likely_offline() { // `Display` is inserting an extra message, so we need to show the wrapped error Some(&self.error) } else if self.problem_details.is_some() { // `Display` is showing problem details, so show the wrapped error as source Some(&self.error) } else { // `Display` is showing the wrapped error, continue with its source self.error.source() } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_problem_details_parsing() { let json = r#"{ "type": "https://example.com/probs/out-of-credit", "title": "You do not have enough credit.", "detail": "Your current balance is 30, but that costs 50.", "status": 403, "instance": "/account/12345/msgs/abc" }"#; let problem_details: ProblemDetails = serde_json::from_slice(json.as_bytes()).unwrap(); assert_eq!( problem_details.problem_type, "https://example.com/probs/out-of-credit" ); assert_eq!( problem_details.title, Some("You do not have enough credit.".to_string()) ); assert_eq!( problem_details.detail, Some("Your current balance is 30, but that costs 50.".to_string()) ); assert_eq!(problem_details.status, Some(403)); assert_eq!( problem_details.instance, Some("/account/12345/msgs/abc".to_string()) ); } #[test] fn test_problem_details_default_type() { let json = r#"{ "detail": "Something went wrong", "status": 500 }"#; let problem_details: ProblemDetails = serde_json::from_slice(json.as_bytes()).unwrap(); assert_eq!(problem_details.problem_type, "about:blank"); assert_eq!( problem_details.detail, Some("Something went wrong".to_string()) ); assert_eq!(problem_details.status, Some(500)); } #[test] fn test_problem_details_description() { let json = r#"{ "detail": "Detailed error message", "title": "Error Title", "status": 400 }"#; let problem_details: ProblemDetails = serde_json::from_slice(json.as_bytes()).unwrap(); assert_eq!( problem_details.description().unwrap(), "Server message: Error Title, Detailed error message" ); let json_no_detail = r#"{ "title": "Error Title", "status": 400 }"#; let problem_details: ProblemDetails = serde_json::from_slice(json_no_detail.as_bytes()).unwrap(); assert_eq!( problem_details.description().unwrap(), "Server message: Error Title" ); let json_minimal = r#"{ "status": 400 }"#; let problem_details: ProblemDetails = serde_json::from_slice(json_minimal.as_bytes()).unwrap(); assert_eq!(problem_details.description().unwrap(), "HTTP error 400"); } #[test] fn test_problem_details_with_extensions() { let json = r#"{ "type": "https://example.com/probs/out-of-credit", "title": "You do not have enough credit.", "detail": "Your current balance is 30, but that costs 50.", "status": 403, "balance": 30, "accounts": ["/account/12345", "/account/67890"] }"#; let problem_details: ProblemDetails = serde_json::from_slice(json.as_bytes()).unwrap(); assert_eq!( problem_details.title, Some("You do not have enough credit.".to_string()) ); } } uv-0.9.17+ds1/crates/uv-client/src/flat_index.rs000066400000000000000000000307651520155276700214130ustar00rootroot00000000000000use std::path::{Path, PathBuf}; use futures::{FutureExt, StreamExt}; use reqwest::Response; use tracing::{Instrument, debug, info_span, warn}; use url::Url; use uv_cache::{Cache, CacheBucket}; use uv_cache_key::cache_digest; use uv_distribution_filename::DistFilename; use uv_distribution_types::{File, FileLocation, IndexUrl, UrlString}; use uv_pypi_types::HashDigests; use uv_redacted::DisplaySafeUrl; use uv_small_str::SmallString; use crate::cached_client::{CacheControl, CachedClientError}; use crate::html::SimpleDetailHTML; use crate::{CachedClient, Connectivity, Error, ErrorKind, OwnedArchive}; #[derive(Debug, thiserror::Error)] pub enum FlatIndexError { #[error("Expected a file URL, but received: {0}")] NonFileUrl(DisplaySafeUrl), #[error("Failed to read `--find-links` directory: {0}")] FindLinksDirectory(PathBuf, #[source] FindLinksDirectoryError), #[error("Failed to read `--find-links` URL: {0}")] FindLinksUrl(DisplaySafeUrl, #[source] Error), } #[derive(Debug, thiserror::Error)] pub enum FindLinksDirectoryError { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] VerbatimUrl(#[from] uv_pep508::VerbatimUrlError), } /// An entry in a `--find-links` index. #[derive(Debug, Clone)] pub struct FlatIndexEntry { pub filename: DistFilename, pub file: File, pub index: IndexUrl, } #[derive(Debug, Default, Clone)] pub struct FlatIndexEntries { /// The list of `--find-links` entries. pub entries: Vec, /// Whether any `--find-links` entries could not be resolved due to a lack of network /// connectivity. pub offline: bool, } impl FlatIndexEntries { /// Create a [`FlatIndexEntries`] from a list of `--find-links` entries. fn from_entries(entries: Vec) -> Self { Self { entries, offline: false, } } /// Create a [`FlatIndexEntries`] to represent an offline `--find-links` entry. fn offline() -> Self { Self { entries: Vec::new(), offline: true, } } /// Extend this list of `--find-links` entries with another list. fn extend(&mut self, other: Self) { self.entries.extend(other.entries); self.offline |= other.offline; } /// Return the number of `--find-links` entries. fn len(&self) -> usize { self.entries.len() } /// Return `true` if there are no `--find-links` entries. fn is_empty(&self) -> bool { self.entries.is_empty() } } /// A client for reading distributions from `--find-links` entries (either local directories or /// remote HTML indexes). #[derive(Debug, Clone)] pub struct FlatIndexClient<'a> { client: &'a CachedClient, connectivity: Connectivity, cache: &'a Cache, } impl<'a> FlatIndexClient<'a> { /// Create a new [`FlatIndexClient`]. pub fn new(client: &'a CachedClient, connectivity: Connectivity, cache: &'a Cache) -> Self { Self { client, connectivity, cache, } } /// Read the directories and flat remote indexes from `--find-links`. pub async fn fetch_all( &self, indexes: impl Iterator, ) -> Result { let mut fetches = futures::stream::iter(indexes) .map(async |index| { let entries = self.fetch_index(index).await?; if entries.is_empty() { warn!("No packages found in `--find-links` entry: {}", index); } else { debug!( "Found {} package{} in `--find-links` entry: {}", entries.len(), if entries.len() == 1 { "" } else { "s" }, index ); } Ok::(entries) }) .buffered(16); let mut results = FlatIndexEntries::default(); while let Some(entries) = fetches.next().await.transpose()? { results.extend(entries); } results .entries .sort_by(|a, b| a.filename.cmp(&b.filename).then(a.index.cmp(&b.index))); Ok(results) } /// Fetch a flat remote index from a `--find-links` URL. pub async fn fetch_index(&self, index: &IndexUrl) -> Result { match index { IndexUrl::Path(url) => { let path = url .to_file_path() .map_err(|()| FlatIndexError::NonFileUrl(url.to_url()))?; Self::read_from_directory(&path, index) .map_err(|err| FlatIndexError::FindLinksDirectory(path.clone(), err)) } IndexUrl::Pypi(url) | IndexUrl::Url(url) => self .read_from_url(url, index) .await .map_err(|err| FlatIndexError::FindLinksUrl(url.to_url(), err)), } } /// Read a flat remote index from a `--find-links` URL. async fn read_from_url( &self, url: &DisplaySafeUrl, flat_index: &IndexUrl, ) -> Result { let cache_entry = self.cache.entry( CacheBucket::FlatIndex, "html", format!("{}.msgpack", cache_digest(&url.to_string())), ); let cache_control = match self.connectivity { Connectivity::Online => CacheControl::from( self.cache .freshness(&cache_entry, None, None) .map_err(ErrorKind::Io)?, ), Connectivity::Offline => CacheControl::AllowStale, }; let flat_index_request = self .client .uncached() .for_host(url) .get(Url::from(url.clone())) .header("Accept-Encoding", "gzip") .header("Accept", "text/html") .build() .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let parse_simple_response = |response: Response| { async { // Use the response URL, rather than the request URL, as the base for relative URLs. // This ensures that we handle redirects and other URL transformations correctly. let url = DisplaySafeUrl::from_url(response.url().clone()); let text = response .text() .await .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let SimpleDetailHTML { base, files } = SimpleDetailHTML::parse(&text, &url) .map_err(|err| Error::from_html_err(err, url.clone()))?; // Convert to a reference-counted string. let base = SmallString::from(base.as_str()); let unarchived: Vec = files .into_iter() .filter_map(|file| { match File::try_from_pypi(file, &base) { Ok(file) => Some(file), Err(err) => { // Ignore files with unparsable version specifiers. warn!("Skipping file in {}: {err}", &url); None } } }) .collect(); OwnedArchive::from_unarchived(&unarchived) } .boxed_local() .instrument(info_span!("parse_flat_index_html", url = % url)) }; let response = self .client .get_cacheable_with_retry( flat_index_request, &cache_entry, cache_control, parse_simple_response, ) .await; match response { Ok(files) => { let files = files .iter() .map(|file| { rkyv::deserialize::(file) .expect("archived version always deserializes") }) .filter_map(|file| { Some(FlatIndexEntry { filename: DistFilename::try_from_normalized_filename(&file.filename)?, file, index: flat_index.clone(), }) }) .collect(); Ok(FlatIndexEntries::from_entries(files)) } Err(CachedClientError::Client { err, .. }) if err.is_offline() => { Ok(FlatIndexEntries::offline()) } Err(err) => Err(err.into()), } } /// Read a flat remote index from a `--find-links` directory. fn read_from_directory( path: &Path, flat_index: &IndexUrl, ) -> Result { // The path context is provided by the caller. #[allow(clippy::disallowed_methods)] let entries = std::fs::read_dir(path)?; let mut dists = Vec::new(); for entry in entries { let entry = entry?; let metadata = entry.metadata()?; if metadata.is_dir() { continue; } if metadata.is_symlink() { let Ok(target) = entry.path().read_link() else { warn!( "Skipping unreadable symlink in `--find-links` directory: {}", entry.path().display() ); continue; }; if target.is_dir() { continue; } } let filename = entry.file_name(); let Some(filename) = filename.to_str() else { warn!( "Skipping non-UTF-8 filename in `--find-links` directory: {}", filename.to_string_lossy() ); continue; }; // SAFETY: The index path is itself constructed from a URL. let url = DisplaySafeUrl::from_file_path(entry.path()).unwrap(); let file = File { dist_info_metadata: false, filename: filename.into(), hashes: HashDigests::empty(), requires_python: None, size: None, upload_time_utc_ms: None, url: FileLocation::AbsoluteUrl(UrlString::from(url)), yanked: None, zstd: None, }; let Some(filename) = DistFilename::try_from_normalized_filename(filename) else { debug!( "Ignoring `--find-links` entry (expected a wheel or source distribution filename): {}", entry.path().display() ); continue; }; dists.push(FlatIndexEntry { filename, file, index: flat_index.clone(), }); } dists.sort_by(|a, b| { a.filename .cmp(&b.filename) .then_with(|| a.index.cmp(&b.index)) }); Ok(FlatIndexEntries::from_entries(dists)) } } #[cfg(test)] mod tests { use super::*; use fs_err::File; use std::io::Write; use tempfile::tempdir; #[test] fn read_from_directory_sorts_distributions() { let dir = tempdir().unwrap(); let filenames = [ "beta-2.0.0-py3-none-any.whl", "alpha-1.0.0.tar.gz", "alpha-1.0.0-py3-none-any.whl", ]; for name in &filenames { let mut file = File::create(dir.path().join(name)).unwrap(); file.write_all(b"").unwrap(); } let entries = FlatIndexClient::read_from_directory( dir.path(), &IndexUrl::parse(&dir.path().to_string_lossy(), None).unwrap(), ) .unwrap(); let actual = entries .entries .iter() .map(|entry| entry.filename.to_string()) .collect::>(); let mut expected = filenames .iter() .map(|name| DistFilename::try_from_normalized_filename(name).unwrap()) .collect::>(); expected.sort(); let expected = expected .into_iter() .map(|filename| filename.to_string()) .collect::>(); assert_eq!(actual, expected); } } uv-0.9.17+ds1/crates/uv-client/src/html.rs000066400000000000000000001542721520155276700202420ustar00rootroot00000000000000use std::str::FromStr; use jiff::Timestamp; use tl::HTMLTag; use tracing::{debug, instrument, warn}; use uv_normalize::PackageName; use uv_pep440::VersionSpecifiers; use uv_pypi_types::{BaseUrl, CoreMetadata, Hashes, PypiFile, Yanked}; use uv_pypi_types::{HashError, LenientVersionSpecifiers}; use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError}; /// A parsed structure from PyPI "HTML" index format for a single package. #[derive(Debug, Clone)] pub(crate) struct SimpleDetailHTML { /// The [`BaseUrl`] to which all relative URLs should be resolved. pub(crate) base: BaseUrl, /// The list of [`PypiFile`]s available for download sorted by filename. pub(crate) files: Vec, } impl SimpleDetailHTML { /// Parse the list of [`PypiFile`]s from the simple HTML page returned by the given URL. #[instrument(skip_all, fields(url = % url))] pub(crate) fn parse(text: &str, url: &DisplaySafeUrl) -> Result { let dom = tl::parse(text, tl::ParserOptions::default())?; // Parse the first `` tag, if any, to determine the base URL to which all // relative URLs should be resolved. The HTML spec requires that the `` tag // appear before other tags with attribute values of URLs. let base = BaseUrl::from( dom.nodes() .iter() .filter_map(|node| node.as_tag()) .take_while(|tag| !matches!(tag.name().as_bytes(), b"a" | b"link")) .find(|tag| tag.name().as_bytes() == b"base") .map(|base| Self::parse_base(base)) .transpose()? .flatten() .unwrap_or_else(|| url.clone()), ); // Parse each `` tag, to extract the filename, hash, and URL. let mut files: Vec = dom .nodes() .iter() .filter_map(|node| node.as_tag()) .filter(|link| link.name().as_bytes() == b"a") .map(|link| Self::parse_anchor(link)) .filter_map(|result| match result { Ok(None) => None, Ok(Some(file)) => Some(Ok(file)), Err(err) => Some(Err(err)), }) .collect::, _>>()?; // While it has not been positively observed, we sort the files // to ensure we have a defined ordering. Otherwise, if we rely on // the API to provide a stable ordering and doesn't, it can lead // non-deterministic behavior elsewhere. (This is somewhat hand-wavy // and a bit of a band-aide, since arguably, the order of this API // response probably shouldn't have an impact on things downstream from // this. That is, if something depends on ordering, then it should // probably be the thing that does the sorting.) files.sort_unstable_by(|f1, f2| f1.filename.cmp(&f2.filename)); Ok(Self { base, files }) } /// Parse the `href` from a `` tag. fn parse_base(base: &HTMLTag) -> Result, Error> { let Some(Some(href)) = base.attributes().get("href") else { return Ok(None); }; let href = std::str::from_utf8(href.as_bytes())?; let url = DisplaySafeUrl::parse(href).map_err(|err| Error::UrlParse(href.to_string(), err))?; Ok(Some(url)) } /// Parse a [`PypiFile`] from an `` tag. /// /// Returns `None` if the `` doesn't have an `href` attribute. fn parse_anchor(link: &HTMLTag) -> Result, Error> { // Extract the href. let Some(href) = link .attributes() .get("href") .flatten() .filter(|bytes| !bytes.as_bytes().is_empty()) else { return Ok(None); }; let href = std::str::from_utf8(href.as_bytes())?; // Extract the hash, which should be in the fragment. let decoded = html_escape::decode_html_entities(href); let (path, hashes) = if let Some((path, fragment)) = decoded.split_once('#') { let fragment = percent_encoding::percent_decode_str(fragment).decode_utf8()?; ( path, if fragment.trim().is_empty() { Hashes::default() } else { match Hashes::parse_fragment(&fragment) { Ok(hashes) => hashes, Err( err @ (HashError::InvalidFragment(..) | HashError::InvalidStructure(..)), ) => { // If the URL includes an irrelevant hash (e.g., `#main`), ignore it. debug!("{err}"); Hashes::default() } Err(HashError::UnsupportedHashAlgorithm(fragment)) => { if fragment == "egg" { // If the URL references an egg hash, ignore it. debug!("{}", HashError::UnsupportedHashAlgorithm(fragment)); Hashes::default() } else { // If the URL references a hash, but it's unsupported, error. return Err(HashError::UnsupportedHashAlgorithm(fragment).into()); } } } }, ) } else { (decoded.as_ref(), Hashes::default()) }; // Extract the filename from the body text, which MUST match that of // the final path component of the URL. let filename = path .split('/') .next_back() .ok_or_else(|| Error::MissingFilename(href.to_string()))?; // Strip any query string from the filename. let filename = filename.split('?').next().unwrap_or(filename); // Unquote the filename. let filename = percent_encoding::percent_decode_str(filename) .decode_utf8() .map_err(|_| Error::UnsupportedFilename(filename.to_string()))?; // Extract the `requires-python` value, which should be set on the // `data-requires-python` attribute. let requires_python = if let Some(requires_python) = link.attributes().get("data-requires-python").flatten() { let requires_python = std::str::from_utf8(requires_python.as_bytes())?; let requires_python = html_escape::decode_html_entities(requires_python); Some(LenientVersionSpecifiers::from_str(&requires_python).map(VersionSpecifiers::from)) } else { None }; // Extract the `core-metadata` field, which is either set on: // - `data-core-metadata`, per PEP 714. // - `data-dist-info-metadata`, per PEP 658. let core_metadata = if let Some(dist_info_metadata) = link .attributes() .get("data-core-metadata") .flatten() .or_else(|| link.attributes().get("data-dist-info-metadata").flatten()) { let dist_info_metadata = std::str::from_utf8(dist_info_metadata.as_bytes())?; let dist_info_metadata = html_escape::decode_html_entities(dist_info_metadata); match dist_info_metadata.as_ref() { "true" => Some(CoreMetadata::Bool(true)), "false" => Some(CoreMetadata::Bool(false)), fragment => match Hashes::parse_fragment(fragment) { Ok(hash) => Some(CoreMetadata::Hashes(hash)), Err(err) => { warn!("Failed to parse core metadata value `{fragment}`: {err}"); None } }, } } else { None }; // Extract the `yanked` field, which should be set on the `data-yanked` // attribute. let yanked = if let Some(yanked) = link.attributes().get("data-yanked").flatten() { let yanked = std::str::from_utf8(yanked.as_bytes())?; let yanked = html_escape::decode_html_entities(yanked); Some(Box::new(Yanked::Reason(yanked.into()))) } else { None }; // Extract the `size` field, which should be set on the `data-size` attribute. This isn't // included in PEP 700, which omits the HTML API, but we respect it anyway. Since this // field isn't standardized, we discard errors. let size = link .attributes() .get("data-size") .flatten() .and_then(|size| std::str::from_utf8(size.as_bytes()).ok()) .map(|size| html_escape::decode_html_entities(size)) .and_then(|size| size.parse().ok()); // Extract the `upload-time` field, which should be set on the `data-upload-time` attribute. This isn't // included in PEP 700, which omits the HTML API, but we respect it anyway. Since this // field isn't standardized, we discard errors. let upload_time = link .attributes() .get("data-upload-time") .flatten() .and_then(|upload_time| std::str::from_utf8(upload_time.as_bytes()).ok()) .map(|upload_time| html_escape::decode_html_entities(upload_time)) .and_then(|upload_time| Timestamp::from_str(&upload_time).ok()); Ok(Some(PypiFile { core_metadata, yanked, requires_python, hashes, filename: filename.into(), url: path.into(), size, upload_time, })) } } /// A parsed structure from PyPI "HTML" index format listing all available packages. #[derive(Debug, Clone)] pub(crate) struct SimpleIndexHtml { /// The list of project names available in the index. pub(crate) projects: Vec, } impl SimpleIndexHtml { /// Parse the list of project names from the Simple API index HTML page. pub(crate) fn parse(text: &str) -> Result { let dom = tl::parse(text, tl::ParserOptions::default())?; // Parse each `` tag to extract the project name. let parser = dom.parser(); let mut projects = dom .nodes() .iter() .filter_map(|node| node.as_tag()) .filter(|link| link.name().as_bytes() == b"a") .filter_map(|link| Self::parse_anchor_project_name(link, parser)) .collect::>(); // Sort for deterministic ordering. projects.sort_unstable(); Ok(Self { projects }) } /// Parse a project name from an `` tag. /// /// Returns `None` if the `` doesn't have an `href` attribute or text content. fn parse_anchor_project_name(link: &HTMLTag, parser: &tl::Parser) -> Option { // Extract the href. link.attributes() .get("href") .flatten() .filter(|bytes| !bytes.as_bytes().is_empty())?; // Extract the text content, which should be the project name. let inner_text = link.inner_text(parser); let project_name = inner_text.trim(); if project_name.is_empty() { return None; } PackageName::from_str(project_name).ok() } } #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] Utf8(#[from] std::str::Utf8Error), #[error(transparent)] FromUtf8(#[from] std::string::FromUtf8Error), #[error("Failed to parse URL: {0}")] UrlParse(String, #[source] DisplaySafeUrlError), #[error(transparent)] HtmlParse(#[from] tl::ParseError), #[error("Missing href attribute on anchor link: `{0}`")] MissingHref(String), #[error("Expected distribution filename as last path component of URL: {0}")] MissingFilename(String), #[error("Expected distribution filename to be UTF-8: {0}")] UnsupportedFilename(String), #[error("Missing hash attribute on URL: {0}")] MissingHash(String), #[error(transparent)] FragmentParse(#[from] HashError), #[error("Invalid `requires-python` specifier: {0}")] Pep440(#[source] uv_pep440::VersionSpecifiersParseError), } #[cfg(test)] mod tests { use super::*; #[test] fn parse_sha256() { let text = r#"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "download.pytorch.org", ), ), port: None, path: "/whl/jinja2/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "Jinja2-3.1.2-py3-none-any.whl", hashes: Hashes { md5: None, sha256: Some( "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", ), sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.2-py3-none-any.whl", yanked: None, }, ], } "#); } #[test] fn parse_md5() { let text = r#"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "download.pytorch.org", ), ), port: None, path: "/whl/jinja2/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "Jinja2-3.1.2-py3-none-any.whl", hashes: Hashes { md5: Some( "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", ), sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.2-py3-none-any.whl", yanked: None, }, ], } "#); } #[test] fn parse_base() { let text = r#"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "index.python.org", ), ), port: None, path: "/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "Jinja2-3.1.2-py3-none-any.whl", hashes: Hashes { md5: None, sha256: Some( "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", ), sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.2-py3-none-any.whl", yanked: None, }, ], } "#); } #[test] fn parse_escaped_fragment() { let text = r#"

Links for jinja2

Jinja2-3.1.2+233fca715f49-py3-none-any.whl
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "download.pytorch.org", ), ), port: None, path: "/whl/jinja2/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "Jinja2-3.1.2+233fca715f49-py3-none-any.whl", hashes: Hashes { md5: None, sha256: Some( "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", ), sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.2+233fca715f49-py3-none-any.whl", yanked: None, }, ], } "#); } #[test] fn parse_encoded_fragment() { let text = r#"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "download.pytorch.org", ), ), port: None, path: "/whl/jinja2/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "Jinja2-3.1.2-py3-none-any.whl", hashes: Hashes { md5: None, sha256: Some( "4095ada29e51070f7d199a0a5bdf5c8d8e238e03f0bf4dcc02571e78c9ae800d", ), sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.2-py3-none-any.whl", yanked: None, }, ], } "#); } #[test] fn parse_quoted_filepath() { let text = r#"

Links for jinja2

cpu/torchtext-0.17.0%2Bcpu-cp39-cp39-win_amd64.whl
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "download.pytorch.org", ), ), port: None, path: "/whl/jinja2/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "torchtext-0.17.0+cpu-cp39-cp39-win_amd64.whl", hashes: Hashes { md5: None, sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "cpu/torchtext-0.17.0%2Bcpu-cp39-cp39-win_amd64.whl", yanked: None, }, ], } "#); } #[test] fn parse_missing_hash() { let text = r#"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "download.pytorch.org", ), ), port: None, path: "/whl/jinja2/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "Jinja2-3.1.2-py3-none-any.whl", hashes: Hashes { md5: None, sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.2-py3-none-any.whl", yanked: None, }, ], } "#); } #[test] fn parse_missing_href() { let text = r"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl
"; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "download.pytorch.org", ), ), port: None, path: "/whl/jinja2/", query: None, fragment: None, }, ), files: [], } "#); } #[test] fn parse_empty_href() { let text = r#"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "download.pytorch.org", ), ), port: None, path: "/whl/jinja2/", query: None, fragment: None, }, ), files: [], } "#); } #[test] fn parse_empty_fragment() { let text = r#"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "download.pytorch.org", ), ), port: None, path: "/whl/jinja2/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "Jinja2-3.1.2-py3-none-any.whl", hashes: Hashes { md5: None, sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.2-py3-none-any.whl", yanked: None, }, ], } "#); } #[test] fn parse_query_string() { let text = r#"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "download.pytorch.org", ), ), port: None, path: "/whl/jinja2/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "Jinja2-3.1.2-py3-none-any.whl", hashes: Hashes { md5: None, sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.2-py3-none-any.whl?project=legacy", yanked: None, }, ], } "#); } #[test] fn parse_unknown_fragment() { let text = r#"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base); insta::assert_debug_snapshot!(result, @r#" Ok( SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "download.pytorch.org", ), ), port: None, path: "/whl/jinja2/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "Jinja2-3.1.2-py3-none-any.whl", hashes: Hashes { md5: None, sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.2-py3-none-any.whl", yanked: None, }, ], }, ) "#); } #[test] fn parse_egg_fragment() { let text = r#"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl#egg=public-hello-0.1
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base); insta::assert_debug_snapshot!(result, @r#" Ok( SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "download.pytorch.org", ), ), port: None, path: "/whl/jinja2/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "Jinja2-3.1.2-py3-none-any.whl", hashes: Hashes { md5: None, sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.2-py3-none-any.whl", yanked: None, }, ], }, ) "#); } #[test] fn parse_unknown_hash() { let text = r#"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap_err(); insta::assert_snapshot!(result, @"Unsupported hash algorithm (expected one of: `md5`, `sha256`, `sha384`, `sha512`, or `blake2b`) on: `blake2=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61`"); } #[test] fn parse_flat_index_html() { let text = r#" cuda100/jaxlib-0.1.52+cuda100-cp36-none-manylinux2010_x86_64.whl
cuda100/jaxlib-0.1.52+cuda100-cp37-none-manylinux2010_x86_64.whl
"#; let base = DisplaySafeUrl::parse( "https://storage.googleapis.com/jax-releases/jax_cuda_releases.html", ) .unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "storage.googleapis.com", ), ), port: None, path: "/jax-releases/jax_cuda_releases.html", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "jaxlib-0.1.52+cuda100-cp36-none-manylinux2010_x86_64.whl", hashes: Hashes { md5: None, sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "https://storage.googleapis.com/jax-releases/cuda100/jaxlib-0.1.52+cuda100-cp36-none-manylinux2010_x86_64.whl", yanked: None, }, PypiFile { core_metadata: None, filename: "jaxlib-0.1.52+cuda100-cp37-none-manylinux2010_x86_64.whl", hashes: Hashes { md5: None, sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "https://storage.googleapis.com/jax-releases/cuda100/jaxlib-0.1.52+cuda100-cp37-none-manylinux2010_x86_64.whl", yanked: None, }, ], } "#); } /// Test for AWS Code Artifact /// /// See: #[test] fn parse_code_artifact_index_html() { let text = r#" Links for flask

Links for flask

Flask-0.1.tar.gz
Flask-0.10.1.tar.gz
flask-3.0.1.tar.gz
"#; let base = DisplaySafeUrl::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/") .unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "account.d.codeartifact.us-west-2.amazonaws.com", ), ), port: None, path: "/pypi/shared-packages-pypi/simple/flask/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "Flask-0.1.tar.gz", hashes: Hashes { md5: None, sha256: Some( "9da884457e910bf0847d396cb4b778ad9f3c3d17db1c5997cb861937bd284237", ), sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "0.1/Flask-0.1.tar.gz", yanked: None, }, PypiFile { core_metadata: None, filename: "Flask-0.10.1.tar.gz", hashes: Hashes { md5: None, sha256: Some( "4c83829ff83d408b5e1d4995472265411d2c414112298f2eb4b359d9e4563373", ), sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "0.10.1/Flask-0.10.1.tar.gz", yanked: None, }, PypiFile { core_metadata: None, filename: "flask-3.0.1.tar.gz", hashes: Hashes { md5: None, sha256: Some( "6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403", ), sha384: None, sha512: None, blake2b: None, }, requires_python: Some( Ok( VersionSpecifiers( [ VersionSpecifier { operator: GreaterThanEqual, version: "3.8", }, ], ), ), ), size: None, upload_time: None, url: "3.0.1/flask-3.0.1.tar.gz", yanked: None, }, ], } "#); } #[test] fn parse_file_requires_python_trailing_comma() { let text = r#"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl
"#; let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "download.pytorch.org", ), ), port: None, path: "/whl/jinja2/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: None, filename: "Jinja2-3.1.2-py3-none-any.whl", hashes: Hashes { md5: None, sha256: Some( "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", ), sha384: None, sha512: None, blake2b: None, }, requires_python: Some( Ok( VersionSpecifiers( [ VersionSpecifier { operator: GreaterThanEqual, version: "3.8", }, ], ), ), ), size: None, upload_time: None, url: "/whl/Jinja2-3.1.2-py3-none-any.whl", yanked: None, }, ], } "#); } /// Respect PEP 714 (see: ). #[test] fn parse_core_metadata() { let text = r#"

Links for jinja2

Jinja2-3.1.2-py3-none-any.whl
Jinja2-3.1.3-py3-none-any.whl
Jinja2-3.1.4-py3-none-any.whl
Jinja2-3.1.5-py3-none-any.whl
Jinja2-3.1.6-py3-none-any.whl
"#; let base = DisplaySafeUrl::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/") .unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleDetailHTML { base: BaseUrl( DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some( Domain( "account.d.codeartifact.us-west-2.amazonaws.com", ), ), port: None, path: "/pypi/shared-packages-pypi/simple/flask/", query: None, fragment: None, }, ), files: [ PypiFile { core_metadata: Some( Bool( true, ), ), filename: "Jinja2-3.1.2-py3-none-any.whl", hashes: Hashes { md5: None, sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.2-py3-none-any.whl", yanked: None, }, PypiFile { core_metadata: Some( Bool( true, ), ), filename: "Jinja2-3.1.3-py3-none-any.whl", hashes: Hashes { md5: None, sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.3-py3-none-any.whl", yanked: None, }, PypiFile { core_metadata: Some( Bool( false, ), ), filename: "Jinja2-3.1.4-py3-none-any.whl", hashes: Hashes { md5: None, sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.4-py3-none-any.whl", yanked: None, }, PypiFile { core_metadata: Some( Bool( false, ), ), filename: "Jinja2-3.1.5-py3-none-any.whl", hashes: Hashes { md5: None, sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.5-py3-none-any.whl", yanked: None, }, PypiFile { core_metadata: Some( Bool( true, ), ), filename: "Jinja2-3.1.6-py3-none-any.whl", hashes: Hashes { md5: None, sha256: None, sha384: None, sha512: None, blake2b: None, }, requires_python: None, size: None, upload_time: None, url: "/whl/Jinja2-3.1.6-py3-none-any.whl", yanked: None, }, ], } "#); } /// Test parsing Simple API index (root) HTML. #[test] fn parse_simple_index() { let text = r#" Simple Index

Simple Index

flask
jinja2
requests
"#; let result = SimpleIndexHtml::parse(text).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleIndexHtml { projects: [ PackageName( "flask", ), PackageName( "jinja2", ), PackageName( "requests", ), ], } "#); } /// Test that project names are sorted. #[test] fn parse_simple_index_sorted() { let text = r#" zebra
apple
monkey
"#; let result = SimpleIndexHtml::parse(text).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleIndexHtml { projects: [ PackageName( "apple", ), PackageName( "monkey", ), PackageName( "zebra", ), ], } "#); } /// Test that links without `href` attributes are ignored. #[test] fn parse_simple_index_missing_href() { let text = r#"

Simple Index

flask
no-href-project
requests
"#; let result = SimpleIndexHtml::parse(text).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleIndexHtml { projects: [ PackageName( "flask", ), PackageName( "requests", ), ], } "#); } /// Test that links with empty `href` attributes are ignored. #[test] fn parse_simple_index_empty_href() { let text = r#" empty-href
flask
"#; let result = SimpleIndexHtml::parse(text).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleIndexHtml { projects: [ PackageName( "flask", ), ], } "#); } /// Test that links with empty text content are ignored. #[test] fn parse_simple_index_empty_text() { let text = r#"
flask

"#; let result = SimpleIndexHtml::parse(text).unwrap(); insta::assert_debug_snapshot!(result, @r#" SimpleIndexHtml { projects: [ PackageName( "flask", ), ], } "#); } /// Test parsing with case variations and normalization. #[test] fn parse_simple_index_case_variations() { let text = r#" Flask
django
PyYAML
"#; let result = SimpleIndexHtml::parse(text).unwrap(); // Note: We preserve the case as returned by the server insta::assert_debug_snapshot!(result, @r#" SimpleIndexHtml { projects: [ PackageName( "django", ), PackageName( "flask", ), PackageName( "pyyaml", ), ], } "#); } } uv-0.9.17+ds1/crates/uv-client/src/httpcache/000077500000000000000000000000001520155276700206605ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-client/src/httpcache/control.rs000066400000000000000000000707711520155276700227220ustar00rootroot00000000000000use std::collections::HashSet; use crate::rkyvutil::OwnedArchive; /// Represents values for relevant cache-control directives. /// /// This does include some directives that we don't use mostly because they are /// trivial to support. (For example, `must-understand` at time of writing is /// not used in our HTTP cache semantics. Neither is `proxy-revalidate` since /// we are not a proxy.) #[derive( Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, )] #[rkyv(derive(Debug))] pub struct CacheControl { // directives for requests and responses /// * /// * pub max_age_seconds: Option, /// * /// * pub no_cache: bool, /// * /// * pub no_store: bool, /// * /// * pub no_transform: bool, // request-only directives /// pub max_stale_seconds: Option, /// pub min_fresh_seconds: Option, // response-only directives /// pub only_if_cached: bool, /// pub must_revalidate: bool, /// pub must_understand: bool, /// pub private: bool, /// pub proxy_revalidate: bool, /// pub public: bool, /// pub s_maxage_seconds: Option, /// pub immutable: bool, } impl CacheControl { /// Convert this to an owned archive value. pub fn to_archived(&self) -> OwnedArchive { // There's no way (other than OOM) for serializing this type to fail. OwnedArchive::from_unarchived(self).expect("all possible values can be archived") } } impl<'b, B: 'b + ?Sized + AsRef<[u8]>> FromIterator<&'b B> for CacheControl { fn from_iter>(it: T) -> Self { CacheControlParser::new(it).collect() } } impl FromIterator for CacheControl { fn from_iter>(it: T) -> Self { fn parse_int(value: &[u8]) -> Option { if !value.iter().all(u8::is_ascii_digit) { return None; } std::str::from_utf8(value).ok()?.parse().ok() } let mut cc = Self::default(); for ccd in it { // Note that when we see invalid directive values, we follow [RFC // 9111 S4.2.1]. It says that invalid cache-control directives // should result in treating the response as stale. (Which we // accomplished by setting `must_revalidate` to `true`.) // // [RFC 9111 S4.2.1]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.1 match &*ccd.name { // request + response directives "max-age" => match parse_int(&ccd.value) { None => cc.must_revalidate = true, Some(seconds) => cc.max_age_seconds = Some(seconds), }, "no-cache" => cc.no_cache = true, "no-store" => cc.no_store = true, "no-transform" => cc.no_transform = true, // request-only directives "max-stale" => { // As per [RFC 9111 S5.2.1.2], "If no value is assigned to // max-stale, then the client will accept a stale response // of any age." We implement that by just using the maximum // number of seconds. // // [RFC 9111 S5.2.1.2]: https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.2 if ccd.value.is_empty() { cc.max_stale_seconds = Some(u64::MAX); } else { match parse_int(&ccd.value) { None => cc.must_revalidate = true, Some(seconds) => cc.max_stale_seconds = Some(seconds), } } } "min-fresh" => match parse_int(&ccd.value) { None => cc.must_revalidate = true, Some(seconds) => cc.min_fresh_seconds = Some(seconds), }, "only-if-cached" => cc.only_if_cached = true, "must-revalidate" => cc.must_revalidate = true, "must-understand" => cc.must_understand = true, "private" => cc.private = true, "proxy-revalidate" => cc.proxy_revalidate = true, "public" => cc.public = true, "s-maxage" => match parse_int(&ccd.value) { None => cc.must_revalidate = true, Some(seconds) => cc.s_maxage_seconds = Some(seconds), }, "immutable" => cc.immutable = true, _ => {} } } cc } } /// A parser for the HTTP `Cache-Control` header. /// /// The parser is mostly defined across multiple parts of multiple RFCs. /// Namely, [RFC 9110 S5.6.2] says how to parse the names (or "keys") of each /// directive (whose format is a "token"). [RFC 9110 S5.6.4] says how to parse /// quoted values. And finally, [RFC 9111 Appendix A] gives the ABNF for the /// overall header value. /// /// This parser accepts an iterator of anything that can be cheaply converted /// to a byte string (e.g., `http::header::HeaderValue`). Directives are parsed /// from zero or more of these byte strings. Parsing cannot return an error, /// but if something unexpected is found, the rest of that header value is /// skipped. /// /// Duplicate directives provoke an automatic insertion of `must-revalidate`, /// as implied by [RFC 9111 S4.2.1], to ensure that the client will talk to the /// server before using anything in case. /// /// This parser handles a bit more than what we actually need in /// `uv-client`. For example, we don't need to handle quoted values at all /// since either don't use or care about values that require quoted. With that /// said, the parser handles these because it wasn't that much extra work to do /// so and just generally seemed like good sense. (If we didn't handle them and /// parsed them incorrectly, that might mean parsing subsequent directives that /// we do care about incorrectly.) /// /// [RFC 9110 S5.6.2]: https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens /// [RFC 9110 S5.6.4]: https://www.rfc-editor.org/rfc/rfc9110.html#name-quoted-strings /// [RFC 9111 Appendix A]: https://www.rfc-editor.org/rfc/rfc9111.html#name-collected-abnf /// [RFC 9111 S4.2.1]: https://www.rfc-editor.org/rfc/rfc9111.html#calculating.freshness.lifetime struct CacheControlParser<'b, I> { cur: &'b [u8], directives: I, seen: HashSet, } impl<'b, B: 'b + ?Sized + AsRef<[u8]>, I: Iterator> CacheControlParser<'b, I> { /// Create a new parser of zero or more `Cache-Control` header values. The /// given iterator should yield elements that satisfy `AsRef<[u8]>`. fn new>(headers: II) -> Self { let mut directives = headers.into_iter(); let cur = directives.next().map(AsRef::as_ref).unwrap_or(b""); CacheControlParser { cur, directives, seen: HashSet::new(), } } /// Parses a token according to [RFC 9110 S5.6.2]. /// /// If no token is found at the current position, then this returns `None`. /// Usually this indicates an invalid cache-control directive. /// /// This does not trim whitespace before or after the token. /// /// [RFC 9110 S5.6.2]: https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens fn parse_token(&mut self) -> Option { /// Returns true when the given byte can appear in a token, as /// defined by [RFC 9110 S5.6.2]. /// /// [RFC 9110 S5.6.2]: https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens fn is_token_byte(byte: u8) -> bool { matches!( byte, | b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' | b'^' | b'_' | b'`' | b'|' | b'~' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z', ) } let mut end = 0; while self.cur.get(end).copied().is_some_and(is_token_byte) { end += 1; } if end == 0 { None } else { let (token, rest) = self.cur.split_at(end); self.cur = rest; // This can't fail because `end` is only incremented when the // current byte is a valid token byte. And all valid token bytes // are ASCII and thus valid UTF-8. Some(String::from_utf8(token.to_vec()).expect("all valid token bytes are valid UTF-8")) } } /// Looks for an `=` as per [RFC 9111 Appendix A] which indicates that a /// cache directive has a value. /// /// This returns true if one was found. In which case, the `=` is consumed. /// /// This does not trim whitespace before or after the token. /// /// [RFC 9111 Appendix A]: https://www.rfc-editor.org/rfc/rfc9111.html#name-collected-abnf fn maybe_parse_equals(&mut self) -> bool { if self.cur.first().is_some_and(|&byte| byte == b'=') { self.cur = &self.cur[1..]; true } else { false } } /// Parses a directive value as either an unquoted token or a quoted string /// as per [RFC 9111 Appendix A]. /// /// If a valid value could not be found (for example, end-of-input or an /// opening quote without a closing quote), then `None` is returned. In /// this case, one should consider the cache-control header invalid. /// /// This does not trim whitespace before or after the token. /// /// Note that the returned value is *not* guaranteed to be valid UTF-8. /// Namely, it is possible for a quoted string to contain invalid UTF-8. /// /// [RFC 9111 Appendix A]: https://www.rfc-editor.org/rfc/rfc9111.html#name-collected-abnf fn parse_value(&mut self) -> Option> { if *self.cur.first()? == b'"' { self.cur = &self.cur[1..]; self.parse_quoted_string() } else { self.parse_token().map(String::into_bytes) } } /// Parses a quoted string as per [RFC 9110 S5.6.4]. /// /// This assumes the opening quote has already been consumed. /// /// If an invalid quoted string was found (e.g., no closing quote), then /// `None` is returned. An empty value may be returned. /// /// Note that the returned value is *not* guaranteed to be valid UTF-8. /// Namely, it is possible for a quoted string to contain invalid UTF-8. /// /// [RFC 9110 S5.6.4]: https://www.rfc-editor.org/rfc/rfc9110.html#name-quoted-strings fn parse_quoted_string(&mut self) -> Option> { fn is_qdtext_byte(byte: u8) -> bool { matches!(byte, b'\t' | b' ' | 0x21 | 0x23..=0x5B | 0x5D..=0x7E | 0x80..=0xFF) } fn is_quoted_pair_byte(byte: u8) -> bool { matches!(byte, b'\t' | b' ' | 0x21..=0x7E | 0x80..=0xFF) } let mut value = vec![]; while !self.cur.is_empty() { let byte = self.cur[0]; self.cur = &self.cur[1..]; if byte == b'"' { return Some(value); } else if byte == b'\\' { let byte = *self.cur.first()?; self.cur = &self.cur[1..]; // If we saw an escape but didn't see a valid // escaped byte, then we treat this value as // invalid. if !is_quoted_pair_byte(byte) { return None; } value.push(byte); } else if is_qdtext_byte(byte) { value.push(byte); } else { break; } } // If we got here, it means we hit end-of-input before seeing a closing // quote. So we treat this as invalid and return `None`. None } /// Looks for a `,` as per [RFC 9111 Appendix A]. If one is found, then it /// is consumed and this returns true. /// /// This does not trim whitespace before or after the token. /// /// [RFC 9111 Appendix A]: https://www.rfc-editor.org/rfc/rfc9111.html#name-collected-abnf fn maybe_parse_directive_delimiter(&mut self) -> bool { if self.cur.first().is_some_and(|&byte| byte == b',') { self.cur = &self.cur[1..]; true } else { false } } /// [RFC 9111 Appendix A] says that optional whitespace may appear between /// cache directives. We actually also allow whitespace to appear before /// the first directive and after the last directive. /// /// [RFC 9111 Appendix A]: https://www.rfc-editor.org/rfc/rfc9111.html#name-collected-abnf fn skip_whitespace(&mut self) { while self.cur.first().is_some_and(u8::is_ascii_whitespace) { self.cur = &self.cur[1..]; } } fn emit_directive( &mut self, directive: CacheControlDirective, ) -> Option { let duplicate = !self.seen.insert(directive.name.clone()); if duplicate { self.emit_revalidation() } else { Some(directive) } } fn emit_revalidation(&mut self) -> Option { if self.seen.insert("must-revalidate".to_string()) { Some(CacheControlDirective::must_revalidate()) } else { // If we've already emitted a must-revalidate // directive, then don't do it again. None } } } impl<'b, B: 'b + ?Sized + AsRef<[u8]>, I: Iterator> Iterator for CacheControlParser<'b, I> { type Item = CacheControlDirective; fn next(&mut self) -> Option { loop { if self.cur.is_empty() { self.cur = self.directives.next().map(AsRef::as_ref)?; } while !self.cur.is_empty() { self.skip_whitespace(); let Some(mut name) = self.parse_token() else { // If we fail to parse a token, then this header value is // either corrupt or empty. So skip the rest of it. let invalid = !self.cur.is_empty(); self.cur = b""; // But if it was invalid, force revalidation. if invalid { if let Some(d) = self.emit_revalidation() { return Some(d); } } break; }; name.make_ascii_lowercase(); if !self.maybe_parse_equals() { // Eat up whitespace and the next delimiter. We don't care // if we find a terminator. self.skip_whitespace(); self.maybe_parse_directive_delimiter(); let directive = CacheControlDirective { name, value: vec![], }; match self.emit_directive(directive) { None => continue, Some(d) => return Some(d), } } let Some(value) = self.parse_value() else { // If we expected a value (we saw an =) but couldn't find a // valid value, then this header value is probably corrupt. // So skip the rest of it. self.cur = b""; match self.emit_revalidation() { None => break, Some(d) => return Some(d), } }; // Eat up whitespace and the next delimiter. We don't care if // we find a terminator. self.skip_whitespace(); self.maybe_parse_directive_delimiter(); let directive = CacheControlDirective { name, value }; if let Some(d) = self.emit_directive(directive) { return Some(d); } } } } } /// A single directive from the `Cache-Control` header. #[derive(Debug, Eq, PartialEq)] struct CacheControlDirective { /// The name of the directive. name: String, /// A possibly empty value. /// /// Note that directive values may contain invalid UTF-8. (Although they /// cannot actually contain arbitrary bytes. For example, NUL bytes, among /// others, are not allowed.) value: Vec, } impl CacheControlDirective { /// Returns a `must-revalidate` directive. This is useful for forcing a /// cache decision that the response is stale, and thus the server should /// be consulted for whether the cached response ought to be used or not. fn must_revalidate() -> Self { Self { name: "must-revalidate".to_string(), value: vec![], } } } #[cfg(test)] mod tests { use super::*; #[test] fn cache_control_token() { let cc: CacheControl = CacheControlParser::new(["no-cache"]).collect(); assert!(cc.no_cache); assert!(!cc.must_revalidate); } #[test] fn cache_control_max_age() { let cc: CacheControl = CacheControlParser::new(["max-age=60"]).collect(); assert_eq!(Some(60), cc.max_age_seconds); assert!(!cc.must_revalidate); } // [RFC 9111 S5.2.1.1] says that client MUST NOT quote max-age, but we // support parsing it that way anyway. // // [RFC 9111 S5.2.1.1]: https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1 #[test] fn cache_control_max_age_quoted() { let cc: CacheControl = CacheControlParser::new([r#"max-age="60""#]).collect(); assert_eq!(Some(60), cc.max_age_seconds); assert!(!cc.must_revalidate); } #[test] fn cache_control_max_age_invalid() { let cc: CacheControl = CacheControlParser::new(["max-age=6a0"]).collect(); assert_eq!(None, cc.max_age_seconds); assert!(cc.must_revalidate); } #[test] fn cache_control_immutable() { let cc: CacheControl = CacheControlParser::new(["max-age=31536000, immutable"]).collect(); assert_eq!(Some(31_536_000), cc.max_age_seconds); assert!(cc.immutable); assert!(!cc.must_revalidate); } #[test] fn cache_control_unrecognized() { let cc: CacheControl = CacheControlParser::new(["lion,max-age=60,zebra"]).collect(); assert_eq!(Some(60), cc.max_age_seconds); } #[test] fn cache_control_invalid_squashes_remainder() { let cc: CacheControl = CacheControlParser::new(["no-cache,\x00,max-age=60"]).collect(); // The invalid data doesn't impact things before it. assert!(cc.no_cache); // The invalid data precludes parsing anything after. assert_eq!(None, cc.max_age_seconds); // The invalid contents should force revalidation. assert!(cc.must_revalidate); } #[test] fn cache_control_invalid_squashes_remainder_but_not_other_header_values() { let cc: CacheControl = CacheControlParser::new(["no-cache,\x00,max-age=60", "max-stale=30"]).collect(); // The invalid data doesn't impact things before it. assert!(cc.no_cache); // The invalid data precludes parsing anything after // in the same header value, but not in other // header values. assert_eq!(Some(30), cc.max_stale_seconds); // The invalid contents should force revalidation. assert!(cc.must_revalidate); } #[test] fn cache_control_parse_token() { let directives = CacheControlParser::new(["no-cache"]).collect::>(); assert_eq!( directives, vec![CacheControlDirective { name: "no-cache".to_string(), value: vec![] }] ); } #[test] fn cache_control_parse_token_to_token_value() { let directives = CacheControlParser::new(["max-age=60"]).collect::>(); assert_eq!( directives, vec![CacheControlDirective { name: "max-age".to_string(), value: b"60".to_vec(), }] ); } #[test] fn cache_control_parse_token_to_quoted_string() { let directives = CacheControlParser::new([r#"private="cookie,x-something-else""#]).collect::>(); assert_eq!( directives, vec![CacheControlDirective { name: "private".to_string(), value: b"cookie,x-something-else".to_vec(), }] ); } #[test] fn cache_control_parse_token_to_quoted_string_with_escape() { let directives = CacheControlParser::new([r#"private="something\"crazy""#]).collect::>(); assert_eq!( directives, vec![CacheControlDirective { name: "private".to_string(), value: br#"something"crazy"#.to_vec(), }] ); } #[test] fn cache_control_parse_multiple_directives() { let header = r#"max-age=60, no-cache, private="cookie", no-transform"#; let directives = CacheControlParser::new([header]).collect::>(); assert_eq!( directives, vec![ CacheControlDirective { name: "max-age".to_string(), value: b"60".to_vec(), }, CacheControlDirective { name: "no-cache".to_string(), value: vec![] }, CacheControlDirective { name: "private".to_string(), value: b"cookie".to_vec(), }, CacheControlDirective { name: "no-transform".to_string(), value: vec![] }, ] ); } #[test] fn cache_control_parse_multiple_directives_across_multiple_header_values() { let headers = [ r"max-age=60, no-cache", r#"private="cookie""#, r"no-transform", ]; let directives = CacheControlParser::new(headers).collect::>(); assert_eq!( directives, vec![ CacheControlDirective { name: "max-age".to_string(), value: b"60".to_vec(), }, CacheControlDirective { name: "no-cache".to_string(), value: vec![] }, CacheControlDirective { name: "private".to_string(), value: b"cookie".to_vec(), }, CacheControlDirective { name: "no-transform".to_string(), value: vec![] }, ] ); } #[test] fn cache_control_parse_one_header_invalid() { let headers = [ r"max-age=60, no-cache", r#", private="cookie""#, r"no-transform", ]; let directives = CacheControlParser::new(headers).collect::>(); assert_eq!( directives, vec![ CacheControlDirective { name: "max-age".to_string(), value: b"60".to_vec(), }, CacheControlDirective { name: "no-cache".to_string(), value: vec![] }, CacheControlDirective { name: "must-revalidate".to_string(), value: vec![] }, CacheControlDirective { name: "no-transform".to_string(), value: vec![] }, ] ); } #[test] fn cache_control_parse_invalid_directive_drops_remainder() { let header = r#"max-age=60, no-cache, ="cookie", no-transform"#; let directives = CacheControlParser::new([header]).collect::>(); assert_eq!( directives, vec![ CacheControlDirective { name: "max-age".to_string(), value: b"60".to_vec(), }, CacheControlDirective { name: "no-cache".to_string(), value: vec![] }, CacheControlDirective { name: "must-revalidate".to_string(), value: vec![] }, ] ); } #[test] fn cache_control_parse_name_normalized() { let header = r"MAX-AGE=60"; let directives = CacheControlParser::new([header]).collect::>(); assert_eq!( directives, vec![CacheControlDirective { name: "max-age".to_string(), value: b"60".to_vec(), }] ); } // When a duplicate directive is found, we keep the first one // and add in a `must-revalidate` directive to indicate that // things are stale and the client should do a re-check. #[test] fn cache_control_parse_duplicate_directives() { let header = r"max-age=60, no-cache, max-age=30"; let directives = CacheControlParser::new([header]).collect::>(); assert_eq!( directives, vec![ CacheControlDirective { name: "max-age".to_string(), value: b"60".to_vec(), }, CacheControlDirective { name: "no-cache".to_string(), value: vec![] }, CacheControlDirective { name: "must-revalidate".to_string(), value: vec![] }, ] ); } #[test] fn cache_control_parse_duplicate_directives_across_headers() { let headers = [r"max-age=60, no-cache", r"max-age=30"]; let directives = CacheControlParser::new(headers).collect::>(); assert_eq!( directives, vec![ CacheControlDirective { name: "max-age".to_string(), value: b"60".to_vec(), }, CacheControlDirective { name: "no-cache".to_string(), value: vec![] }, CacheControlDirective { name: "must-revalidate".to_string(), value: vec![] }, ] ); } // Tests that we don't emit must-revalidate multiple times // even when something is duplicated multiple times. #[test] fn cache_control_parse_duplicate_redux() { let header = r"max-age=60, no-cache, no-cache, max-age=30"; let directives = CacheControlParser::new([header]).collect::>(); assert_eq!( directives, vec![ CacheControlDirective { name: "max-age".to_string(), value: b"60".to_vec(), }, CacheControlDirective { name: "no-cache".to_string(), value: vec![] }, CacheControlDirective { name: "must-revalidate".to_string(), value: vec![] }, ] ); } } uv-0.9.17+ds1/crates/uv-client/src/httpcache/mod.rs000066400000000000000000001707031520155276700220150ustar00rootroot00000000000000/*! A somewhat simplistic implementation of HTTP cache semantics. This implementation was guided by the following things: * RFCs 9110 and 9111. * The `http-cache-semantics` crate. (The implementation here is completely different, but the source of `http-cache-semantics` helped guide the implementation here and understanding of HTTP caching.) * A desire for our cache policy to support zero-copy deserialization. That is, we want the cached response fast path (where no revalidation request is necessary) to avoid any costly deserialization for the cache policy at all. # Flow While one has to read the relevant RFCs to get a full understanding of HTTP caching, doing so is... difficult to say the least. It is at the very least not quick to do because the semantics are scattered all over the place. But, I think we can do a quick overview here. Let's start with the obvious. HTTP caching exists to avoid network requests, and, if a request is unavoidable, bandwidth. The central actor in HTTP caching is the `Cache-Control` header, which can exist on *both* requests and responses. The value of this header is a list of directives that control caching behavior. They can outright disable it (`no-store`), force cache invalidation (`no-cache`) or even permit the cache to return responses that are explicitly stale (`max-stale`). The main thing that typically drives cache interactions is `max-age`. When set on a response, this means that the server is willing to let clients hold on to a response for up to the amount of time in `max-age` before the client must ask the server for a fresh response. In our case, the main utility of `max-age` is two fold: * PyPI sets a `max-age` of 600 seconds (10 minutes) on its responses. As long as our cached responses have an age less than this, we can completely avoid talking to PyPI at all when we need access to the full set of versions for a package. * Most other assets, like wheels, are forever immutable. They will never change. So servers will typically set a very high `max-age`, which means we will almost never need to ask the server for permission to reuse our cached wheel. When a cached response exceeds the `max-age` configured on a response, then we call that response stale. Generally speaking, we won't return responses from the cache that are known to be stale. (This can be overridden in the request by adding a `max-stale` cache-control directive, but nothing in uv does this at time of writing.) When a response is stale, we don't necessarily need to give up completely. It is at this point that we can send something called a re-validation request. A re-validation request includes with it some metadata (usually an "entity tag" or `etag` for short) that was on the cached response (which is now stale). When we send this request, the server can compare it with its most up-to-date version of the resource. If its entity tag matches the one we gave it (among other possible criteria), then the server can skip returning the body and instead just return a small HTTP 304 NOT MODIFIED response. When we get this type of response, it's the server telling us that our cached response which we *thought* was stale is no longer stale. It's fresh again and we needn't get a new copy. We will need to update our stored `CachePolicy` though, since the HTTP 304 NOT MODIFIED response we got might included updated metadata relevant to the behavior of caching (like a new `Age` header). # Scope In general, the cache semantics implemented below are targeted toward uv's use case: a private client cache for custom data objects. This constraint results in a modest simplification in what we need to support. That is, we don't need to cache the entirety of the request's or response's headers (like what `http-cache-semantics`) does. Instead, we only need to cache the data necessary to *make decisions* about HTTP caching. One example of this is the `Vary` response header. This requires checking the the headers listed in a cached response have the same value in the original request and the new request. If the new request has different values for those headers (as specified in the cached response) than what was in the original request, then the new request cannot used our cached response. Normally, this would seemingly require storing all of the original request's headers. But we only store the headers listed in the response. Also, since we aren't a proxy, there are a host of proxy-specific rules for managing headers and data that we needn't care about. # Zero-copy deserialization As mentioned above, we would really like our fast path (that is, a cached response that we deem "fresh" and thus don't need to send a re-validation request for) to avoid needing to actually deserialize a `CachePolicy`. While a `CachePolicy` isn't particularly big, it is in our critical path. Yet, we still need a `CachePolicy` to be able to decide whether a cached response is still fresh or not. (This decision procedure is non-trivial, so it *probably* doesn't make too much sense to hack around it with something simpler.) We attempt to achieve this by implementing the `rkyv` traits for all of our types. This means that if we read a `Vec` from a file, then we can very cheaply turn that into a `rkyvutil::OwnedArchive`. Creating that only requires a quick validation step, but is otherwise free. We can then use that as-if it were an `Archived` (which is an alias for the `ArchivedCachePolicy` type implicitly introduced by `derive(rkyv::Archive)`). Crucially, this is why we implement all of our HTTP cache semantics logic on `ArchivedCachePolicy` and *not* `CachePolicy`. It can be easy to forget this because `rkyv` does such an amazing job of making its use of archived types very closely resemble that of the standard types. For example, whenever the methods below are accessing a field whose type is a `Vec` in the normal type, what's actually being accessed is a [`rkyv::vec::ArchivedVec`]. Similarly, for strings, it's [`rkyv::string::ArchivedString`] and not a standard library `String`. This all works somewhat seamlessly because all of the cache semantics are generally just read-only operations, but if you stray from the path, you're likely to get whacked over the head. One catch here is that we actually want the HTTP cache semantics to be available on `CachePolicy` too. At least, at time of writing, we do. To achieve this `CachePolicy::to_archived` is provided, which will serialize the `CachePolicy` to its archived representation in bytes, and then turn that into an `OwnedArchive` which derefs to `ArchivedCachePolicy`. This is a little extra cost, but the idea is that a `CachePolicy` (not an `ArchivedCachePolicy`) should only be used in the slower path (i.e., when you actually need to make an HTTP request). [`rkyv::vec::ArchivedVec`]: https://docs.rs/rkyv/0.7.43/rkyv/vec/struct.ArchivedVec.html [`rkyv::string::ArchivedString`]: https://docs.rs/rkyv/0.7.43/rkyv/string/struct.ArchivedString.html # Additional reading * Short introduction to `Cache-Control`: * Caching best practices: * Overview of HTTP caching: * MDN docs for `Cache-Control`: * The 1997 RFC for HTTP 1.1: * The 1999 update to HTTP 1.1: * The "stale content" cache-control extension: * HTTP 1.1 caching (superseded by RFC 9111): * The "immutable" cache-control extension: * HTTP semantics (If-None-Match, etc.): * HTTP caching (obsoletes RFC 7234): */ use std::time::{Duration, SystemTime}; use http::header::HeaderValue; use crate::rkyvutil::OwnedArchive; use self::control::CacheControl; mod control; /// Knobs to configure uv's cache behavior. /// /// At time of writing, we don't expose any way of modifying these since I /// suspect we won't ever need to. We split them out into their own type so /// that they can be shared between `CachePolicyBuilder` and `CachePolicy`. #[derive( Clone, Debug, Default, rkyv::Archive, rkyv::Deserialize, rkyv::Portable, rkyv::Serialize, bytecheck::CheckBytes, )] // Since `CacheConfig` is so simple, we can use itself as the archived type. // But note that this will fall apart if even something like an Option is // added. #[rkyv(as = Self)] #[repr(C)] struct CacheConfig { shared: bool, } /// A builder for constructing a `CachePolicy`. /// /// A builder can be used directly when spawning fresh HTTP requests /// without a cached response. A builder is also constructed for you via /// [`CachePolicy::before_request`] when a cached response exists but is stale. /// /// The main idea of a builder is that it manages the flow of data needed to /// construct a `CachePolicy`. That is, you start with an HTTP request, then /// you get a response and finally a new `CachePolicy`. #[derive(Debug)] pub struct CachePolicyBuilder { /// The configuration controlling the behavior of the cache. config: CacheConfig, /// A subset of information from the HTTP request that we will store. This /// is needed to make future decisions about cache behavior. request: Request, /// The full set of request headers. This copy is necessary because the /// headers are needed in order to correctly capture the values necessary /// to implement the `Vary` check, as per [RFC 9111 S4.1]. The upside is /// that this is not actually persisted in a `CachePolicy`. We only need it /// until we have the response. /// /// The precise reason why this copy is intrinsically needed is because /// sending a request requires ownership of the request. Yet, we don't know /// which header values we need to store in our cache until we get the /// response back. Thus, these headers must be persisted until after the /// point we've given up ownership of the request. /// /// [RFC 9111 S4.1]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1 request_headers: http::HeaderMap, } impl CachePolicyBuilder { /// Create a new builder of a cache policy, starting with the request. pub fn new(request: &reqwest::Request) -> Self { let config = CacheConfig::default(); let request_headers = request.headers().clone(); let request = Request::from(request); Self { config, request, request_headers, } } /// Return a new policy given the response to the request that this builder /// was created with. pub fn build(self, response: &reqwest::Response) -> CachePolicy { let vary = Vary::from_request_response_headers(&self.request_headers, response.headers()); CachePolicy { config: self.config, request: self.request, response: Response::from(response), vary, } } } /// A value encapsulating the data needed to implement HTTP caching behavior /// for uv. /// /// A cache policy is meant to be stored and persisted with the data being /// cached. It is specifically meant to capture the smallest amount of /// information needed to determine whether a cached response is stale or not, /// and the information required to issue a re-validation request. /// /// This does not provide a complete set of HTTP cache semantics. Notably /// absent from this (among other things that uv probably doesn't care /// about it) are proxy cache semantics. #[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] pub struct CachePolicy { /// The configuration controlling the behavior of the cache. config: CacheConfig, /// A subset of information from the HTTP request that we will store. This /// is needed to make future decisions about cache behavior. request: Request, /// A subset of information from the HTTP response that we will store. This /// is needed to make future decisions about cache behavior. response: Response, /// This contains the set of vary header names (from the cached response) /// and the corresponding values (from the original request) used to verify /// whether a new request can utilize a cached response or not. This is /// placed outside of `request` and `response` because it contains bits /// from both! vary: Vary, } impl CachePolicy { /// Convert this to an owned archive value. /// /// It's necessary to call this in order to make decisions with this cache /// policy. Namely, all of the cache semantics logic is implemented on the /// archived types. /// /// These do incur an extra cost, but this should only be needed when you /// don't have an `ArchivedCachePolicy`. And that should only occur when /// you're actually performing an HTTP request. In that case, the extra /// cost that is done here to convert a `CachePolicy` to its archived form /// should be marginal. pub fn to_archived(&self) -> OwnedArchive { // There's no way (other than OOM) for serializing this type to fail. OwnedArchive::from_unarchived(self).expect("all possible values can be archived") } } impl ArchivedCachePolicy { /// Determines what caching behavior is correct given an existing /// `CachePolicy` and a new HTTP request for the resource managed by this /// cache policy. This is done as per [RFC 9111 S4]. /// /// Calling this method conceptually corresponds to asking the following /// question: "I have a cached response for an incoming HTTP request. May I /// return that cached response, or do I need to go back to the progenitor /// of that response to determine whether it's still the latest thing?" /// /// This returns one of three possible behaviors: /// /// 1. The cached response is still fresh, and the caller may return /// the cached response without issuing an HTTP requests. /// 2. The cached response is stale. The caller should send a re-validation /// request and then call `CachePolicy::after_response` to determine whether /// the cached response is actually fresh, or if it's stale and needs to /// be updated. /// 3. The given request does not match the cache policy identification. /// Generally speaking, this usually implies a bug with the cache in that /// it loaded a cache policy that does not match the request. /// /// In the case of (2), the given request is modified in place such that /// it is suitable as a revalidation request. /// /// [RFC 9111 S4]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4 pub fn before_request(&self, request: &mut reqwest::Request) -> BeforeRequest { let now = SystemTime::now(); // If the response was never storable, then we just bail out // completely. if !self.is_storable() { tracing::trace!( "Request {} does not match cache request {} because it isn't storable", request.url(), self.request.uri, ); return BeforeRequest::NoMatch; } // "When presented with a request, a cache MUST NOT reuse a stored // response unless..." // // "the presented target URI and that of the stored response match, // and..." if self.request.uri != request.url().as_str() { tracing::trace!( "Request {} does not match cache URL of {}", request.url(), self.request.uri, ); return BeforeRequest::NoMatch; } // "the request method associated with the stored response allows it to // be used for the presented request, and..." if request.method() != http::Method::GET && request.method() != http::Method::HEAD { tracing::trace!( "Method {:?} for request {} is not supported by this cache", request.method(), request.url(), ); return BeforeRequest::NoMatch; } // "Request header fields nominated by the stored response (if any) // match those presented, and..." // // We don't support the `Vary` header, so if it was set, we // conservatively require revalidation. if !self.vary.matches(request.headers()) { tracing::trace!( "Request {} does not match cached request because of the 'Vary' header", request.url(), ); self.set_revalidation_headers(request); return BeforeRequest::Stale(self.new_cache_policy_builder(request)); } // "the stored response does not contain the no-cache directive, unless // it is successfully validated, and..." if self.response.headers.cc.no_cache { self.set_revalidation_headers(request); return BeforeRequest::Stale(self.new_cache_policy_builder(request)); } // "the stored response is one of the following: ..." // // "fresh, or..." // "allowed to be served stale, or..." if self.is_fresh(now, request) { return BeforeRequest::Fresh; } // "successfully validated." // // In this case, callers will need to send a revalidation request. self.set_revalidation_headers(request); BeforeRequest::Stale(self.new_cache_policy_builder(request)) } /// This implements the logic for handling the response to a request that /// may be a revalidation request, as per [RFC 9111 S4.3.3] and [RFC 9111 /// S4.3.4]. That is, the cache policy builder given here should be the one /// returned by `CachePolicy::before_request` with the response received /// from the origin server for the possibly-revalidating request. /// /// Even if the request is new (in that there is no response cached /// for it), callers may use this routine. But generally speaking, /// callers are only supposed to use this routine after getting a /// [`BeforeRequest::Stale`]. /// /// The return value indicates whether the cached response is still fresh /// (that is, `AfterResponse::NotModified`) or if it has changed (that is, /// `AfterResponse::Modified`). In the latter case, the cached response has /// been invalidated and the caller should cache the new response. In the /// former case, the cached response is still considered fresh. /// /// In either case, callers should update their cache with the new policy. /// /// [RFC 9111 S4.3.3]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.3 /// [RFC 9111 S4.3.4]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.4 pub fn after_response( &self, cache_policy_builder: CachePolicyBuilder, response: &reqwest::Response, ) -> AfterResponse { let mut new_policy = cache_policy_builder.build(response); if self.is_modified(&new_policy) { AfterResponse::Modified(new_policy) } else { new_policy.response.status = self.response.status.into(); AfterResponse::NotModified(new_policy) } } fn is_modified(&self, new_policy: &CachePolicy) -> bool { // From [RFC 9111 S4.3.3], // // "A 304 (Not Modified) response status code indicates that the stored // response can be updated and reused" // // So if we don't get a 304, then we know our cached response is seen // as stale by the origin server. // // [RFC 9111 S4.3.3]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.3 if new_policy.response.status != 304 { tracing::trace!( "Resource is modified because status is {:?} and not 304", new_policy.response.status ); return true; } // As per [RFC 9111 S4.3.4], we need to confirm that our validators match. Here, // we check `ETag`. // // [RFC 9111 S4.3.4]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.4 if let Some(old_etag) = self.response.headers.etag.as_ref() { if let Some(new_etag) = new_policy.response.headers.etag.as_ref() { // We don't support weak validators, so only match if they're // both strong. if !old_etag.weak && !new_etag.weak && old_etag.value == new_etag.value { tracing::trace!( "Resource is not modified because old and new etag values ({:?}) match", new_etag.value, ); return false; } } } // As per [RFC 9111 S4.3.4], we need to confirm that our validators match. Here, // we check `Last-Modified`. // // [RFC 9111 S4.3.4]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.4 if let Some(old_last_modified) = self.response.headers.last_modified_unix_timestamp.as_ref() { if let Some(new_last_modified) = new_policy .response .headers .last_modified_unix_timestamp .as_ref() { if old_last_modified == new_last_modified { tracing::trace!( "Resource is not modified because modified times ({new_last_modified:?}) match", ); return false; } } } // As per [RFC 9111 S4.3.4], if we have no validators anywhere, then // we can just rely on the HTTP 304 status code and reuse the cached // response. // // [RFC 9111 S4.3.4]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.4 if self.response.headers.etag.is_none() && new_policy.response.headers.etag.is_none() && self.response.headers.last_modified_unix_timestamp.is_none() && new_policy .response .headers .last_modified_unix_timestamp .is_none() { tracing::trace!( "Resource is not modified because there are no etags or last modified \ timestamps, so we assume the 304 status is correct", ); return false; } true } /// Sets the relevant headers on the given request so that it can be used /// as a revalidation request. As per [RFC 9111 S4.3.1], this permits the /// origin server to check if the content is different from our cached /// response. If it isn't, then the origin server can return an HTTP 304 /// NOT MODIFIED status, which avoids the need to re-transmit the response /// body. That is, it indicates that our cached response is still fresh. /// /// This will always use a strong etag validator if it's present on the /// cached response. If the given request already has an etag validator /// on it, this routine will add to it and not replace it. /// /// In contrast, if the request already has the `If-Modified-Since` header /// set, then this will not change or replace it. If it's not set, then one /// is added if the cached response had a valid `Last-Modified` header. /// /// [RFC 9111 S4.3.1]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.1 fn set_revalidation_headers(&self, request: &mut reqwest::Request) { // As per [RFC 9110 13.1.2] and [RFC 9111 S4.3.1], if our stored // response has an etag, we should send it back via the `If-None-Match` // header. The idea is that the server should only "do" the request if // none of the tags match. If there is a match, then the server can // return HTTP 304 indicating that our stored response is still fresh. // // [RFC 9110 S13.1.2]: https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2 // [RFC 9111 S4.3.1]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.1 if let Some(etag) = self.response.headers.etag.as_ref() { // We don't support weak validation principally because we want to // be notified if there was a change in the content. Namely, from // RFC 9110 S13.1.2: "... weak entity tags can be used for cache // validation even if there have been changes to the representation // data." if !etag.weak { if let Ok(header) = HeaderValue::from_bytes(&etag.value) { request.headers_mut().append("if-none-match", header); } } } // We also set `If-Modified-Since` as per [RFC 9110 S13.1.3] and [RFC // 9111 S4.3.1]. Generally, `If-None-Match` will override this, but we // set it in case `If-None-Match` is not supported. // // [RFC 9110 S13.1.3]: https://www.rfc-editor.org/rfc/rfc9110#section-13.1.3 // [RFC 9111 S4.3.1]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3.1 if !request.headers().contains_key("if-modified-since") { if let Some(&last_modified_unix_timestamp) = self.response.headers.last_modified_unix_timestamp.as_ref() { if let Some(last_modified) = unix_timestamp_to_header(last_modified_unix_timestamp.into()) { request .headers_mut() .insert("if-modified-since", last_modified); } } } } /// Returns true if and only if the response is storable as per /// [RFC 9111 S3]. /// /// [RFC 9111 S3]: https://www.rfc-editor.org/rfc/rfc9111.html#section-3 pub fn is_storable(&self) -> bool { // In the absence of other signals, we are limited to caching responses // with a code that is heuristically cacheable as per [RFC 9110 S15.1]. // // [RFC 9110 S15.1]: https://www.rfc-editor.org/rfc/rfc9110#section-15.1 const HEURISTICALLY_CACHEABLE_STATUS_CODES: &[u16] = &[200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501]; // N.B. This routine could be "simpler", but we bias toward // following the flow of logic as closely as possible as written // in RFC 9111 S3. // "the request method is understood by the cache" // // We just don't bother with anything that isn't GET. if !matches!( self.request.method, ArchivedMethod::Get | ArchivedMethod::Head ) { tracing::trace!( "Response from {} is not storable because of the request method {:?}", self.request.uri, self.request.method ); return false; } // "the response status code is final" // // ... and we'll put more restrictions on status code // below, but we can bail out early here. if !self.response.has_final_status() { tracing::trace!( "Response from {} is not storable because it has \ a non-final status code {:?}", self.request.uri, self.response.status, ); return false; } // "if the response status code is 206 or 304, or the must-understand // cache directive (see Section 5.2.2.3) is present: the cache // understands the response status code" // // We don't currently support `must-understand`. We also don't support // partial content (206). And 304 not modified shouldn't be cached // itself. if self.response.status == 206 || self.response.status == 304 { tracing::trace!( "Response from {} is not storable because it has \ an unsupported status code {:?}", self.request.uri, self.response.status, ); return false; } // "The no-store request directive indicates that a cache MUST NOT // store any part of either this request or any response to it." // // (This is from RFC 9111 S5.2.1.5, and doesn't seem to be mentioned in // S3.) if self.request.headers.cc.no_store { tracing::trace!( "Response from {} is not storable because its request has \ a 'no-store' cache-control directive", self.request.uri, ); return false; } // "the no-store cache directive is not present in the response" if self.response.headers.cc.no_store { tracing::trace!( "Response from {} is not storable because it has \ a 'no-store' cache-control directive", self.request.uri, ); return false; } // "if the cache is shared ..." if self.config.shared { // "if the cache is shared: the private response directive is either // not present or allows a shared cache to store a modified response" // // We don't support more granular "private" directives (which allow // caching all of a private HTTP response in a shared cache only after // removing some subset of the response's headers that are deemed // private). if self.response.headers.cc.private { tracing::trace!( "Response from {} is not storable because this is a shared \ cache and has a 'private' cache-control directive", self.request.uri, ); return false; } // "if the cache is shared: the Authorization header field is not // present in the request or a response directive is present that // explicitly allows shared caching" if self.request.headers.authorization && !self.allows_authorization_storage() { tracing::trace!( "Response from {} is not storable because this is a shared \ cache and the request has an 'Authorization' header set and \ the response has indicated that caching requests with an \ 'Authorization' header is allowed", self.request.uri, ); return false; } } // "the response contains at least one of the following ..." // // "a public response directive" if self.response.headers.cc.public { tracing::trace!( "Response from {} is storable because it has \ a 'public' cache-control directive", self.request.uri, ); return true; } // "a private response directive, if the cache is not shared" if !self.config.shared && self.response.headers.cc.private { tracing::trace!( "Response from {} is storable because this is a shared cache \ and has a 'private' cache-control directive", self.request.uri, ); return true; } // "an Expires header field" if self.response.headers.expires_unix_timestamp.is_some() { tracing::trace!( "Response from {} is storable because it has an \ 'Expires' header set", self.request.uri, ); return true; } // "a max-age response directive" if self.response.headers.cc.max_age_seconds.is_some() { tracing::trace!( "Response from {} is storable because it has an \ 'max-age' cache-control directive", self.request.uri, ); return true; } // "if the cache is shared: an s-maxage response directive" if self.config.shared && self.response.headers.cc.s_maxage_seconds.is_some() { tracing::trace!( "Response from {} is storable because this is a shared cache \ and has a 's-maxage' cache-control directive", self.request.uri, ); return true; } // "a cache extension that allows it to be cached" // ... we don't support any extensions. // // "a status code that is defined as heuristically cacheable" if HEURISTICALLY_CACHEABLE_STATUS_CODES.contains(&self.response.status.into()) { tracing::trace!( "Response from {} is storable because it has a \ heuristically cacheable status code {:?}", self.request.uri, self.response.status, ); return true; } tracing::trace!( "Response from {} is not storable because it does not meet any \ of the necessary criteria (e.g., it doesn't have an 'Expires' \ header set or a 'max-age' cache-control directive)", self.request.uri, ); false } /// Returns true when a response is storable even if it has an /// `Authorization` header, as per [RFC 9111 S3.5]. /// /// [RFC 9111 S3.5]: https://www.rfc-editor.org/rfc/rfc9111.html#section-3.5 fn allows_authorization_storage(&self) -> bool { self.response.headers.cc.must_revalidate || self.response.headers.cc.public || self.response.headers.cc.s_maxage_seconds.is_some() } /// Returns true if the response is considered fresh as per [RFC 9111 /// S4.2]. If the response is not fresh, then it considered stale and ought /// to be revalidated with the origin server. /// /// [RFC 9111 S4.2]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2 fn is_fresh(&self, now: SystemTime, request: &reqwest::Request) -> bool { let freshness_lifetime = self.freshness_lifetime().as_secs(); let age = self.age(now).as_secs(); // Per RFC 8246, the `immutable` directive means that a reload from an // end user should not result in a revalidation request. Indeed, the // `immutable` directive seems to imply that clients should never talk // to the origin server until the cached response is stale with respect // to its freshness lifetime (as set by the server). // // A *force* reload from an end user should override this, but we // currently have no path for that in this implementation. Instead, we // just interpret `immutable` as meaning that any directives on the // new request that would otherwise result in sending a revalidation // request are ignored. // // [RFC 8246]: https://httpwg.org/specs/rfc8246.html if !self.response.headers.cc.immutable { let reqcc = request .headers() .get_all("cache-control") .iter() .collect::(); // As per [RFC 9111 S5.2.1.4], if the request has `no-cache`, then we should // respect that. // // [RFC 9111 S5.2.1.4]: https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.4 if reqcc.no_cache { tracing::trace!( "Request to {} does not have a fresh cache entry because \ it has a 'no-cache' cache-control directive", request.url(), ); return false; } // If the request has a max-age directive, then we should respect that // as per [RFC 9111 S5.2.1.1]. // // [RFC 9111 S5.2.1.1]: https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1 if let Some(&max_age) = reqcc.max_age_seconds.as_ref() { if age > max_age { tracing::trace!( "Request to {} does not have a fresh cache entry because \ the cached response's age is {} seconds and the max age \ allowed by the request is {} seconds", request.url(), age, max_age, ); return false; } } // If the request has a min-fresh directive, then we only consider a // cached response fresh if the remaining time it has to live exceeds // the threshold provided, as per [RFC 9111 S5.2.1.3]. // // [RFC 9111 S5.2.1.3]: https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.3 if let Some(&min_fresh) = reqcc.min_fresh_seconds.as_ref() { let time_to_live = freshness_lifetime.saturating_sub(unix_timestamp(now)); if time_to_live < min_fresh { tracing::trace!( "Request to {} does not have a fresh cache entry because \ the request set a 'min-fresh' cache-control directive, \ and its time-to-live is {} seconds but it needs to be \ at least {} seconds", request.url(), time_to_live, min_fresh, ); // Note that S5.2.1.3 does not say that max-stale overrides // this, so we ignore it here. return false; } } } if age > freshness_lifetime { let allows_stale = self.allows_stale(now); if !allows_stale { tracing::trace!( "Request to {} does not have a fresh cache entry because \ its age is {} seconds, it is greater than the freshness \ lifetime of {} seconds and stale cached responses are not \ allowed", request.url(), age, freshness_lifetime, ); return false; } } true } /// Returns true if we're allowed to serve a stale response, as per [RFC /// 9111 S4.2.4]. /// /// [RFC 9111 S4.2.4]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.4 fn allows_stale(&self, now: SystemTime) -> bool { // As per [RFC 9111 S5.2.2.2], if `must-revalidate` is present, then // caches cannot reuse a stale response without talking to the server // first. Note that RFC 9111 doesn't seem to say anything about the // interaction between must-revalidate and max-stale, so we assume that // must-revalidate takes precedent. // // [RFC 9111 S5.2.2.2]: https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.2 if self.response.headers.cc.must_revalidate { tracing::trace!( "Request to {} has a cached response that does not \ permit staleness because the response has a 'must-revalidate' \ cache-control directive set", self.request.uri, ); return false; } if let Some(&max_stale) = self.request.headers.cc.max_stale_seconds.as_ref() { // As per [RFC 9111 S5.2.1.2], if the client has max-stale set, // then stale responses are allowed, but only if they are stale // within a given threshold. // // [RFC 9111 S5.2.1.2]: https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.2 let stale_amount = self .age(now) .as_secs() .saturating_sub(self.freshness_lifetime().as_secs()); if stale_amount <= max_stale.into() { tracing::trace!( "Request to {} has a cached response that allows staleness \ in this case because the stale amount is {} seconds and the \ 'max-stale' cache-control directive set by the cached request \ is {} seconds", self.request.uri, stale_amount, max_stale, ); return true; } } // As per [RFC 9111 S4.2.4], we shouldn't use stale responses unless // we're explicitly allowed to (e.g., via `max-stale` above): // // "A cache MUST NOT generate a stale response unless it is // disconnected or doing so is explicitly permitted by the client or // origin server..." // // [RFC 9111 S4.2.4]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.4 tracing::trace!( "Request to {} has a cached response that does not allow staleness", self.request.uri, ); false } /// Returns the age of the HTTP response as per [RFC 9111 S4.2.3]. /// /// The age of a response, essentially, refers to how long it has been /// since the response was created by the origin server. The age is used /// to compare with the freshness lifetime of the response to determine /// whether the response is fresh or stale. /// /// [RFC 9111 S4.2.3]: https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-age fn age(&self, now: SystemTime) -> Duration { // RFC 9111 S4.2.3 let apparent_age = u64::from(self.response.unix_timestamp).saturating_sub(self.response.header_date()); let response_delay = u64::from(self.response.unix_timestamp) .saturating_sub(self.request.unix_timestamp.into()); let corrected_age_value = self.response.header_age().saturating_add(response_delay); let corrected_initial_age = apparent_age.max(corrected_age_value); let resident_age = unix_timestamp(now).saturating_sub(self.response.unix_timestamp.into()); let current_age = corrected_initial_age + resident_age; Duration::from_secs(current_age) } /// Returns how long a response should be considered "fresh" as per /// [RFC 9111 S4.2.1]. When this returns false, the response should be /// considered stale and the client should revalidate with the server. /// /// If there are no indicators of a response's freshness lifetime, then /// this returns `0`. That is, the response will be considered stale in all /// cases. /// /// [RFC 9111 S4.2.1]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.1 fn freshness_lifetime(&self) -> Duration { if self.config.shared { if let Some(&s_maxage) = self.response.headers.cc.s_maxage_seconds.as_ref() { let duration = Duration::from_secs(s_maxage.into()); tracing::trace!( "Freshness lifetime found via shared \ cache-control max age setting: {duration:?}" ); return duration; } } if let Some(&max_age) = self.response.headers.cc.max_age_seconds.as_ref() { let duration = Duration::from_secs(max_age.into()); tracing::trace!( "Freshness lifetime found via cache-control max age setting: {duration:?}" ); return duration; } if let Some(&expires) = self.response.headers.expires_unix_timestamp.as_ref() { let duration = Duration::from_secs(u64::from(expires).saturating_sub(self.response.header_date())); tracing::trace!("Freshness lifetime found via expires header: {duration:?}"); return duration; } if self.response.headers.last_modified_unix_timestamp.is_some() { // We previously computed this heuristic freshness lifetime by // looking at the difference between the last modified header and // the response's date header. We then asserted that the cached // response ought to be "fresh" for 10% of that interval. // // It turns out that this can result in very long freshness // lifetimes[1] that lead to uv caching too aggressively. // // Since PyPI sets a max-age of 600 seconds and since we're // principally just interacting with Python package indices here, // we just assume a freshness lifetime equal to what PyPI has. // // Note though that a better solution here is for the index to // support proper HTTP caching headers (ideally Cache-Control, but // Expires also works too, as above). // // [1]: https://github.com/astral-sh/uv/issues/5351#issuecomment-2260588764 let duration = Duration::from_secs(600); tracing::trace!( "Freshness lifetime heuristically assumed \ because of presence of last-modified header: {duration:?}" ); return duration; } // Without any indicators as to the freshness lifetime, we act // conservatively and use a value that will always result in a response // being treated as stale. tracing::trace!("Could not determine freshness lifetime, assuming none exists"); Duration::ZERO } fn new_cache_policy_builder(&self, request: &reqwest::Request) -> CachePolicyBuilder { let request_headers = request.headers().clone(); CachePolicyBuilder { config: self.config.clone(), request: Request::from(request), request_headers, } } } /// The result of calling [`CachePolicy::before_request`]. /// /// This dictates what the caller should do next by indicating whether the /// cached response is stale or not. #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum BeforeRequest { /// The cached response is still fresh, and the caller may return the /// cached response without issuing an HTTP requests. Fresh, /// The cached response is stale. The caller should send a re-validation /// request and then call `CachePolicy::after_response` to determine /// whether the cached response is actually fresh, or if it's stale and /// needs to be updated. Stale(CachePolicyBuilder), /// The given request does not match the cache policy identification. /// Generally speaking, this is usually implies a bug with the cache in /// that it loaded a cache policy that does not match the request. NoMatch, } /// The result of called [`CachePolicy::after_response`]. /// /// This is meant to report whether a revalidation request was successful or /// not. If it was, then a `AfterResponse::NotModified` is returned. Otherwise, /// the server determined the cached response was truly stale and in need of /// updated. #[derive(Debug)] pub enum AfterResponse { /// The cached response is still fresh. NotModified(CachePolicy), /// The cached response has been invalidated and needs to be updated with /// the new data in the response to the revalidation request. Modified(CachePolicy), } #[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] struct Request { uri: String, method: Method, headers: RequestHeaders, unix_timestamp: u64, } impl<'a> From<&'a reqwest::Request> for Request { fn from(from: &'a reqwest::Request) -> Self { Self { uri: from.url().to_string(), method: Method::from(from.method()), headers: RequestHeaders::from(from.headers()), unix_timestamp: unix_timestamp(SystemTime::now()), } } } #[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] struct RequestHeaders { /// The cache control directives from the `Cache-Control` header. cc: CacheControl, /// This is set to `true` only when an `Authorization` header is present. /// We don't need to record the value. authorization: bool, } impl<'a> From<&'a http::HeaderMap> for RequestHeaders { fn from(from: &'a http::HeaderMap) -> Self { Self { cc: from.get_all("cache-control").iter().collect(), authorization: from.contains_key("authorization"), } } } /// The HTTP method used on a request. /// /// We don't both representing methods of requests whose responses we won't /// cache. Instead, we treat them as "unrecognized" and consider the responses /// not-storable. #[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] #[repr(u8)] enum Method { Get, Head, Unrecognized, } impl<'a> From<&'a http::Method> for Method { fn from(from: &'a http::Method) -> Self { if from == http::Method::GET { Self::Get } else if from == http::Method::HEAD { Self::Head } else { Self::Unrecognized } } } #[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] struct Response { status: u16, headers: ResponseHeaders, unix_timestamp: u64, } impl ArchivedResponse { /// Returns the "age" header value on this response, with a fallback of `0` /// if the header doesn't exist or is invalid, as per [RFC 9111 S4.2.3]. /// /// Note that this does not reflect the true "age" of a response. That /// is computed via `ArchivedCachePolicy::age` as it may need additional /// information (such as the request time). /// /// [RFC 9111 S4.2.3]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.3 fn header_age(&self) -> u64 { self.headers .age_seconds .as_ref() .map(u64::from) .unwrap_or(0) } /// Returns the "date" header value on this response, with a fallback to /// the time the response was received as per [RFC 9110 S6.6.1]. /// /// [RFC 9110 S6.6.1]: https://www.rfc-editor.org/rfc/rfc9110#section-6.6.1 fn header_date(&self) -> u64 { self.headers .date_unix_timestamp .unwrap_or(self.unix_timestamp) .into() } /// Returns true when this response has a status code that is considered /// "final" as per [RFC 9110 S15]. /// /// [RFC 9110 S15]: https://www.rfc-editor.org/rfc/rfc9110#section-15 fn has_final_status(&self) -> bool { self.status >= 200 } } impl<'a> From<&'a reqwest::Response> for Response { fn from(from: &'a reqwest::Response) -> Self { Self { status: from.status().as_u16(), headers: ResponseHeaders::from(from.headers()), unix_timestamp: unix_timestamp(SystemTime::now()), } } } #[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] struct ResponseHeaders { /// The directives from the `Cache-Control` header. cc: CacheControl, /// The value of the `Age` header corresponding to `age_value` as defined /// in [RFC 9111 S4.2.3]. If the `Age` header is not present, it should be /// interpreted at `0`. /// /// [RFC 9111 S4.2.3]: https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-age age_seconds: Option, /// This is `date_value` from [RFC 9111 S4.2.3], which says it corresponds /// to the `Date` header on a response as defined in [RFC 7231 S7.1.1.2]. /// In RFC 7231, if the `Date` header is not present, then the recipient /// should treat its value as equivalent to the time the response was /// received. In this case, that would be `Response::unix_timestamp`. /// /// [RFC 9111 S4.2.3]: https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-age /// [RFC 7231 S7.1.1.2]: https://httpwg.org/specs/rfc7231.html#header.date date_unix_timestamp: Option, /// This is from the `Expires` header as per [RFC 9111 S5.3]. Note that this /// is overridden by the presence of either the `max-age` or `s-maxage` cache /// control directives. /// /// If an `Expires` header was present but did not contain a valid RFC 2822 /// datetime, then this is set to `Some(0)`. (That is, some time in the /// past, which implies the response has already expired.) /// /// [RFC 9111 S5.3]: https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3 expires_unix_timestamp: Option, /// The date from the `Last-Modified` header as specified in [RFC 9110 S8.8.2] /// in RFC 2822 format. It's used to compute a heuristic freshness lifetime for /// the response when other indicators are missing as per [RFC 9111 S4.2.2]. /// /// [RFC 9110 S8.8.2]: https://www.rfc-editor.org/rfc/rfc9110#section-8.8.2 /// [RFC 9111 S4.2.2]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.2 last_modified_unix_timestamp: Option, /// The "entity tag" from the response as per [RFC 9110 S8.8.3], which is /// used in revalidation requests. /// /// [RFC 9110 S8.8.3]: https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3 etag: Option, } impl<'a> From<&'a http::HeaderMap> for ResponseHeaders { fn from(from: &'a http::HeaderMap) -> Self { Self { cc: from.get_all("cache-control").iter().collect(), age_seconds: from .get("age") .and_then(|header| parse_seconds(header.as_bytes())), date_unix_timestamp: from .get("date") .and_then(|header| header.to_str().ok()) .and_then(rfc2822_to_unix_timestamp), expires_unix_timestamp: from .get("expires") .and_then(|header| header.to_str().ok()) .and_then(rfc2822_to_unix_timestamp), last_modified_unix_timestamp: from .get("last-modified") .and_then(|header| header.to_str().ok()) .and_then(rfc2822_to_unix_timestamp), etag: from .get("etag") .map(|header| ETag::parse(header.as_bytes())), } } } #[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] struct ETag { /// The actual `ETag` validator value. /// /// This is received in the response, recorded as part of the cache policy /// and then sent back in a re-validation request. This is the "best" /// way for an HTTP server to return an HTTP 304 NOT MODIFIED status, /// indicating that our cached response is still fresh. value: Vec, /// When `weak` is true, this etag is considered a "weak" validator. In /// effect, it provides weaker semantics than a "strong" validator. As per /// [RFC 9110 S8.8.1]: /// /// "In contrast, a "weak validator" is representation metadata that might /// not change for every change to the representation data. This weakness /// might be due to limitations in how the value is calculated (e.g., /// clock resolution), an inability to ensure uniqueness for all possible /// representations of the resource, or a desire of the resource owner to /// group representations by some self-determined set of equivalency rather /// than unique sequences of data." /// /// We don't currently support weak validation. /// /// [RFC 9110 S8.8.1]: https://www.rfc-editor.org/rfc/rfc9110#section-8.8.1-6 weak: bool, } impl ETag { /// Parses an `ETag` from a header value. /// /// We are a little permissive here and allow arbitrary bytes, /// where as [RFC 9110 S8.8.3] is a bit more restrictive. /// /// [RFC 9110 S8.8.3]: https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3 fn parse(header_value: &[u8]) -> Self { let (value, weak) = if header_value.starts_with(b"W/") { (&header_value[2..], true) } else { (header_value, false) }; Self { value: value.to_vec(), weak, } } } /// Represents the `Vary` header on a cached response, as per [RFC 9110 /// S12.5.5] and [RFC 9111 S4.1]. /// /// This permits responses from the server to express things like, "only used /// an existing cached response if the request from the client has the same /// header values for the headers listed in `Vary` as in the original request." /// /// [RFC 9110 S12.5.5]: https://www.rfc-editor.org/rfc/rfc9110#section-12.5.5 /// [RFC 9111 S4.1]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1 #[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] struct Vary { fields: Vec, } impl Vary { /// Returns a `Vary` header value that will never match any request. fn always_fails_to_match() -> Self { Self { fields: vec![VaryField { name: "*".to_string(), value: vec![], }], } } fn from_request_response_headers( request: &http::HeaderMap, response: &http::HeaderMap, ) -> Self { // Parses the `Vary` header as per [RFC 9110 S12.5.5]. // // [RFC 9110 S12.5.5]: https://www.rfc-editor.org/rfc/rfc9110#section-12.5.5 let mut fields = vec![]; for header in response.get_all("vary") { let Ok(csv) = header.to_str() else { continue }; for header_name in csv.split(',') { let header_name = header_name.trim().to_ascii_lowercase(); // When we see a `*`, that means a failed match is an // inevitability, regardless of anything else. So just give up // and return a `Vary` that will never match. if header_name == "*" { return Self::always_fails_to_match(); } let value = request .get(&header_name) .map(|header| header.as_bytes().to_vec()) .unwrap_or_default(); fields.push(VaryField { name: header_name, value, }); } } Self { fields } } } impl ArchivedVary { /// Returns true only when the `Vary` header on a cached response satisfies /// the request header values given, as per [RFC 9111 S4.1]. /// /// [RFC 9111 S4.1]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1 fn matches(&self, request_headers: &http::HeaderMap) -> bool { for field in self.fields.iter() { // A `*` anywhere means the match always fails. if field.name == "*" { return false; } let request_header_value = request_headers .get(field.name.as_str()) .map_or(&b""[..], |header| header.as_bytes()); if field.value.as_slice() != request_header_value { return false; } } true } } /// A single field and value in a `Vary` header set by the response, /// as per [RFC 9111 S4.1]. /// /// The `name` of the field comes from the `Vary` header in the response, /// while the value of the field comes from the value of the header with the /// same `name` in the original request. These field and value pairs are then /// compared with new incoming requests. If there is a mismatch, then the /// cached response cannot be used. /// /// [RFC 9111 S4.1]: https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1 #[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] struct VaryField { name: String, value: Vec, } fn unix_timestamp(time: SystemTime) -> u64 { time.duration_since(SystemTime::UNIX_EPOCH) .expect("UNIX_EPOCH is as early as it gets") .as_secs() } fn rfc2822_to_unix_timestamp(s: &str) -> Option { rfc2822_to_datetime(s).and_then(|timestamp| u64::try_from(timestamp.as_second()).ok()) } fn rfc2822_to_datetime(s: &str) -> Option { jiff::fmt::rfc2822::DateTimeParser::new() .parse_timestamp(s) .ok() } fn unix_timestamp_to_header(seconds: u64) -> Option { unix_timestamp_to_rfc2822(seconds).and_then(|string| HeaderValue::from_str(&string).ok()) } fn unix_timestamp_to_rfc2822(seconds: u64) -> Option { use jiff::fmt::rfc2822::DateTimePrinter; unix_timestamp_to_datetime(seconds).and_then(|timestamp| { DateTimePrinter::new() .timestamp_to_rfc9110_string(×tamp) .ok() }) } fn unix_timestamp_to_datetime(seconds: u64) -> Option { jiff::Timestamp::from_second(i64::try_from(seconds).ok()?).ok() } fn parse_seconds(value: &[u8]) -> Option { if !value.iter().all(u8::is_ascii_digit) { return None; } std::str::from_utf8(value).ok()?.parse().ok() } uv-0.9.17+ds1/crates/uv-client/src/lib.rs000066400000000000000000000016561520155276700200410ustar00rootroot00000000000000pub use base_client::{ AuthIntegration, BaseClient, BaseClientBuilder, DEFAULT_RETRIES, ExtraMiddleware, RedirectClientWithMiddleware, RequestBuilder, RetryParsingError, UvRetryableStrategy, is_transient_network_error, }; pub use cached_client::{CacheControl, CachedClient, CachedClientError, DataWithCachePolicy}; pub use error::{Error, ErrorKind, WrappedReqwestError}; pub use flat_index::{FlatIndexClient, FlatIndexEntries, FlatIndexEntry, FlatIndexError}; pub use linehaul::LineHaul; pub use registry_client::{ Connectivity, MetadataFormat, RegistryClient, RegistryClientBuilder, SimpleDetailMetadata, SimpleDetailMetadatum, SimpleIndexMetadata, VersionFiles, }; pub use rkyvutil::{Deserializer, OwnedArchive, Serializer, Validator}; mod base_client; mod cached_client; mod error; mod flat_index; mod html; mod httpcache; mod linehaul; mod middleware; mod registry_client; mod remote_metadata; mod rkyvutil; mod tls; uv-0.9.17+ds1/crates/uv-client/src/linehaul.rs000066400000000000000000000124731520155276700210730ustar00rootroot00000000000000use std::env; use serde::{Deserialize, Serialize}; use tracing::instrument; use uv_pep508::MarkerEnvironment; use uv_platform_tags::{Os, Platform}; use uv_static::EnvVars; use uv_version::version; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Installer { pub name: Option, pub version: Option, pub subcommand: Option>, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Implementation { pub name: Option, pub version: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Libc { pub lib: Option, pub version: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Distro { pub name: Option, pub version: Option, pub id: Option, pub libc: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct System { pub name: Option, pub release: Option, } /// Linehaul structs were derived from /// . /// For the sake of parity, the nullability of all the values was kept intact. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct LineHaul { pub installer: Option, pub python: Option, pub implementation: Option, pub distro: Option, pub system: Option, pub cpu: Option, pub openssl_version: Option, pub setuptools_version: Option, pub rustc_version: Option, pub ci: Option, } /// Implements Linehaul information format as defined by /// . /// This metadata is added to the user agent to enrich PyPI statistics. impl LineHaul { /// Initializes Linehaul information based on PEP 508 markers. #[instrument(name = "linehaul", skip_all)] pub fn new( markers: Option<&MarkerEnvironment>, platform: Option<&Platform>, subcommand: Option>, ) -> Self { // https://github.com/pypa/pip/blob/24.0/src/pip/_internal/network/session.py#L87 let looks_like_ci = [ EnvVars::BUILD_BUILDID, EnvVars::BUILD_ID, EnvVars::CI, EnvVars::PIP_IS_CI, ] .iter() .find_map(|&var_name| env::var(var_name).ok().map(|_| true)); let libc = match platform.map(Platform::os) { Some(Os::Manylinux { major, minor }) => Some(Libc { lib: Some("glibc".to_string()), version: Some(format!("{major}.{minor}")), }), Some(Os::Musllinux { major, minor }) => Some(Libc { lib: Some("musl".to_string()), version: Some(format!("{major}.{minor}")), }), _ => None, }; // Build Distro as Linehaul expects. let distro: Option = if cfg!(target_os = "linux") { // Gather distribution info from /etc/os-release. sys_info::linux_os_release().ok().map(|info| Distro { // e.g., Jammy, Focal, etc. id: info.version_codename, // e.g., Ubuntu, Fedora, etc. name: info.name, // e.g., 22.04, etc. version: info.version_id, // e.g., glibc 2.38, musl 1.2 libc, }) } else if cfg!(target_os = "macos") { let version = match platform.map(Platform::os) { Some(Os::Macos { major, minor }) => Some(format!("{major}.{minor}")), _ => None, }; Some(Distro { // N/A id: None, // pip hardcodes distro name to macOS. name: Some("macOS".to_string()), // Same as python's platform.mac_ver()[0]. version, // N/A libc: None, }) } else { // Always empty on Windows. None }; Self { installer: Option::from(Installer { name: Some("uv".to_string()), version: Some(version().to_string()), subcommand, }), python: markers.map(|markers| markers.python_full_version().version.to_string()), implementation: Option::from(Implementation { name: markers.map(|markers| markers.platform_python_implementation().to_string()), version: markers.map(|markers| markers.python_full_version().version.to_string()), }), distro, system: Option::from(System { name: markers.map(|markers| markers.platform_system().to_string()), release: markers.map(|markers| markers.platform_release().to_string()), }), cpu: markers.map(|markers| markers.platform_machine().to_string()), // Should probably always be None in uv. openssl_version: None, // Should probably always be None in uv. setuptools_version: None, // Calling rustc --version is likely too slow. rustc_version: None, ci: looks_like_ci, } } } uv-0.9.17+ds1/crates/uv-client/src/middleware.rs000066400000000000000000000025041520155276700214010ustar00rootroot00000000000000use http::Extensions; use std::fmt::Debug; use uv_redacted::DisplaySafeUrl; use reqwest::{Request, Response}; use reqwest_middleware::{Middleware, Next}; /// A custom error type for the offline middleware. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct OfflineError { url: DisplaySafeUrl, } impl OfflineError { /// Returns the URL that caused the error. pub(crate) fn url(&self) -> &DisplaySafeUrl { &self.url } } impl std::fmt::Display for OfflineError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Network connectivity is disabled, but the requested data wasn't found in the cache for: `{}`", self.url ) } } impl std::error::Error for OfflineError {} /// A middleware that always returns an error indicating that the client is offline. pub(crate) struct OfflineMiddleware; #[async_trait::async_trait] impl Middleware for OfflineMiddleware { async fn handle( &self, req: Request, _extensions: &mut Extensions, _next: Next<'_>, ) -> reqwest_middleware::Result { Err(reqwest_middleware::Error::Middleware( OfflineError { url: DisplaySafeUrl::from_url(req.url().clone()), } .into(), )) } } uv-0.9.17+ds1/crates/uv-client/src/registry_client.rs000066400000000000000000002170111520155276700224730ustar00rootroot00000000000000use std::collections::BTreeMap; use std::fmt::Debug; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use async_http_range_reader::AsyncHttpRangeReader; use futures::{FutureExt, StreamExt, TryStreamExt}; use http::{HeaderMap, StatusCode}; use itertools::Either; use reqwest::{Proxy, Response}; use rustc_hash::FxHashMap; use tokio::sync::{Mutex, Semaphore}; use tracing::{Instrument, debug, info_span, instrument, trace, warn}; use url::Url; use uv_auth::{CredentialsCache, Indexes, PyxTokenStore}; use uv_cache::{Cache, CacheBucket, CacheEntry, WheelCache}; use uv_configuration::IndexStrategy; use uv_configuration::KeyringProviderType; use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use uv_distribution_types::{ BuiltDist, File, IndexCapabilities, IndexFormat, IndexLocations, IndexMetadataRef, IndexStatusCodeDecision, IndexStatusCodeStrategy, IndexUrl, IndexUrls, Name, }; use uv_metadata::{read_metadata_async_seek, read_metadata_async_stream}; use uv_normalize::PackageName; use uv_pep440::Version; use uv_pep508::MarkerEnvironment; use uv_platform_tags::Platform; use uv_pypi_types::{ PypiSimpleDetail, PypiSimpleIndex, PyxSimpleDetail, PyxSimpleIndex, ResolutionMetadata, }; use uv_redacted::DisplaySafeUrl; use uv_small_str::SmallString; use uv_torch::TorchStrategy; use crate::base_client::{BaseClientBuilder, ExtraMiddleware, RedirectPolicy}; use crate::cached_client::CacheControl; use crate::flat_index::FlatIndexEntry; use crate::html::SimpleDetailHTML; use crate::remote_metadata::wheel_metadata_from_remote_zip; use crate::rkyvutil::OwnedArchive; use crate::{ BaseClient, CachedClient, Error, ErrorKind, FlatIndexClient, FlatIndexEntries, RedirectClientWithMiddleware, }; /// A builder for an [`RegistryClient`]. #[derive(Debug, Clone)] pub struct RegistryClientBuilder<'a> { index_locations: IndexLocations, index_strategy: IndexStrategy, torch_backend: Option, cache: Cache, base_client_builder: BaseClientBuilder<'a>, } impl<'a> RegistryClientBuilder<'a> { pub fn new(base_client_builder: BaseClientBuilder<'a>, cache: Cache) -> Self { Self { index_locations: IndexLocations::default(), index_strategy: IndexStrategy::default(), torch_backend: None, cache, base_client_builder, } } #[must_use] pub fn with_reqwest_client(mut self, client: reqwest::Client) -> Self { self.base_client_builder = self.base_client_builder.custom_client(client); self } #[must_use] pub fn index_locations(mut self, index_locations: IndexLocations) -> Self { self.index_locations = index_locations; self } #[must_use] pub fn index_strategy(mut self, index_strategy: IndexStrategy) -> Self { self.index_strategy = index_strategy; self } #[must_use] pub fn torch_backend(mut self, torch_backend: Option) -> Self { self.torch_backend = torch_backend; self } #[must_use] pub fn keyring(mut self, keyring_type: KeyringProviderType) -> Self { self.base_client_builder = self.base_client_builder.keyring(keyring_type); self } #[must_use] pub fn built_in_root_certs(mut self, built_in_root_certs: bool) -> Self { self.base_client_builder = self .base_client_builder .built_in_root_certs(built_in_root_certs); self } #[must_use] pub fn cache(mut self, cache: Cache) -> Self { self.cache = cache; self } #[must_use] pub fn extra_middleware(mut self, middleware: ExtraMiddleware) -> Self { self.base_client_builder = self.base_client_builder.extra_middleware(middleware); self } #[must_use] pub fn markers(mut self, markers: &'a MarkerEnvironment) -> Self { self.base_client_builder = self.base_client_builder.markers(markers); self } #[must_use] pub fn platform(mut self, platform: &'a Platform) -> Self { self.base_client_builder = self.base_client_builder.platform(platform); self } #[must_use] pub fn proxy(mut self, proxy: Proxy) -> Self { self.base_client_builder = self.base_client_builder.proxy(proxy); self } /// Allows credentials to be propagated on cross-origin redirects. /// /// WARNING: This should only be available for tests. In production code, propagating credentials /// during cross-origin redirects can lead to security vulnerabilities including credential /// leakage to untrusted domains. #[cfg(test)] #[must_use] pub fn allow_cross_origin_credentials(mut self) -> Self { self.base_client_builder = self.base_client_builder.allow_cross_origin_credentials(); self } /// Add all authenticated sources to the cache. pub fn cache_index_credentials(&mut self) { for index in self.index_locations.known_indexes() { if let Some(credentials) = index.credentials() { trace!( "Read credentials for index {}", index .name .as_ref() .map(ToString::to_string) .unwrap_or_else(|| index.url.to_string()) ); if let Some(root_url) = index.root_url() { self.base_client_builder .store_credentials(&root_url, credentials.clone()); } self.base_client_builder .store_credentials(index.raw_url(), credentials); } } } pub fn build(mut self) -> RegistryClient { self.cache_index_credentials(); let index_urls = self.index_locations.index_urls(); // Build a base client let builder = self .base_client_builder .indexes(Indexes::from(&self.index_locations)) .redirect(RedirectPolicy::RetriggerMiddleware); let client = builder.build(); let timeout = client.timeout(); let connectivity = client.connectivity(); // Wrap in the cache middleware. let client = CachedClient::new(client); RegistryClient { index_urls, index_strategy: self.index_strategy, torch_backend: self.torch_backend, cache: self.cache, connectivity, client, timeout, flat_indexes: Arc::default(), pyx_token_store: PyxTokenStore::from_settings().ok(), } } /// Share the underlying client between two different middleware configurations. pub fn wrap_existing(mut self, existing: &BaseClient) -> RegistryClient { self.cache_index_credentials(); let index_urls = self.index_locations.index_urls(); // Wrap in any relevant middleware and handle connectivity. let client = self .base_client_builder .indexes(Indexes::from(&self.index_locations)) .wrap_existing(existing); let timeout = client.timeout(); let connectivity = client.connectivity(); // Wrap in the cache middleware. let client = CachedClient::new(client); RegistryClient { index_urls, index_strategy: self.index_strategy, torch_backend: self.torch_backend, cache: self.cache, connectivity, client, timeout, flat_indexes: Arc::default(), pyx_token_store: PyxTokenStore::from_settings().ok(), } } } /// A client for fetching packages from a `PyPI`-compatible index. #[derive(Debug, Clone)] pub struct RegistryClient { /// The index URLs to use for fetching packages. index_urls: IndexUrls, /// The strategy to use when fetching across multiple indexes. index_strategy: IndexStrategy, /// The strategy to use when selecting a PyTorch backend, if any. torch_backend: Option, /// The underlying HTTP client. client: CachedClient, /// Used for the remote wheel METADATA cache. cache: Cache, /// The connectivity mode to use. connectivity: Connectivity, /// Configured client timeout, in seconds. timeout: Duration, /// The flat index entries for each `--find-links`-style index URL. flat_indexes: Arc>, /// The pyx token store to use for persistent credentials. // TODO(charlie): The token store is only needed for `is_known_url`; can we avoid storing it here? pyx_token_store: Option, } /// The format of the package metadata returned by querying an index. #[derive(Debug)] pub enum MetadataFormat { /// The metadata adheres to the Simple Repository API format. Simple(OwnedArchive), /// The metadata consists of a list of distributions from a "flat" index. Flat(Vec), } impl RegistryClient { /// Return the [`CachedClient`] used by this client. pub fn cached_client(&self) -> &CachedClient { &self.client } /// Return the [`BaseClient`] used by this client. pub fn uncached_client(&self, url: &DisplaySafeUrl) -> &RedirectClientWithMiddleware { self.client.uncached().for_host(url) } /// Returns `true` if SSL verification is disabled for the given URL. pub fn disable_ssl(&self, url: &DisplaySafeUrl) -> bool { self.client.uncached().disable_ssl(url) } /// Return the [`Connectivity`] mode used by this client. pub fn connectivity(&self) -> Connectivity { self.connectivity } /// Return the timeout this client is configured with, in seconds. pub fn timeout(&self) -> Duration { self.timeout } pub fn credentials_cache(&self) -> &CredentialsCache { self.client.uncached().credentials_cache() } /// Return the appropriate index URLs for the given [`PackageName`]. fn index_urls_for( &self, package_name: &PackageName, ) -> impl Iterator> { self.torch_backend .as_ref() .and_then(|torch_backend| { torch_backend .applies_to(package_name) .then(|| torch_backend.index_urls()) .map(|indexes| indexes.map(IndexMetadataRef::from)) }) .map(Either::Left) .unwrap_or_else(|| Either::Right(self.index_urls.indexes().map(IndexMetadataRef::from))) } /// Return the appropriate [`IndexStrategy`] for the given [`PackageName`]. fn index_strategy_for(&self, package_name: &PackageName) -> IndexStrategy { self.torch_backend .as_ref() .and_then(|torch_backend| { torch_backend .applies_to(package_name) .then_some(IndexStrategy::UnsafeFirstMatch) }) .unwrap_or(self.index_strategy) } /// Fetch package metadata from an index. /// /// Supports both the "Simple" API and `--find-links`-style flat indexes. /// /// "Simple" here refers to [PEP 503 – Simple Repository API](https://peps.python.org/pep-0503/) /// and [PEP 691 – JSON-based Simple API for Python Package Indexes](https://peps.python.org/pep-0691/), /// which the PyPI JSON API implements. #[instrument(skip_all, fields(package = % package_name))] pub async fn simple_detail<'index>( &'index self, package_name: &PackageName, index: Option>, capabilities: &IndexCapabilities, download_concurrency: &Semaphore, ) -> Result, Error> { // If `--no-index` is specified, avoid fetching regardless of whether the index is implicit, // explicit, etc. if self.index_urls.no_index() { return Err(ErrorKind::NoIndex(package_name.to_string()).into()); } let indexes = if let Some(index) = index { Either::Left(std::iter::once(index)) } else { Either::Right(self.index_urls_for(package_name)) }; let mut results = Vec::new(); match self.index_strategy_for(package_name) { // If we're searching for the first index that contains the package, fetch serially. IndexStrategy::FirstIndex => { for index in indexes { let _permit = download_concurrency.acquire().await; match index.format { IndexFormat::Simple => { let status_code_strategy = self.index_urls.status_code_strategy_for(index.url); match self .simple_detail_single_index( package_name, index.url, capabilities, &status_code_strategy, ) .await? { SimpleMetadataSearchOutcome::Found(metadata) => { results.push((index.url, MetadataFormat::Simple(metadata))); break; } // Package not found, so we will continue on to the next index (if there is one) SimpleMetadataSearchOutcome::NotFound => {} // The search failed because of an HTTP status code that we don't ignore for // this index. We end our search here. SimpleMetadataSearchOutcome::StatusCodeFailure(status_code) => { debug!( "Indexes search failed because of status code failure: {status_code}" ); break; } } } IndexFormat::Flat => { let entries = self.flat_single_index(package_name, index.url).await?; if !entries.is_empty() { results.push((index.url, MetadataFormat::Flat(entries))); break; } } } } } // Otherwise, fetch concurrently. IndexStrategy::UnsafeBestMatch | IndexStrategy::UnsafeFirstMatch => { results = futures::stream::iter(indexes) .map(async |index| { let _permit = download_concurrency.acquire().await; match index.format { IndexFormat::Simple => { // For unsafe matches, ignore authentication failures. let status_code_strategy = IndexStatusCodeStrategy::ignore_authentication_error_codes(); let metadata = match self .simple_detail_single_index( package_name, index.url, capabilities, &status_code_strategy, ) .await? { SimpleMetadataSearchOutcome::Found(metadata) => Some(metadata), _ => None, }; Ok((index.url, metadata.map(MetadataFormat::Simple))) } IndexFormat::Flat => { let entries = self.flat_single_index(package_name, index.url).await?; Ok((index.url, Some(MetadataFormat::Flat(entries)))) } } }) .buffered(8) .filter_map(async |result: Result<_, Error>| match result { Ok((index, Some(metadata))) => Some(Ok((index, metadata))), Ok((_, None)) => None, Err(err) => Some(Err(err)), }) .try_collect::>() .await?; } } if results.is_empty() { return match self.connectivity { Connectivity::Online => { Err(ErrorKind::RemotePackageNotFound(package_name.clone()).into()) } Connectivity::Offline => Err(ErrorKind::Offline(package_name.to_string()).into()), }; } Ok(results) } /// Fetch the [`FlatIndexEntry`] entries for a given package from a single `--find-links` index. async fn flat_single_index( &self, package_name: &PackageName, index: &IndexUrl, ) -> Result, Error> { // Store the flat index entries in a cache, to avoid redundant fetches. A flat index will // typically contain entries for multiple packages; as such, it's more efficient to cache // the entire index rather than re-fetching it for each package. let mut cache = self.flat_indexes.lock().await; if let Some(entries) = cache.get(index) { return Ok(entries.get(package_name).cloned().unwrap_or_default()); } let client = FlatIndexClient::new(self.cached_client(), self.connectivity, &self.cache); // Fetch the entries for the index. let FlatIndexEntries { entries, .. } = client.fetch_index(index).await.map_err(ErrorKind::Flat)?; // Index by package name. let mut entries_by_package: FxHashMap> = FxHashMap::default(); for entry in entries { entries_by_package .entry(entry.filename.name().clone()) .or_default() .push(entry); } let package_entries = entries_by_package .get(package_name) .cloned() .unwrap_or_default(); // Write to the cache. cache.insert(index.clone(), entries_by_package); Ok(package_entries) } /// Fetch the [`SimpleDetailMetadata`] from a single index for a given package. /// /// The index can either be a PEP 503-compatible remote repository, or a local directory laid /// out in the same format. async fn simple_detail_single_index( &self, package_name: &PackageName, index: &IndexUrl, capabilities: &IndexCapabilities, status_code_strategy: &IndexStatusCodeStrategy, ) -> Result { // Format the URL for PyPI. let mut url = index.url().clone(); url.path_segments_mut() .map_err(|()| ErrorKind::CannotBeABase(index.url().clone()))? .pop_if_empty() .push(package_name.as_ref()) // The URL *must* end in a trailing slash for proper relative path behavior // ref https://github.com/servo/rust-url/issues/333 .push(""); trace!("Fetching metadata for {package_name} from {url}"); let cache_entry = self.cache.entry( CacheBucket::Simple, WheelCache::Index(index).root(), format!("{package_name}.rkyv"), ); let cache_control = match self.connectivity { Connectivity::Online => { if let Some(header) = self.index_urls.simple_api_cache_control_for(index) { CacheControl::Override(header) } else { CacheControl::from( self.cache .freshness(&cache_entry, Some(package_name), None) .map_err(ErrorKind::Io)?, ) } } Connectivity::Offline => CacheControl::AllowStale, }; // Acquire an advisory lock, to guard against concurrent writes. #[cfg(windows)] let _lock = { let lock_entry = cache_entry.with_file(format!("{package_name}.lock")); lock_entry.lock().await.map_err(ErrorKind::CacheLock)? }; let result = if matches!(index, IndexUrl::Path(_)) { self.fetch_local_simple_detail(package_name, &url).await } else { self.fetch_remote_simple_detail(package_name, &url, index, &cache_entry, cache_control) .await }; match result { Ok(metadata) => Ok(SimpleMetadataSearchOutcome::Found(metadata)), Err(err) => match err.kind() { // The package could not be found in the remote index. ErrorKind::WrappedReqwestError(.., reqwest_err) => { let Some(status_code) = reqwest_err.status() else { return Err(err); }; let decision = status_code_strategy.handle_status_code(status_code, index, capabilities); if let IndexStatusCodeDecision::Fail(status_code) = decision { if !matches!( status_code, StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN ) { return Err(err); } } Ok(SimpleMetadataSearchOutcome::from(decision)) } // The package is unavailable due to a lack of connectivity. ErrorKind::Offline(_) => Ok(SimpleMetadataSearchOutcome::NotFound), // The package could not be found in the local index. ErrorKind::LocalPackageNotFound(_) => Ok(SimpleMetadataSearchOutcome::NotFound), _ => Err(err), }, } } /// Fetch the [`SimpleDetailMetadata`] from a remote URL, using the PEP 503 Simple Repository API. async fn fetch_remote_simple_detail( &self, package_name: &PackageName, url: &DisplaySafeUrl, index: &IndexUrl, cache_entry: &CacheEntry, cache_control: CacheControl<'_>, ) -> Result, Error> { // In theory, we should be able to pass `MediaType::all()` to all registries, and as // unsupported media types should be ignored by the server. For now, we implement this // defensively to avoid issues with misconfigured servers. let accept = if self .pyx_token_store .as_ref() .is_some_and(|token_store| token_store.is_known_url(index.url())) { MediaType::all() } else { MediaType::pypi() }; let simple_request = self .uncached_client(url) .get(Url::from(url.clone())) .header("Accept-Encoding", "gzip, deflate, zstd") .header("Accept", accept) .build() .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let parse_simple_response = |response: Response| { async { // Use the response URL, rather than the request URL, as the base for relative URLs. // This ensures that we handle redirects and other URL transformations correctly. let url = DisplaySafeUrl::from_url(response.url().clone()); let content_type = response .headers() .get("content-type") .ok_or_else(|| Error::from(ErrorKind::MissingContentType(url.clone())))?; let content_type = content_type.to_str().map_err(|err| { Error::from(ErrorKind::InvalidContentTypeHeader(url.clone(), err)) })?; let media_type = content_type.split(';').next().unwrap_or(content_type); let media_type = MediaType::from_str(media_type).ok_or_else(|| { Error::from(ErrorKind::UnsupportedMediaType( url.clone(), media_type.to_string(), )) })?; let unarchived = match media_type { MediaType::PyxV1Msgpack => { let bytes = response .bytes() .await .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let data: PyxSimpleDetail = rmp_serde::from_slice(bytes.as_ref()) .map_err(|err| Error::from_msgpack_err(err, url.clone()))?; SimpleDetailMetadata::from_pyx_files( data.files, data.core_metadata, package_name, &url, ) } MediaType::PyxV1Json => { let bytes = response .bytes() .await .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let data: PyxSimpleDetail = serde_json::from_slice(bytes.as_ref()) .map_err(|err| Error::from_json_err(err, url.clone()))?; SimpleDetailMetadata::from_pyx_files( data.files, data.core_metadata, package_name, &url, ) } MediaType::PypiV1Json => { let bytes = response .bytes() .await .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let data: PypiSimpleDetail = serde_json::from_slice(bytes.as_ref()) .map_err(|err| Error::from_json_err(err, url.clone()))?; SimpleDetailMetadata::from_pypi_files(data.files, package_name, &url) } MediaType::PypiV1Html | MediaType::TextHtml => { let text = response .text() .await .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; SimpleDetailMetadata::from_html(&text, package_name, &url)? } }; OwnedArchive::from_unarchived(&unarchived) } .boxed_local() .instrument(info_span!("parse_simple_api", package = %package_name)) }; let simple = self .cached_client() .get_cacheable_with_retry( simple_request, cache_entry, cache_control, parse_simple_response, ) .await?; Ok(simple) } /// Fetch the [`SimpleDetailMetadata`] from a local file, using a PEP 503-compatible directory /// structure. async fn fetch_local_simple_detail( &self, package_name: &PackageName, url: &DisplaySafeUrl, ) -> Result, Error> { let path = url .to_file_path() .map_err(|()| ErrorKind::NonFileUrl(url.clone()))? .join("index.html"); let text = match fs_err::tokio::read_to_string(&path).await { Ok(text) => text, Err(err) if err.kind() == std::io::ErrorKind::NotFound => { return Err(Error::from(ErrorKind::LocalPackageNotFound( package_name.clone(), ))); } Err(err) => { return Err(Error::from(ErrorKind::Io(err))); } }; let metadata = SimpleDetailMetadata::from_html(&text, package_name, url)?; OwnedArchive::from_unarchived(&metadata) } /// Fetch the list of projects from a Simple API index at a remote URL. /// /// This fetches the root of a Simple API index (e.g., `https://pypi.org/simple/`) /// which returns a list of all available projects. pub async fn fetch_simple_index( &self, index_url: &IndexUrl, ) -> Result { // Format the URL for PyPI. let mut url = index_url.url().clone(); url.path_segments_mut() .map_err(|()| ErrorKind::CannotBeABase(index_url.url().clone()))? .pop_if_empty() // The URL *must* end in a trailing slash for proper relative path behavior // ref https://github.com/servo/rust-url/issues/333 .push(""); if url.scheme() == "file" { let archived = self.fetch_local_simple_index(&url).await?; Ok(OwnedArchive::deserialize(&archived)) } else { let archived = self.fetch_remote_simple_index(&url, index_url).await?; Ok(OwnedArchive::deserialize(&archived)) } } /// Fetch the list of projects from a remote Simple API index. async fn fetch_remote_simple_index( &self, url: &DisplaySafeUrl, index: &IndexUrl, ) -> Result, Error> { // In theory, we should be able to pass `MediaType::all()` to all registries, and as // unsupported media types should be ignored by the server. For now, we implement this // defensively to avoid issues with misconfigured servers. let accept = if self .pyx_token_store .as_ref() .is_some_and(|token_store| token_store.is_known_url(index.url())) { MediaType::all() } else { MediaType::pypi() }; let cache_entry = self.cache.entry( CacheBucket::Simple, WheelCache::Index(index).root(), "index.html.rkyv", ); let cache_control = match self.connectivity { Connectivity::Online => { if let Some(header) = self.index_urls.simple_api_cache_control_for(index) { CacheControl::Override(header) } else { CacheControl::from( self.cache .freshness(&cache_entry, None, None) .map_err(ErrorKind::Io)?, ) } } Connectivity::Offline => CacheControl::AllowStale, }; let parse_simple_response = |response: Response| { async { // Use the response URL, rather than the request URL, as the base for relative URLs. // This ensures that we handle redirects and other URL transformations correctly. let url = DisplaySafeUrl::from_url(response.url().clone()); let content_type = response .headers() .get("content-type") .ok_or_else(|| Error::from(ErrorKind::MissingContentType(url.clone())))?; let content_type = content_type.to_str().map_err(|err| { Error::from(ErrorKind::InvalidContentTypeHeader(url.clone(), err)) })?; let media_type = content_type.split(';').next().unwrap_or(content_type); let media_type = MediaType::from_str(media_type).ok_or_else(|| { Error::from(ErrorKind::UnsupportedMediaType( url.clone(), media_type.to_string(), )) })?; let metadata = match media_type { MediaType::PyxV1Msgpack => { let bytes = response .bytes() .await .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let data: PyxSimpleIndex = rmp_serde::from_slice(bytes.as_ref()) .map_err(|err| Error::from_msgpack_err(err, url.clone()))?; SimpleIndexMetadata::from_pyx_index(data) } MediaType::PyxV1Json => { let bytes = response .bytes() .await .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let data: PyxSimpleIndex = serde_json::from_slice(bytes.as_ref()) .map_err(|err| Error::from_json_err(err, url.clone()))?; SimpleIndexMetadata::from_pyx_index(data) } MediaType::PypiV1Json => { let bytes = response .bytes() .await .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let data: PypiSimpleIndex = serde_json::from_slice(bytes.as_ref()) .map_err(|err| Error::from_json_err(err, url.clone()))?; SimpleIndexMetadata::from_pypi_index(data) } MediaType::PypiV1Html | MediaType::TextHtml => { let text = response .text() .await .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; SimpleIndexMetadata::from_html(&text, &url)? } }; OwnedArchive::from_unarchived(&metadata) } }; let simple_request = self .uncached_client(url) .get(Url::from(url.clone())) .header("Accept-Encoding", "gzip, deflate, zstd") .header("Accept", accept) .build() .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; let index = self .cached_client() .get_cacheable_with_retry( simple_request, &cache_entry, cache_control, parse_simple_response, ) .await?; Ok(index) } /// Fetch the list of projects from a local Simple API index. async fn fetch_local_simple_index( &self, url: &DisplaySafeUrl, ) -> Result, Error> { let path = url .to_file_path() .map_err(|()| ErrorKind::NonFileUrl(url.clone()))? .join("index.html"); let text = match fs_err::tokio::read_to_string(&path).await { Ok(text) => text, Err(err) if err.kind() == std::io::ErrorKind::NotFound => { return Err(Error::from(ErrorKind::LocalIndexNotFound(path))); } Err(err) => { return Err(Error::from(ErrorKind::Io(err))); } }; let metadata = SimpleIndexMetadata::from_html(&text, url)?; OwnedArchive::from_unarchived(&metadata) } /// Fetch the metadata for a remote wheel file. /// /// For a remote wheel, we try the following ways to fetch the metadata: /// 1. From a [PEP 658](https://peps.python.org/pep-0658/) data-dist-info-metadata url /// 2. From a remote wheel by partial zip reading /// 3. From a (temp) download of a remote wheel (this is a fallback, the webserver should support range requests) #[instrument(skip_all, fields(% built_dist))] pub async fn wheel_metadata( &self, built_dist: &BuiltDist, capabilities: &IndexCapabilities, ) -> Result { let metadata = match &built_dist { BuiltDist::Registry(wheels) => { #[derive(Debug, Clone)] enum WheelLocation { /// A local file path. Path(PathBuf), /// A remote URL. Url(DisplaySafeUrl), } let wheel = wheels.best_wheel(); let url = wheel.file.url.to_url().map_err(ErrorKind::InvalidUrl)?; let location = if url.scheme() == "file" { let path = url .to_file_path() .map_err(|()| ErrorKind::NonFileUrl(url.clone()))?; WheelLocation::Path(path) } else { WheelLocation::Url(url) }; match location { WheelLocation::Path(path) => { let file = fs_err::tokio::File::open(&path) .await .map_err(ErrorKind::Io)?; let reader = tokio::io::BufReader::new(file); let contents = read_metadata_async_seek(&wheel.filename, reader) .await .map_err(|err| { ErrorKind::Metadata(path.to_string_lossy().to_string(), err) })?; ResolutionMetadata::parse_metadata(&contents).map_err(|err| { ErrorKind::MetadataParseError( wheel.filename.clone(), built_dist.to_string(), Box::new(err), ) })? } WheelLocation::Url(url) => { self.wheel_metadata_registry(&wheel.index, &wheel.file, &url, capabilities) .await? } } } BuiltDist::DirectUrl(wheel) => { self.wheel_metadata_no_pep658( &wheel.filename, &wheel.url, None, WheelCache::Url(&wheel.url), capabilities, ) .await? } BuiltDist::Path(wheel) => { let file = fs_err::tokio::File::open(wheel.install_path.as_ref()) .await .map_err(ErrorKind::Io)?; let reader = tokio::io::BufReader::new(file); let contents = read_metadata_async_seek(&wheel.filename, reader) .await .map_err(|err| { ErrorKind::Metadata(wheel.install_path.to_string_lossy().to_string(), err) })?; ResolutionMetadata::parse_metadata(&contents).map_err(|err| { ErrorKind::MetadataParseError( wheel.filename.clone(), built_dist.to_string(), Box::new(err), ) })? } }; if metadata.name != *built_dist.name() { return Err(Error::from(ErrorKind::NameMismatch { metadata: metadata.name, given: built_dist.name().clone(), })); } Ok(metadata) } /// Fetch the metadata from a wheel file. async fn wheel_metadata_registry( &self, index: &IndexUrl, file: &File, url: &DisplaySafeUrl, capabilities: &IndexCapabilities, ) -> Result { // If the metadata file is available at its own url (PEP 658), download it from there. let filename = WheelFilename::from_str(&file.filename).map_err(ErrorKind::WheelFilename)?; if file.dist_info_metadata { let mut url = url.clone(); let path = format!("{}.metadata", url.path()); url.set_path(&path); let cache_entry = self.cache.entry( CacheBucket::Wheels, WheelCache::Index(index).wheel_dir(filename.name.as_ref()), format!("{}.msgpack", filename.cache_key()), ); let cache_control = match self.connectivity { Connectivity::Online => { if let Some(header) = self.index_urls.artifact_cache_control_for(index) { CacheControl::Override(header) } else { CacheControl::from( self.cache .freshness(&cache_entry, Some(&filename.name), None) .map_err(ErrorKind::Io)?, ) } } Connectivity::Offline => CacheControl::AllowStale, }; // Acquire an advisory lock, to guard against concurrent writes. #[cfg(windows)] let _lock = { let lock_entry = cache_entry.with_file(format!("{}.lock", filename.stem())); lock_entry.lock().await.map_err(ErrorKind::CacheLock)? }; let response_callback = async |response: Response| { let bytes = response .bytes() .await .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; info_span!("parse_metadata21") .in_scope(|| ResolutionMetadata::parse_metadata(bytes.as_ref())) .map_err(|err| { Error::from(ErrorKind::MetadataParseError( filename.clone(), url.to_string(), Box::new(err), )) }) }; let req = self .uncached_client(&url) .get(Url::from(url.clone())) .build() .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; Ok(self .cached_client() .get_serde_with_retry(req, &cache_entry, cache_control, response_callback) .await?) } else { // If we lack PEP 658 support, try using HTTP range requests to read only the // `.dist-info/METADATA` file from the zip, and if that also fails, download the whole wheel // into the cache and read from there self.wheel_metadata_no_pep658( &filename, url, Some(index), WheelCache::Index(index), capabilities, ) .await } } /// Get the wheel metadata if it isn't available in an index through PEP 658 async fn wheel_metadata_no_pep658<'data>( &self, filename: &'data WheelFilename, url: &'data DisplaySafeUrl, index: Option<&'data IndexUrl>, cache_shard: WheelCache<'data>, capabilities: &'data IndexCapabilities, ) -> Result { let cache_entry = self.cache.entry( CacheBucket::Wheels, cache_shard.wheel_dir(filename.name.as_ref()), format!("{}.msgpack", filename.cache_key()), ); let cache_control = match self.connectivity { Connectivity::Online => { if let Some(index) = index { if let Some(header) = self.index_urls.artifact_cache_control_for(index) { CacheControl::Override(header) } else { CacheControl::from( self.cache .freshness(&cache_entry, Some(&filename.name), None) .map_err(ErrorKind::Io)?, ) } } else { CacheControl::from( self.cache .freshness(&cache_entry, Some(&filename.name), None) .map_err(ErrorKind::Io)?, ) } } Connectivity::Offline => CacheControl::AllowStale, }; // Acquire an advisory lock, to guard against concurrent writes. #[cfg(windows)] let _lock = { let lock_entry = cache_entry.with_file(format!("{}.lock", filename.stem())); lock_entry.lock().await.map_err(ErrorKind::CacheLock)? }; // Attempt to fetch via a range request. if index.is_none_or(|index| capabilities.supports_range_requests(index)) { let req = self .uncached_client(url) .head(Url::from(url.clone())) .header( "accept-encoding", http::HeaderValue::from_static("identity"), ) .build() .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; // Copy authorization headers from the HEAD request to subsequent requests let mut headers = HeaderMap::default(); if let Some(authorization) = req.headers().get("authorization") { headers.append("authorization", authorization.clone()); } // This response callback is special, we actually make a number of subsequent requests to // fetch the file from the remote zip. let read_metadata_range_request = |response: Response| { async { let mut reader = AsyncHttpRangeReader::from_head_response( self.uncached_client(url).clone(), response, Url::from(url.clone()), headers.clone(), ) .await .map_err(|err| ErrorKind::AsyncHttpRangeReader(url.clone(), err))?; trace!("Getting metadata for {filename} by range request"); let text = wheel_metadata_from_remote_zip(filename, url, &mut reader).await?; ResolutionMetadata::parse_metadata(text.as_bytes()).map_err(|err| { Error::from(ErrorKind::MetadataParseError( filename.clone(), url.to_string(), Box::new(err), )) }) } .boxed_local() .instrument(info_span!("read_metadata_range_request", wheel = %filename)) }; let result = self .cached_client() .get_serde_with_retry( req, &cache_entry, cache_control, read_metadata_range_request, ) .await .map_err(crate::Error::from); match result { Ok(metadata) => return Ok(metadata), Err(err) => { if err.is_http_range_requests_unsupported() { // The range request version failed. Fall back to streaming the file to search // for the METADATA file. warn!("Range requests not supported for {filename}; streaming wheel"); // Mark the index as not supporting range requests. if let Some(index) = index { capabilities.set_no_range_requests(index.clone()); } } else { return Err(err); } } } } // Create a request to stream the file. let req = self .uncached_client(url) .get(Url::from(url.clone())) .header( // `reqwest` defaults to accepting compressed responses. // Specify identity encoding to get consistent .whl downloading // behavior from servers. ref: https://github.com/pypa/pip/pull/1688 "accept-encoding", reqwest::header::HeaderValue::from_static("identity"), ) .build() .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; // Stream the file, searching for the METADATA. let read_metadata_stream = |response: Response| { async { let reader = response .bytes_stream() .map_err(|err| self.handle_response_errors(err)) .into_async_read(); read_metadata_async_stream(filename, url.as_ref(), reader) .await .map_err(|err| ErrorKind::Metadata(url.to_string(), err)) } .instrument(info_span!("read_metadata_stream", wheel = %filename)) }; self.cached_client() .get_serde_with_retry(req, &cache_entry, cache_control, read_metadata_stream) .await .map_err(crate::Error::from) } /// Handle a specific `reqwest` error, and convert it to [`io::Error`]. fn handle_response_errors(&self, err: reqwest::Error) -> std::io::Error { if err.is_timeout() { std::io::Error::new( std::io::ErrorKind::TimedOut, format!( "Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: {}s).", self.timeout().as_secs() ), ) } else { std::io::Error::other(err) } } } #[derive(Debug)] pub(crate) enum SimpleMetadataSearchOutcome { /// Simple metadata was found Found(OwnedArchive), /// Simple metadata was not found NotFound, /// A status code failure was encountered when searching for /// simple metadata and our strategy did not ignore it StatusCodeFailure(StatusCode), } impl From for SimpleMetadataSearchOutcome { fn from(item: IndexStatusCodeDecision) -> Self { match item { IndexStatusCodeDecision::Ignore => Self::NotFound, IndexStatusCodeDecision::Fail(status_code) => Self::StatusCodeFailure(status_code), } } } /// A map from [`IndexUrl`] to [`FlatIndexEntry`] entries found at the given URL, indexed by /// [`PackageName`]. #[derive(Default, Debug, Clone)] struct FlatIndexCache(FxHashMap>>); impl FlatIndexCache { /// Get the entries for a given index URL. fn get(&self, index: &IndexUrl) -> Option<&FxHashMap>> { self.0.get(index) } /// Insert the entries for a given index URL. fn insert( &mut self, index: IndexUrl, entries: FxHashMap>, ) -> Option>> { self.0.insert(index, entries) } } #[derive(Default, Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] pub struct VersionFiles { pub wheels: Vec, pub source_dists: Vec, } impl VersionFiles { fn push(&mut self, filename: DistFilename, file: File) { match filename { DistFilename::WheelFilename(name) => self.wheels.push(VersionWheel { name, file }), DistFilename::SourceDistFilename(name) => { self.source_dists.push(VersionSourceDist { name, file }); } } } pub fn all(self) -> impl Iterator { self.source_dists .into_iter() .map(|VersionSourceDist { name, file }| (DistFilename::SourceDistFilename(name), file)) .chain( self.wheels .into_iter() .map(|VersionWheel { name, file }| (DistFilename::WheelFilename(name), file)), ) } } #[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] pub struct VersionWheel { pub name: WheelFilename, pub file: File, } #[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] pub struct VersionSourceDist { pub name: SourceDistFilename, pub file: File, } /// The list of projects available in a Simple API index. #[derive(Default, Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] pub struct SimpleIndexMetadata { /// The list of project names available in the index. projects: Vec, } impl SimpleIndexMetadata { /// Iterate over the projects in the index. pub fn iter(&self) -> impl Iterator { self.projects.iter() } /// Create a [`SimpleIndexMetadata`] from a [`PypiSimpleIndex`]. fn from_pypi_index(index: PypiSimpleIndex) -> Self { Self { projects: index.projects.into_iter().map(|entry| entry.name).collect(), } } /// Create a [`SimpleIndexMetadata`] from a [`PyxSimpleIndex`]. fn from_pyx_index(index: PyxSimpleIndex) -> Self { Self { projects: index.projects.into_iter().map(|entry| entry.name).collect(), } } /// Create a [`SimpleIndexMetadata`] from HTML content. fn from_html(text: &str, url: &DisplaySafeUrl) -> Result { let html = crate::html::SimpleIndexHtml::parse(text).map_err(|err| { Error::from(ErrorKind::BadHtml { source: err, url: url.clone(), }) })?; Ok(Self { projects: html.projects, }) } } #[derive(Default, Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] pub struct SimpleDetailMetadata(Vec); #[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] pub struct SimpleDetailMetadatum { pub version: Version, pub files: VersionFiles, pub metadata: Option, } impl SimpleDetailMetadata { pub fn iter(&self) -> impl DoubleEndedIterator { self.0.iter() } fn from_pypi_files( files: Vec, package_name: &PackageName, base: &Url, ) -> Self { let mut version_map: BTreeMap = BTreeMap::default(); // Convert to a reference-counted string. let base = SmallString::from(base.as_str()); // Group the distributions by version and kind for file in files { let Some(filename) = DistFilename::try_from_filename(&file.filename, package_name) else { warn!("Skipping file for {package_name}: {}", file.filename); continue; }; let file = match File::try_from_pypi(file, &base) { Ok(file) => file, Err(err) => { // Ignore files with unparsable version specifiers. warn!("Skipping file for {package_name}: {err}"); continue; } }; match version_map.entry(filename.version().clone()) { std::collections::btree_map::Entry::Occupied(mut entry) => { entry.get_mut().push(filename, file); } std::collections::btree_map::Entry::Vacant(entry) => { let mut files = VersionFiles::default(); files.push(filename, file); entry.insert(files); } } } Self( version_map .into_iter() .map(|(version, files)| SimpleDetailMetadatum { version, files, metadata: None, }) .collect(), ) } fn from_pyx_files( files: Vec, mut core_metadata: FxHashMap, package_name: &PackageName, base: &Url, ) -> Self { let mut version_map: BTreeMap = BTreeMap::default(); // Convert to a reference-counted string. let base = SmallString::from(base.as_str()); // Group the distributions by version and kind for file in files { let file = match File::try_from_pyx(file, &base) { Ok(file) => file, Err(err) => { // Ignore files with unparsable version specifiers. warn!("Skipping file for {package_name}: {err}"); continue; } }; let Some(filename) = DistFilename::try_from_filename(&file.filename, package_name) else { warn!("Skipping file for {package_name}: {}", file.filename); continue; }; match version_map.entry(filename.version().clone()) { std::collections::btree_map::Entry::Occupied(mut entry) => { entry.get_mut().push(filename, file); } std::collections::btree_map::Entry::Vacant(entry) => { let mut files = VersionFiles::default(); files.push(filename, file); entry.insert(files); } } } Self( version_map .into_iter() .map(|(version, files)| { let metadata = core_metadata .remove(&version) .map(|metadata| ResolutionMetadata { name: package_name.clone(), version: version.clone(), requires_dist: metadata.requires_dist, requires_python: metadata.requires_python, provides_extra: metadata.provides_extra, dynamic: false, }); SimpleDetailMetadatum { version, files, metadata, } }) .collect(), ) } /// Read the [`SimpleDetailMetadata`] from an HTML index. fn from_html( text: &str, package_name: &PackageName, url: &DisplaySafeUrl, ) -> Result { let SimpleDetailHTML { base, files } = SimpleDetailHTML::parse(text, url) .map_err(|err| Error::from_html_err(err, url.clone()))?; Ok(Self::from_pypi_files(files, package_name, base.as_url())) } } impl IntoIterator for SimpleDetailMetadata { type Item = SimpleDetailMetadatum; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl ArchivedSimpleDetailMetadata { pub fn iter(&self) -> impl DoubleEndedIterator> { self.0.iter() } pub fn datum(&self, i: usize) -> Option<&rkyv::Archived> { self.0.get(i) } } #[derive(Debug)] enum MediaType { PyxV1Msgpack, PyxV1Json, PypiV1Json, PypiV1Html, TextHtml, } impl MediaType { /// Parse a media type from a string, returning `None` if the media type is not supported. fn from_str(s: &str) -> Option { match s { "application/vnd.pyx.simple.v1+msgpack" => Some(Self::PyxV1Msgpack), "application/vnd.pyx.simple.v1+json" => Some(Self::PyxV1Json), "application/vnd.pypi.simple.v1+json" => Some(Self::PypiV1Json), "application/vnd.pypi.simple.v1+html" => Some(Self::PypiV1Html), "text/html" => Some(Self::TextHtml), _ => None, } } /// Return the `Accept` header value for all PyPI media types. #[inline] const fn pypi() -> &'static str { // See: https://peps.python.org/pep-0691/#version-format-selection "application/vnd.pypi.simple.v1+json, application/vnd.pypi.simple.v1+html;q=0.2, text/html;q=0.01" } /// Return the `Accept` header value for all supported media types. #[inline] const fn all() -> &'static str { // See: https://peps.python.org/pep-0691/#version-format-selection "application/vnd.pyx.simple.v1+msgpack, application/vnd.pyx.simple.v1+json;q=0.9, application/vnd.pypi.simple.v1+json;q=0.8, application/vnd.pypi.simple.v1+html;q=0.2, text/html;q=0.01" } } impl std::fmt::Display for MediaType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::PyxV1Msgpack => write!(f, "application/vnd.pyx.simple.v1+msgpack"), Self::PyxV1Json => write!(f, "application/vnd.pyx.simple.v1+json"), Self::PypiV1Json => write!(f, "application/vnd.pypi.simple.v1+json"), Self::PypiV1Html => write!(f, "application/vnd.pypi.simple.v1+html"), Self::TextHtml => write!(f, "text/html"), } } } #[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] pub enum Connectivity { /// Allow access to the network. #[default] Online, /// Do not allow access to the network. Offline, } impl Connectivity { pub fn is_online(&self) -> bool { matches!(self, Self::Online) } pub fn is_offline(&self) -> bool { matches!(self, Self::Offline) } } #[cfg(test)] mod tests { use std::str::FromStr; use url::Url; use uv_normalize::PackageName; use uv_pypi_types::PypiSimpleDetail; use uv_redacted::DisplaySafeUrl; use crate::{ BaseClientBuilder, SimpleDetailMetadata, SimpleDetailMetadatum, html::SimpleDetailHTML, }; use crate::RegistryClientBuilder; use uv_cache::Cache; use uv_distribution_types::{FileLocation, ToUrlError}; use uv_small_str::SmallString; use wiremock::matchers::{basic_auth, method, path_regex}; use wiremock::{Mock, MockServer, ResponseTemplate}; type Error = Box; async fn start_test_server(username: &'static str, password: &'static str) -> MockServer { let server = MockServer::start().await; Mock::given(method("GET")) .and(basic_auth(username, password)) .respond_with(ResponseTemplate::new(200)) .mount(&server) .await; Mock::given(method("GET")) .respond_with(ResponseTemplate::new(401)) .mount(&server) .await; server } #[tokio::test] async fn test_redirect_to_server_with_credentials() -> Result<(), Error> { let username = "user"; let password = "password"; let auth_server = start_test_server(username, password).await; let auth_base_url = DisplaySafeUrl::parse(&auth_server.uri())?; let redirect_server = MockServer::start().await; // Configure the redirect server to respond with a 302 to the auth server Mock::given(method("GET")) .respond_with( ResponseTemplate::new(302).insert_header("Location", format!("{auth_base_url}")), ) .mount(&redirect_server) .await; let redirect_server_url = DisplaySafeUrl::parse(&redirect_server.uri())?; let cache = Cache::temp()?; let registry_client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache) .allow_cross_origin_credentials() .build(); let client = registry_client.cached_client().uncached(); assert_eq!( client .for_host(&redirect_server_url) .get(redirect_server.uri()) .send() .await? .status(), 401, "Requests should fail if credentials are missing" ); let mut url = redirect_server_url.clone(); let _ = url.set_username(username); let _ = url.set_password(Some(password)); assert_eq!( client .for_host(&redirect_server_url) .get(Url::from(url)) .send() .await? .status(), 200, "Requests should succeed if credentials are present" ); Ok(()) } #[tokio::test] async fn test_redirect_root_relative_url() -> Result<(), Error> { let username = "user"; let password = "password"; let redirect_server = MockServer::start().await; // Configure the redirect server to respond with a 307 with a relative URL. Mock::given(method("GET")) .and(path_regex("/foo/")) .respond_with( ResponseTemplate::new(307).insert_header("Location", "/bar/baz/".to_string()), ) .mount(&redirect_server) .await; Mock::given(method("GET")) .and(path_regex("/bar/baz/")) .and(basic_auth(username, password)) .respond_with(ResponseTemplate::new(200)) .mount(&redirect_server) .await; let redirect_server_url = DisplaySafeUrl::parse(&redirect_server.uri())?.join("foo/")?; let cache = Cache::temp()?; let registry_client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache) .allow_cross_origin_credentials() .build(); let client = registry_client.cached_client().uncached(); let mut url = redirect_server_url.clone(); let _ = url.set_username(username); let _ = url.set_password(Some(password)); assert_eq!( client .for_host(&url) .get(Url::from(url)) .send() .await? .status(), 200, "Requests should succeed for relative URL" ); Ok(()) } #[tokio::test] async fn test_redirect_relative_url() -> Result<(), Error> { let username = "user"; let password = "password"; let redirect_server = MockServer::start().await; // Configure the redirect server to respond with a 307 with a relative URL. Mock::given(method("GET")) .and(path_regex("/foo/bar/baz/")) .and(basic_auth(username, password)) .respond_with(ResponseTemplate::new(200)) .mount(&redirect_server) .await; Mock::given(method("GET")) .and(path_regex("/foo/")) .and(basic_auth(username, password)) .respond_with( ResponseTemplate::new(307).insert_header("Location", "bar/baz/".to_string()), ) .mount(&redirect_server) .await; let cache = Cache::temp()?; let registry_client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache) .allow_cross_origin_credentials() .build(); let client = registry_client.cached_client().uncached(); let redirect_server_url = DisplaySafeUrl::parse(&redirect_server.uri())?.join("foo/")?; let mut url = redirect_server_url.clone(); let _ = url.set_username(username); let _ = url.set_password(Some(password)); assert_eq!( client .for_host(&url) .get(Url::from(url)) .send() .await? .status(), 200, "Requests should succeed for relative URL" ); Ok(()) } #[test] fn ignore_failing_files() { // 1.7.7 has an invalid requires-python field (double comma), 1.7.8 is valid let response = r#" { "files": [ { "core-metadata": false, "data-dist-info-metadata": false, "filename": "pyflyby-1.7.7.tar.gz", "hashes": { "sha256": "0c4d953f405a7be1300b440dbdbc6917011a07d8401345a97e72cd410d5fb291" }, "requires-python": ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*, !=3.2.*, !=3.3.*, !=3.4.*,, !=3.5.*, !=3.6.*, <4", "size": 427200, "upload-time": "2022-05-19T09:14:36.591835Z", "url": "https://files.pythonhosted.org/packages/61/93/9fec62902d0b4fc2521333eba047bff4adbba41f1723a6382367f84ee522/pyflyby-1.7.7.tar.gz", "yanked": false }, { "core-metadata": false, "data-dist-info-metadata": false, "filename": "pyflyby-1.7.8.tar.gz", "hashes": { "sha256": "1ee37474f6da8f98653dbcc208793f50b7ace1d9066f49e2707750a5ba5d53c6" }, "requires-python": ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, <4", "size": 424460, "upload-time": "2022-08-04T10:42:02.190074Z", "url": "https://files.pythonhosted.org/packages/ad/39/17180d9806a1c50197bc63b25d0f1266f745fc3b23f11439fccb3d6baa50/pyflyby-1.7.8.tar.gz", "yanked": false } ] } "#; let data: PypiSimpleDetail = serde_json::from_str(response).unwrap(); let base = DisplaySafeUrl::parse("https://pypi.org/simple/pyflyby/").unwrap(); let simple_metadata = SimpleDetailMetadata::from_pypi_files( data.files, &PackageName::from_str("pyflyby").unwrap(), &base, ); let versions: Vec = simple_metadata .iter() .map(|SimpleDetailMetadatum { version, .. }| version.to_string()) .collect(); assert_eq!(versions, ["1.7.8".to_string()]); } /// Test for AWS Code Artifact registry /// /// See: #[test] fn relative_urls_code_artifact() -> Result<(), ToUrlError> { let text = r#" Links for flask

Links for flask

Flask-0.1.tar.gz
Flask-0.10.1.tar.gz
flask-3.0.1.tar.gz
"#; // Note the lack of a trailing `/` here is important for coverage of url-join behavior let base = DisplaySafeUrl::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask") .unwrap(); let SimpleDetailHTML { base, files } = SimpleDetailHTML::parse(text, &base).unwrap(); let base = SmallString::from(base.as_str()); // Test parsing of the file urls let urls = files .into_iter() .map(|file| FileLocation::new(file.url, &base).to_url()) .collect::, _>>()?; let urls = urls .iter() .map(DisplaySafeUrl::to_string) .collect::>(); insta::assert_debug_snapshot!(urls, @r#" [ "https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/0.1/Flask-0.1.tar.gz", "https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/0.10.1/Flask-0.10.1.tar.gz", "https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/3.0.1/flask-3.0.1.tar.gz", ] "#); Ok(()) } } uv-0.9.17+ds1/crates/uv-client/src/remote_metadata.rs000066400000000000000000000110161520155276700224150ustar00rootroot00000000000000use crate::{Error, ErrorKind}; use async_http_range_reader::AsyncHttpRangeReader; use futures::io::BufReader; use tokio_util::compat::TokioAsyncReadCompatExt; use url::Url; use uv_distribution_filename::WheelFilename; use uv_metadata::find_archive_dist_info; /// Read the `.dist-info/METADATA` file from a async remote zip reader, so we avoid downloading the /// entire wheel just for the one file. /// /// This method is derived from `prefix-dev/rip`, which is available under the following BSD-3 /// Clause license: /// /// ```text /// BSD 3-Clause License /// /// Copyright (c) 2023, prefix.dev GmbH /// /// 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. /// ``` /// /// Additional work and modifications to the originating source are available under the /// Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) /// or MIT license ([LICENSE-MIT](LICENSE-MIT) or ), as per the /// rest of the crate. pub(crate) async fn wheel_metadata_from_remote_zip( filename: &WheelFilename, debug_name: &Url, reader: &mut AsyncHttpRangeReader, ) -> Result { // Make sure we have the back part of the stream. // Best guess for the central directory size inside the zip const CENTRAL_DIRECTORY_SIZE: u64 = 16384; // Because the zip index is at the back reader .prefetch(reader.len().saturating_sub(CENTRAL_DIRECTORY_SIZE)..reader.len()) .await; // Construct a zip reader to uses the stream. let buf = BufReader::new(reader.compat()); let mut reader = async_zip::base::read::seek::ZipFileReader::new(buf) .await .map_err(|err| ErrorKind::Zip(filename.clone(), err))?; let ((metadata_idx, metadata_entry), _dist_info_prefix) = find_archive_dist_info( filename, reader .file() .entries() .iter() .enumerate() .filter_map(|(idx, e)| Some(((idx, e), e.filename().as_str().ok()?))), ) .map_err(|err| ErrorKind::Metadata(debug_name.to_string(), err))?; let offset = metadata_entry.header_offset(); let size = metadata_entry.compressed_size() + 30 // Header size in bytes + metadata_entry.filename().as_bytes().len() as u64; // The zip archive uses as BufReader which reads in chunks of 8192. To ensure we prefetch // enough data we round the size up to the nearest multiple of the buffer size. let buffer_size = 8192; let size = size.div_ceil(buffer_size) * buffer_size; // Fetch the bytes from the zip archive that contain the requested file. reader .inner_mut() .get_mut() .get_mut() .prefetch(offset..offset + size) .await; // Read the contents of the METADATA file let mut contents = String::new(); reader .reader_with_entry(metadata_idx) .await .map_err(|err| ErrorKind::Zip(filename.clone(), err))? .read_to_string_checked(&mut contents) .await .map_err(|err| ErrorKind::Zip(filename.clone(), err))?; Ok(contents) } uv-0.9.17+ds1/crates/uv-client/src/rkyvutil.rs000066400000000000000000000153251520155276700211620ustar00rootroot00000000000000/*! Defines some helpers for use with `rkyv`. # Owned archived type Typical usage patterns with rkyv involve using an `&Archived`, where values of that type are cast from a `&[u8]`. The owned archive type in this module effectively provides a way to use `Archive` without needing to worry about the lifetime of the buffer it's attached to. This works by making the owned archive type own the buffer itself. It then provides convenient routines for serializing and deserializing. */ use rkyv::{ Archive, Deserialize, Portable, Serialize, api::high::{HighDeserializer, HighSerializer, HighValidator}, bytecheck::CheckBytes, rancor, ser::allocator::ArenaHandle, util::AlignedVec, }; use crate::{Error, ErrorKind}; /// A convenient alias for the rkyv serializer used by `uv-client`. /// /// This utilizes rkyv's `HighSerializer` but fixes its type parameters where /// possible since we don't need the full flexibility of a generic serializer. pub type Serializer<'a> = HighSerializer, rancor::Error>; /// A convenient alias for the rkyv deserializer used by `uv-client`. /// /// This utilizes rkyv's `HighDeserializer` but fixes its type parameters /// where possible since we don't need the full flexibility of a generic /// deserializer. pub type Deserializer = HighDeserializer; /// A convenient alias for the rkyv validator used by `uv-client`. /// /// This utilizes rkyv's `HighValidator` but fixes its type parameters where /// possible since we don't need the full flexibility of a generic validator. pub type Validator<'a> = HighValidator<'a, rancor::Error>; /// An owned archived type. /// /// This type is effectively an owned version of `Archived`. Normally, when /// one gets an archived type from a buffer, the archive type is bound to the /// lifetime of the buffer. This effectively provides a home for that buffer so /// that one can pass around an archived type as if it were owned. /// /// Constructing the type requires validating the bytes are a valid /// representation of an `Archived`, but subsequent accesses (via deref) are /// free. /// /// Note that this type makes a number of assumptions about the specific /// serializer, deserializer and validator used. This type could be made /// more generic, but it's not clear we need that in uv. By making our /// choices concrete here, we make use of this type much simpler to understand. /// Unfortunately, AG couldn't find a way of making the trait bounds simpler, /// so if `OwnedVec` is being used in trait implementations, the traits bounds /// will likely need to be copied from here. #[derive(Debug)] pub struct OwnedArchive { raw: AlignedVec, archive: std::marker::PhantomData, } impl OwnedArchive where A: Archive + for<'a> Serialize>, A::Archived: Portable + Deserialize + for<'a> CheckBytes>, { /// Create a new owned archived value from the raw aligned bytes of the /// serialized representation of an `A`. /// /// # Errors /// /// If the bytes fail validation (e.g., contains unaligned pointers or /// strings aren't valid UTF-8), then this returns an error. pub fn new(raw: AlignedVec) -> Result { // We convert the error to a simple string because... the error type // does not implement Send. And I don't think we really need to keep // the error type around anyway. let _ = rkyv::access::(&raw) .map_err(|e| ErrorKind::ArchiveRead(e.to_string()))?; Ok(Self { raw, archive: std::marker::PhantomData, }) } /// Like `OwnedArchive::new`, but reads the value from the given reader. /// /// Note that this consumes the entirety of the given reader. /// /// # Errors /// /// If the bytes fail validation (e.g., contains unaligned pointers or /// strings aren't valid UTF-8), then this returns an error. pub fn from_reader(mut rdr: R) -> Result { let mut buf = AlignedVec::with_capacity(1024); buf.extend_from_reader(&mut rdr).map_err(ErrorKind::Io)?; Self::new(buf) } /// Creates an owned archive value from the unarchived value. /// /// # Errors /// /// This can fail if creating an archive for the given type fails. /// Currently, this, at minimum, includes cases where an `A` contains a /// `PathBuf` that is not valid UTF-8. pub fn from_unarchived(unarchived: &A) -> Result { let raw = rkyv::to_bytes::(unarchived) .map_err(|e| ErrorKind::ArchiveWrite(e.to_string()))?; Ok(Self { raw, archive: std::marker::PhantomData, }) } /// Write the underlying bytes of this archived value to the given writer. /// /// Note that because this type has a `Deref` impl, this method requires /// fully-qualified syntax. So, if `o` is an `OwnedValue`, then use /// `OwnedValue::write(&o, wtr)`. /// /// # Errors /// /// Any failures from writing are returned to the caller. pub fn write(this: &Self, mut wtr: W) -> Result<(), Error> { Ok(wtr.write_all(&this.raw).map_err(ErrorKind::Io)?) } /// Returns the raw underlying bytes of this owned archive value. /// /// They are guaranteed to be a valid serialization of `Archived`. /// /// Note that because this type has a `Deref` impl, this method requires /// fully-qualified syntax. So, if `o` is an `OwnedValue`, then use /// `OwnedValue::as_bytes(&o)`. pub fn as_bytes(this: &Self) -> &[u8] { &this.raw } /// Deserialize this owned archived value into the original /// `SimpleMetadata`. /// /// Note that because this type has a `Deref` impl, this method requires /// fully-qualified syntax. So, if `o` is an `OwnedValue`, then use /// `OwnedValue::deserialize(&o)`. pub fn deserialize(this: &Self) -> A { rkyv::deserialize(&**this).expect("valid archive must deserialize correctly") } } impl std::ops::Deref for OwnedArchive where A: Archive + for<'a> Serialize>, A::Archived: Portable + Deserialize + for<'a> CheckBytes>, { type Target = A::Archived; fn deref(&self) -> &A::Archived { // SAFETY: We've validated that our underlying buffer is a valid // archive for SimpleMetadata in the constructor, so we can skip // validation here. Since we don't mutate the buffer, this conversion // is guaranteed to be correct. #[allow(unsafe_code)] unsafe { rkyv::access_unchecked::(&self.raw) } } } uv-0.9.17+ds1/crates/uv-client/src/tls.rs000066400000000000000000000012401520155276700200620ustar00rootroot00000000000000use reqwest::Identity; use std::ffi::OsStr; use std::io::Read; #[derive(thiserror::Error, Debug)] pub(crate) enum CertificateError { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] Reqwest(reqwest::Error), } /// Return the `Identity` from the provided file. pub(crate) fn read_identity(ssl_client_cert: &OsStr) -> Result { let mut buf = Vec::new(); fs_err::File::open(ssl_client_cert)?.read_to_end(&mut buf)?; Identity::from_pem(&buf).map_err(|tls_err| { debug_assert!(tls_err.is_builder(), "must be a rustls::Error internally"); CertificateError::Reqwest(tls_err) }) } uv-0.9.17+ds1/crates/uv-client/tests/000077500000000000000000000000001520155276700172705ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-client/tests/it/000077500000000000000000000000001520155276700177045ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-client/tests/it/http_util.rs000066400000000000000000000341661520155276700223000ustar00rootroot00000000000000use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; use anyhow::{Context, Result}; use futures::future; use http_body_util::combinators::BoxBody; use http_body_util::{BodyExt, Full}; use hyper::body::{Bytes, Incoming}; use hyper::header::USER_AGENT; use hyper::service::service_fn; use hyper::{Request, Response}; use hyper_util::rt::{TokioExecutor, TokioIo}; use hyper_util::server::conn::auto::Builder; use rcgen::{ BasicConstraints, Certificate, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose, SanType, date_time_ymd, }; use rustls::pki_types::{CertificateDer, PrivateKeyDer}; use rustls::server::WebPkiClientVerifier; use rustls::{RootCertStore, ServerConfig}; use tokio::net::TcpListener; use tokio::task::JoinHandle; use tokio_rustls::TlsAcceptor; use uv_fs::Simplified; /// An issued certificate, together with the subject keypair. #[derive(Debug)] pub(crate) struct SelfSigned { /// An issued certificate. pub public: Certificate, /// The certificate's subject signing key. pub private: KeyPair, } /// Defines the base location for temporary generated certs. /// /// See [`TestContext::test_bucket_dir`] for implementation rationale. pub(crate) fn test_cert_dir() -> PathBuf { std::env::temp_dir() .simple_canonicalize() .expect("failed to canonicalize temp dir") .join("uv") .join("tests") .join("certs") } /// Generates a self-signed server certificate for `uv-test-server`, `localhost` and `127.0.0.1`. /// This certificate is standalone and not issued by a self-signed Root CA. /// /// Use sparingly as generation of certs is a slow operation. pub(crate) fn generate_self_signed_certs() -> Result { let mut params = CertificateParams::default(); params.is_ca = IsCa::NoCa; params.not_before = date_time_ymd(1975, 1, 1); params.not_after = date_time_ymd(4096, 1, 1); params.key_usages.push(KeyUsagePurpose::DigitalSignature); params.key_usages.push(KeyUsagePurpose::KeyEncipherment); params .extended_key_usages .push(ExtendedKeyUsagePurpose::ServerAuth); params .distinguished_name .push(DnType::OrganizationName, "Astral Software Inc."); params .distinguished_name .push(DnType::CommonName, "uv-test-server"); params .subject_alt_names .push(SanType::DnsName("uv-test-server".try_into()?)); params .subject_alt_names .push(SanType::DnsName("localhost".try_into()?)); params .subject_alt_names .push(SanType::IpAddress("127.0.0.1".parse()?)); let private = KeyPair::generate()?; let public = params.self_signed(&private)?; Ok(SelfSigned { public, private }) } /// Generates a self-signed root CA, server certificate, and client certificate. /// There are no intermediate certs generated as part of this function. /// The server certificate is for `uv-test-server`, `localhost` and `127.0.0.1` issued by this CA. /// The client certificate is for `uv-test-client` issued by this CA. /// /// Use sparingly as generation of these certs is a very slow operation. pub(crate) fn generate_self_signed_certs_with_ca() -> Result<(SelfSigned, SelfSigned, SelfSigned)> { // Generate the CA let mut ca_params = CertificateParams::default(); ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); // root cert ca_params.not_before = date_time_ymd(1975, 1, 1); ca_params.not_after = date_time_ymd(4096, 1, 1); ca_params.key_usages.push(KeyUsagePurpose::DigitalSignature); ca_params.key_usages.push(KeyUsagePurpose::KeyCertSign); ca_params.key_usages.push(KeyUsagePurpose::CrlSign); ca_params .distinguished_name .push(DnType::OrganizationName, "Astral Software Inc."); ca_params .distinguished_name .push(DnType::CommonName, "uv-test-ca"); ca_params .subject_alt_names .push(SanType::DnsName("uv-test-ca".try_into()?)); let ca_private_key = KeyPair::generate()?; let ca_public_cert = ca_params.self_signed(&ca_private_key)?; let ca_cert_issuer = Issuer::new(ca_params, &ca_private_key); // Generate server cert issued by this CA let mut server_params = CertificateParams::default(); server_params.is_ca = IsCa::NoCa; server_params.not_before = date_time_ymd(1975, 1, 1); server_params.not_after = date_time_ymd(4096, 1, 1); server_params.use_authority_key_identifier_extension = true; server_params .key_usages .push(KeyUsagePurpose::DigitalSignature); server_params .key_usages .push(KeyUsagePurpose::KeyEncipherment); server_params .extended_key_usages .push(ExtendedKeyUsagePurpose::ServerAuth); server_params .distinguished_name .push(DnType::OrganizationName, "Astral Software Inc."); server_params .distinguished_name .push(DnType::CommonName, "uv-test-server"); server_params .subject_alt_names .push(SanType::DnsName("uv-test-server".try_into()?)); server_params .subject_alt_names .push(SanType::DnsName("localhost".try_into()?)); server_params .subject_alt_names .push(SanType::IpAddress("127.0.0.1".parse()?)); let server_private_key = KeyPair::generate()?; let server_public_cert = server_params.signed_by(&server_private_key, &ca_cert_issuer)?; // Generate client cert issued by this CA let mut client_params = CertificateParams::default(); client_params.is_ca = IsCa::NoCa; client_params.not_before = date_time_ymd(1975, 1, 1); client_params.not_after = date_time_ymd(4096, 1, 1); client_params.use_authority_key_identifier_extension = true; client_params .key_usages .push(KeyUsagePurpose::DigitalSignature); client_params .extended_key_usages .push(ExtendedKeyUsagePurpose::ClientAuth); client_params .distinguished_name .push(DnType::OrganizationName, "Astral Software Inc."); client_params .distinguished_name .push(DnType::CommonName, "uv-test-client"); client_params .subject_alt_names .push(SanType::DnsName("uv-test-client".try_into()?)); let client_private_key = KeyPair::generate()?; let client_public_cert = client_params.signed_by(&client_private_key, &ca_cert_issuer)?; let ca_self_signed = SelfSigned { public: ca_public_cert, private: ca_private_key, }; let server_self_signed = SelfSigned { public: server_public_cert, private: server_private_key, }; let client_self_signed = SelfSigned { public: client_public_cert, private: client_private_key, }; Ok((ca_self_signed, server_self_signed, client_self_signed)) } // Plain is fine for now; Arc/Box could be used later if we need to support move. type ServerSvcFn = fn( Request, ) -> future::Ready>, hyper::Error>>; #[derive(Default)] pub(crate) struct TestServerBuilder<'a> { // Custom server response function svc_fn: Option, // CA certificate ca_cert: Option<&'a SelfSigned>, // Server certificate server_cert: Option<&'a SelfSigned>, // Enable mTLS Verification mutual_tls: bool, } impl<'a> TestServerBuilder<'a> { pub(crate) fn new() -> Self { Self { svc_fn: None, server_cert: None, ca_cert: None, mutual_tls: false, } } #[expect(unused)] /// Provide a custom server response function. pub(crate) fn with_svc_fn(mut self, svc_fn: ServerSvcFn) -> Self { self.svc_fn = Some(svc_fn); self } /// Provide the server certificate. This will enable TLS (HTTPS). pub(crate) fn with_server_cert(mut self, server_cert: &'a SelfSigned) -> Self { self.server_cert = Some(server_cert); self } /// CA certificate used to build the `RootCertStore` for client verification. /// Requires `with_server_cert`. pub(crate) fn with_ca_cert(mut self, ca_cert: &'a SelfSigned) -> Self { self.ca_cert = Some(ca_cert); self } /// Enforce mutual TLS (client cert auth). /// Requires `with_server_cert` and `with_ca_cert`. pub(crate) fn with_mutual_tls(mut self, mutual: bool) -> Self { self.mutual_tls = mutual; self } /// Starts the HTTP(S) server with optional mTLS enforcement. pub(crate) async fn start(self) -> Result<(JoinHandle>, SocketAddr)> { // Validate builder input combinations if self.ca_cert.is_some() && self.server_cert.is_none() { anyhow::bail!("server certificate is required when CA certificate is provided"); } if self.mutual_tls && (self.ca_cert.is_none() || self.server_cert.is_none()) { anyhow::bail!("ca certificate is required for mTLS"); } // Set up the TCP listener on a random available port let listener = TcpListener::bind("127.0.0.1:0").await?; let addr = listener.local_addr()?; // Setup TLS Config (if any) let tls_acceptor = if let Some(server_cert) = self.server_cert { // Prepare Server Cert and KeyPair let server_key = PrivateKeyDer::try_from(server_cert.private.serialize_der()).unwrap(); let server_cert = vec![CertificateDer::from(server_cert.public.der().to_vec())]; // Setup CA Verifier let client_verifier = if let Some(ca_cert) = self.ca_cert { let mut root_store = RootCertStore::empty(); root_store .add(CertificateDer::from(ca_cert.public.der().to_vec())) .expect("failed to add CA cert"); if self.mutual_tls { // Setup mTLS CA config WebPkiClientVerifier::builder(root_store.into()) .build() .expect("failed to setup client verifier") } else { // Only load the CA roots WebPkiClientVerifier::builder(root_store.into()) .allow_unauthenticated() .build() .expect("failed to setup client verifier") } } else { WebPkiClientVerifier::no_client_auth() }; let mut tls_config = ServerConfig::builder() .with_client_cert_verifier(client_verifier) .with_single_cert(server_cert, server_key)?; tls_config.alpn_protocols = vec![b"http/1.1".to_vec(), b"http/1.0".to_vec()]; Some(TlsAcceptor::from(Arc::new(tls_config))) } else { None }; // Setup Response Handler let svc_fn = if let Some(custom_svc_fn) = self.svc_fn { custom_svc_fn } else { |req: Request| { // Get User Agent Header and send it back in the response let user_agent = req .headers() .get(USER_AGENT) .and_then(|v| v.to_str().ok()) .map(ToString::to_string) .unwrap_or_default(); // Empty Default let response_content = Full::new(Bytes::from(user_agent)) .map_err(|_| unreachable!()) .boxed(); // If we ever want a true echo server, we can use instead // let response_content = req.into_body().boxed(); // although uv-client doesn't expose post currently. future::ok::<_, hyper::Error>(Response::new(response_content)) } }; // Spawn the server loop in a background task let server_task = tokio::spawn(async move { let svc = service_fn(move |req: Request| svc_fn(req)); let (tcp_stream, _remote_addr) = listener .accept() .await .context("Failed to accept TCP connection")?; // Start Server (not wrapped in loop {} since we want a single response server) // If we want server to accept multiple connections, we can wrap it in loop {} // but we'll need to ensure to handle termination signals in the tests otherwise // it may never stop. if let Some(tls_acceptor) = tls_acceptor { let tls_stream = tls_acceptor .accept(tcp_stream) .await .context("Failed to accept TLS connection")?; let socket = TokioIo::new(tls_stream); tokio::task::spawn(async move { Builder::new(TokioExecutor::new()) .serve_connection(socket, svc) .await .expect("HTTPS Server Started"); }); } else { let socket = TokioIo::new(tcp_stream); tokio::task::spawn(async move { Builder::new(TokioExecutor::new()) .serve_connection(socket, svc) .await .expect("HTTP Server Started"); }); } Ok(()) }); Ok((server_task, addr)) } } /// Single Request HTTP server that echoes the User Agent Header. pub(crate) async fn start_http_user_agent_server() -> Result<(JoinHandle>, SocketAddr)> { TestServerBuilder::new().start().await } /// Single Request HTTPS server that echoes the User Agent Header. pub(crate) async fn start_https_user_agent_server( server_cert: &SelfSigned, ) -> Result<(JoinHandle>, SocketAddr)> { TestServerBuilder::new() .with_server_cert(server_cert) .start() .await } /// Single Request HTTPS mTLS server that echoes the User Agent Header. pub(crate) async fn start_https_mtls_user_agent_server( ca_cert: &SelfSigned, server_cert: &SelfSigned, ) -> Result<(JoinHandle>, SocketAddr)> { TestServerBuilder::new() .with_ca_cert(ca_cert) .with_server_cert(server_cert) .with_mutual_tls(true) .start() .await } uv-0.9.17+ds1/crates/uv-client/tests/it/main.rs000066400000000000000000000001131520155276700211710ustar00rootroot00000000000000mod http_util; mod remote_metadata; mod ssl_certs; mod user_agent_version; uv-0.9.17+ds1/crates/uv-client/tests/it/remote_metadata.rs000066400000000000000000000024441520155276700234110ustar00rootroot00000000000000use std::str::FromStr; use anyhow::Result; use uv_cache::Cache; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; use uv_distribution_filename::WheelFilename; use uv_distribution_types::{BuiltDist, DirectUrlBuiltDist, IndexCapabilities}; use uv_pep508::VerbatimUrl; use uv_redacted::DisplaySafeUrl; #[tokio::test] async fn remote_metadata_with_and_without_cache() -> Result<()> { let cache = Cache::temp()?.init().await?; let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build(); // The first run is without cache (the tempdir is empty), the second has the cache from the // first run. for _ in 0..2 { let url = "https://files.pythonhosted.org/packages/00/e5/f12a80907d0884e6dff9c16d0c0114d81b8cd07dc3ae54c5e962cc83037e/tqdm-4.66.1-py3-none-any.whl"; let filename = WheelFilename::from_str(url.rsplit_once('/').unwrap().1)?; let dist = BuiltDist::DirectUrl(DirectUrlBuiltDist { filename, location: Box::new(DisplaySafeUrl::parse(url)?), url: VerbatimUrl::from_str(url)?, }); let capabilities = IndexCapabilities::default(); let metadata = client.wheel_metadata(&dist, &capabilities).await?; assert_eq!(metadata.version.to_string(), "4.66.1"); } Ok(()) } uv-0.9.17+ds1/crates/uv-client/tests/it/ssl_certs.rs000066400000000000000000000274311520155276700222620ustar00rootroot00000000000000use std::str::FromStr; use anyhow::Result; use rustls::AlertDescription; use url::Url; use uv_cache::Cache; use uv_client::BaseClientBuilder; use uv_client::RegistryClientBuilder; use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; use crate::http_util::{ generate_self_signed_certs, generate_self_signed_certs_with_ca, start_https_mtls_user_agent_server, start_https_user_agent_server, test_cert_dir, }; // SAFETY: This test is meant to run with single thread configuration #[tokio::test] #[allow(unsafe_code)] async fn ssl_env_vars() -> Result<()> { // Ensure our environment is not polluted with anything that may affect `rustls-native-certs` unsafe { std::env::remove_var(EnvVars::UV_NATIVE_TLS); std::env::remove_var(EnvVars::SSL_CERT_FILE); std::env::remove_var(EnvVars::SSL_CERT_DIR); std::env::remove_var(EnvVars::SSL_CLIENT_CERT); } // Create temporary cert dirs let cert_dir = test_cert_dir(); fs_err::create_dir_all(&cert_dir).expect("Failed to create test cert bucket"); let cert_dir = tempfile::TempDir::new_in(cert_dir).expect("Failed to create test cert directory"); let does_not_exist_cert_dir = cert_dir.path().join("does_not_exist"); // Generate self-signed standalone cert let standalone_server_cert = generate_self_signed_certs()?; let standalone_public_pem_path = cert_dir.path().join("standalone_public.pem"); let standalone_private_pem_path = cert_dir.path().join("standalone_private.pem"); // Generate self-signed CA, server, and client certs let (ca_cert, server_cert, client_cert) = generate_self_signed_certs_with_ca()?; let ca_public_pem_path = cert_dir.path().join("ca_public.pem"); let ca_private_pem_path = cert_dir.path().join("ca_private.pem"); let server_public_pem_path = cert_dir.path().join("server_public.pem"); let server_private_pem_path = cert_dir.path().join("server_private.pem"); let client_combined_pem_path = cert_dir.path().join("client_combined.pem"); // Persist the certs in PKCS8 format as the env vars expect a path on disk fs_err::write( standalone_public_pem_path.as_path(), standalone_server_cert.public.pem(), )?; fs_err::write( standalone_private_pem_path.as_path(), standalone_server_cert.private.serialize_pem(), )?; fs_err::write(ca_public_pem_path.as_path(), ca_cert.public.pem())?; fs_err::write( ca_private_pem_path.as_path(), ca_cert.private.serialize_pem(), )?; fs_err::write(server_public_pem_path.as_path(), server_cert.public.pem())?; fs_err::write( server_private_pem_path.as_path(), server_cert.private.serialize_pem(), )?; fs_err::write( client_combined_pem_path.as_path(), // SSL_CLIENT_CERT expects a "combined" cert with the public and private key. format!( "{}\n{}", client_cert.public.pem(), client_cert.private.serialize_pem() ), )?; // ** Set SSL_CERT_FILE to non-existent location // ** Then verify our request fails to establish a connection unsafe { std::env::set_var(EnvVars::SSL_CERT_FILE, does_not_exist_cert_dir.as_os_str()); } let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?; let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?; let cache = Cache::temp()?.init().await?; let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build(); let res = client .cached_client() .uncached() .for_host(&url) .get(Url::from(url)) .send() .await; unsafe { std::env::remove_var(EnvVars::SSL_CERT_FILE); } // Validate the client error let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.err() else { panic!("expected middleware error"); }; let reqwest_error = middleware_error .chain() .find_map(|err| { err.downcast_ref::().map(|err| { if let reqwest_middleware::Error::Reqwest(inner) = err { inner } else { panic!("expected reqwest error") } }) }) .expect("expected reqwest error"); assert!(reqwest_error.is_connect()); // Validate the server error let server_res = server_task.await?; let expected_err = if let Err(anyhow_err) = server_res && let Some(io_err) = anyhow_err.downcast_ref::() && let Some(wrapped_err) = io_err.get_ref() && let Some(tls_err) = wrapped_err.downcast_ref::() && matches!( tls_err, rustls::Error::AlertReceived(AlertDescription::UnknownCA) ) { true } else { false }; assert!(expected_err); // ** Set SSL_CERT_FILE to our public certificate // ** Then verify our request successfully establishes a connection unsafe { std::env::set_var( EnvVars::SSL_CERT_FILE, standalone_public_pem_path.as_os_str(), ); } let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?; let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?; let cache = Cache::temp()?.init().await?; let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build(); let res = client .cached_client() .uncached() .for_host(&url) .get(Url::from(url)) .send() .await; assert!(res.is_ok()); let _ = server_task.await?; // wait for server shutdown unsafe { std::env::remove_var(EnvVars::SSL_CERT_FILE); } // ** Set SSL_CERT_DIR to our cert dir as well as some other dir that does not exist // ** Then verify our request still successfully establishes a connection unsafe { std::env::set_var( EnvVars::SSL_CERT_DIR, std::env::join_paths(vec![ cert_dir.path().as_os_str(), does_not_exist_cert_dir.as_os_str(), ])?, ); } let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?; let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?; let cache = Cache::temp()?.init().await?; let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build(); let res = client .cached_client() .uncached() .for_host(&url) .get(Url::from(url)) .send() .await; assert!(res.is_ok()); let _ = server_task.await?; // wait for server shutdown unsafe { std::env::remove_var(EnvVars::SSL_CERT_DIR); } // ** Set SSL_CERT_DIR to only the dir that does not exist // ** Then verify our request fails to establish a connection unsafe { std::env::set_var(EnvVars::SSL_CERT_DIR, does_not_exist_cert_dir.as_os_str()); } let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?; let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?; let cache = Cache::temp()?.init().await?; let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build(); let res = client .cached_client() .uncached() .for_host(&url) .get(Url::from(url)) .send() .await; unsafe { std::env::remove_var(EnvVars::SSL_CERT_DIR); } // Validate the client error let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.err() else { panic!("expected middleware error"); }; let reqwest_error = middleware_error .chain() .find_map(|err| { err.downcast_ref::().map(|err| { if let reqwest_middleware::Error::Reqwest(inner) = err { inner } else { panic!("expected reqwest error") } }) }) .expect("expected reqwest error"); assert!(reqwest_error.is_connect()); // Validate the server error let server_res = server_task.await?; let expected_err = if let Err(anyhow_err) = server_res && let Some(io_err) = anyhow_err.downcast_ref::() && let Some(wrapped_err) = io_err.get_ref() && let Some(tls_err) = wrapped_err.downcast_ref::() && matches!( tls_err, rustls::Error::AlertReceived(AlertDescription::UnknownCA) ) { true } else { false }; assert!(expected_err); // *** mTLS Tests // ** Set SSL_CERT_FILE to our CA and SSL_CLIENT_CERT to our client cert // ** Then verify our request still successfully establishes a connection // We need to set SSL_CERT_FILE or SSL_CERT_DIR to our CA as we need to tell // our HTTP client that we trust certificates issued by our self-signed CA. // This inherently also tests that our server cert is also validated as part // of the certificate path validation algorithm. unsafe { std::env::set_var(EnvVars::SSL_CERT_FILE, ca_public_pem_path.as_os_str()); std::env::set_var( EnvVars::SSL_CLIENT_CERT, client_combined_pem_path.as_os_str(), ); } let (server_task, addr) = start_https_mtls_user_agent_server(&ca_cert, &server_cert).await?; let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?; let cache = Cache::temp()?.init().await?; let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build(); let res = client .cached_client() .uncached() .for_host(&url) .get(Url::from(url)) .send() .await; assert!(res.is_ok()); let _ = server_task.await?; // wait for server shutdown unsafe { std::env::remove_var(EnvVars::SSL_CERT_FILE); std::env::remove_var(EnvVars::SSL_CLIENT_CERT); } // ** Set SSL_CERT_FILE to our CA and unset SSL_CLIENT_CERT // ** Then verify our request fails to establish a connection unsafe { std::env::set_var(EnvVars::SSL_CERT_FILE, ca_public_pem_path.as_os_str()); } let (server_task, addr) = start_https_mtls_user_agent_server(&ca_cert, &server_cert).await?; let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?; let cache = Cache::temp()?.init().await?; let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build(); let res = client .cached_client() .uncached() .for_host(&url) .get(Url::from(url)) .send() .await; unsafe { std::env::remove_var(EnvVars::SSL_CERT_FILE); } // Validate the client error let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.err() else { panic!("expected middleware error"); }; let reqwest_error = middleware_error .chain() .find_map(|err| { err.downcast_ref::().map(|err| { if let reqwest_middleware::Error::Reqwest(inner) = err { inner } else { panic!("expected reqwest error") } }) }) .expect("expected reqwest error"); assert!(reqwest_error.is_connect()); // Validate the server error let server_res = server_task.await?; let expected_err = if let Err(anyhow_err) = server_res && let Some(io_err) = anyhow_err.downcast_ref::() && let Some(wrapped_err) = io_err.get_ref() && let Some(tls_err) = wrapped_err.downcast_ref::() && matches!(tls_err, rustls::Error::NoCertificatesPresented) { true } else { false }; assert!(expected_err); // Fin. Ok(()) } uv-0.9.17+ds1/crates/uv-client/tests/it/user_agent_version.rs000066400000000000000000000201101520155276700241450ustar00rootroot00000000000000use std::str::FromStr; use anyhow::Result; use insta::{assert_json_snapshot, assert_snapshot, with_settings}; use url::Url; use uv_cache::Cache; use uv_client::RegistryClientBuilder; use uv_client::{BaseClientBuilder, LineHaul}; use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder}; use uv_platform_tags::{Arch, Os, Platform}; use uv_redacted::DisplaySafeUrl; use uv_version::version; use crate::http_util::start_http_user_agent_server; #[tokio::test] async fn test_user_agent_has_version() -> Result<()> { // Initialize dummy http server let (server_task, addr) = start_http_user_agent_server().await?; // Initialize uv-client let cache = Cache::temp()?.init().await?; let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build(); // Send request to our dummy server let url = DisplaySafeUrl::from_str(&format!("http://{addr}"))?; let res = client .cached_client() .uncached() .for_host(&url) .get(Url::from(url)) .send() .await?; // Check the HTTP status assert!(res.status().is_success()); // Check User Agent let body = res.text().await?; let (uv_version, uv_linehaul) = body .split_once(' ') .expect("Failed to split User-Agent header"); // Deserializing Linehaul let linehaul: LineHaul = serde_json::from_str(uv_linehaul)?; // Assert linehaul user agent let filters = vec![(version(), "[VERSION]")]; with_settings!({ filters => filters }, { // Assert uv version assert_snapshot!(uv_version, @"uv/[VERSION]"); // Assert linehaul json assert_json_snapshot!(&linehaul.installer, @r#" { "name": "uv", "version": "[VERSION]", "subcommand": null } "#); }); // Wait for the server task to complete, to be a good citizen. let _ = server_task.await?; Ok(()) } #[tokio::test] async fn test_user_agent_has_subcommand() -> Result<()> { // Initialize dummy http server let (server_task, addr) = start_http_user_agent_server().await?; // Initialize uv-client let cache = Cache::temp()?.init().await?; let client = RegistryClientBuilder::new( BaseClientBuilder::default().subcommand(vec!["foo".to_owned(), "bar".to_owned()]), cache, ) .build(); // Send request to our dummy server let url = DisplaySafeUrl::from_str(&format!("http://{addr}"))?; let res = client .cached_client() .uncached() .for_host(&url) .get(Url::from(url)) .send() .await?; // Check the HTTP status assert!(res.status().is_success()); // Check User Agent let body = res.text().await?; let (uv_version, uv_linehaul) = body .split_once(' ') .expect("Failed to split User-Agent header"); // Deserializing Linehaul let linehaul: LineHaul = serde_json::from_str(uv_linehaul)?; // Assert linehaul user agent let filters = vec![(version(), "[VERSION]")]; with_settings!({ filters => filters }, { // Assert uv version assert_snapshot!(uv_version, @"uv/[VERSION]"); // Assert linehaul json assert_json_snapshot!(&linehaul.installer, @r#" { "name": "uv", "version": "[VERSION]", "subcommand": [ "foo", "bar" ] } "#); }); // Wait for the server task to complete, to be a good citizen. let _ = server_task.await?; Ok(()) } #[tokio::test] async fn test_user_agent_has_linehaul() -> Result<()> { // Initialize dummy http server let (server_task, addr) = start_http_user_agent_server().await?; // Add some representative markers for an Ubuntu CI runner let markers = MarkerEnvironment::try_from(MarkerEnvironmentBuilder { implementation_name: "cpython", implementation_version: "3.12.2", os_name: "posix", platform_machine: "x86_64", platform_python_implementation: "CPython", platform_release: "6.5.0-1016-azure", platform_system: "Linux", platform_version: "#16~22.04.1-Ubuntu SMP Fri Feb 16 15:42:02 UTC 2024", python_full_version: "3.12.2", python_version: "3.12", sys_platform: "linux", })?; // Initialize uv-client let cache = Cache::temp()?.init().await?; let mut builder = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).markers(&markers); let linux = Platform::new( Os::Manylinux { major: 2, minor: 38, }, Arch::X86_64, ); let macos = Platform::new( Os::Macos { major: 14, minor: 4, }, Arch::Aarch64, ); if cfg!(target_os = "linux") { builder = builder.platform(&linux); } else if cfg!(target_os = "macos") { builder = builder.platform(&macos); } let client = builder.build(); // Send request to our dummy server let url = DisplaySafeUrl::from_str(&format!("http://{addr}"))?; let res = client .cached_client() .uncached() .for_host(&url) .get(Url::from(url)) .send() .await?; // Check the HTTP status assert!(res.status().is_success()); // Check User Agent let body = res.text().await?; // Wait for the server task to complete, to be a good citizen. let _ = server_task.await?; // Unpack User-Agent with linehaul let (uv_version, uv_linehaul) = body .split_once(' ') .expect("Failed to split User-Agent header"); // Deserializing Linehaul let linehaul: LineHaul = serde_json::from_str(uv_linehaul)?; // Assert linehaul user agent let filters = vec![(version(), "[VERSION]")]; with_settings!({ filters => filters }, { // Assert uv version assert_snapshot!(uv_version, @"uv/[VERSION]"); // Assert linehaul json assert_json_snapshot!(&linehaul, { ".distro" => "[distro]", ".ci" => "[ci]" }, @r#" { "installer": { "name": "uv", "version": "[VERSION]", "subcommand": null }, "python": "3.12.2", "implementation": { "name": "CPython", "version": "3.12.2" }, "distro": "[distro]", "system": { "name": "Linux", "release": "6.5.0-1016-azure" }, "cpu": "x86_64", "openssl_version": null, "setuptools_version": null, "rustc_version": null, "ci": "[ci]" } "#); }); // Assert distro if cfg!(windows) { assert_json_snapshot!(&linehaul.distro, @"null"); } else if cfg!(target_os = "linux") { assert_json_snapshot!(&linehaul.distro, { ".id" => "[distro.id]", ".name" => "[distro.name]", ".version" => "[distro.version]" // We mock the libc version already }, @r###" { "name": "[distro.name]", "version": "[distro.version]", "id": "[distro.id]", "libc": { "lib": "glibc", "version": "2.38" } }"### ); // Check dynamic values let distro_info = linehaul .distro .expect("got no distro, but expected one in linehaul"); // Gather distribution info from /etc/os-release. let release_info = sys_info::linux_os_release() .expect("got no os release info, but expected one in linux"); assert_eq!(distro_info.id, release_info.version_codename); assert_eq!(distro_info.name, release_info.name); assert_eq!(distro_info.version, release_info.version_id); } else if cfg!(target_os = "macos") { // We mock the macOS distro assert_json_snapshot!(&linehaul.distro, @r###" { "name": "macOS", "version": "14.4", "id": null, "libc": null }"### ); } Ok(()) } uv-0.9.17+ds1/crates/uv-configuration/000077500000000000000000000000001520155276700175175ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-configuration/Cargo.toml000066400000000000000000000023451520155276700214530ustar00rootroot00000000000000[package] name = "uv-configuration" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-auth = { workspace = true } uv-cache = { workspace = true } uv-cache-info = { workspace = true } uv-distribution-types = { workspace = true } uv-git = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true, features = ["schemars"] } uv-platform-tags = { workspace = true } uv-static = { workspace = true } clap = { workspace = true, features = ["derive"], optional = true } either = { workspace = true } fs-err = { workspace = true } rayon = { workspace = true } rustc-hash = { workspace = true } same-file = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true } serde-untagged = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } url = { workspace = true } [dev-dependencies] anyhow = { workspace = true } [features] default = [] uv-0.9.17+ds1/crates/uv-configuration/README.md000066400000000000000000000010431520155276700207740ustar00rootroot00000000000000 # uv-configuration This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-configuration). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-configuration/src/000077500000000000000000000000001520155276700203065ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-configuration/src/authentication.rs000066400000000000000000000024171520155276700236770ustar00rootroot00000000000000use uv_auth::{self, KeyringProvider}; /// Keyring provider type to use for credential lookup. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum KeyringProviderType { /// Do not use keyring for credential lookup. #[default] Disabled, /// Use the `keyring` command for credential lookup. Subprocess, // /// Not yet implemented // Auto, // /// Not implemented yet. Maybe use for this? // Import, } // See for details. impl KeyringProviderType { pub fn to_provider(&self) -> Option { match self { Self::Disabled => None, Self::Subprocess => Some(KeyringProvider::subprocess()), } } } impl std::fmt::Display for KeyringProviderType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Disabled => write!(f, "disabled"), Self::Subprocess => write!(f, "subprocess"), } } } uv-0.9.17+ds1/crates/uv-configuration/src/build_options.rs000066400000000000000000000344431520155276700235360ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use uv_normalize::PackageName; use crate::{PackageNameSpecifier, PackageNameSpecifiers}; #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] pub enum BuildKind { /// A PEP 517 wheel build. #[default] Wheel, /// A PEP 517 source distribution build. Sdist, /// A PEP 660 editable installation wheel build. Editable, } impl Display for BuildKind { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Wheel => f.write_str("wheel"), Self::Sdist => f.write_str("sdist"), Self::Editable => f.write_str("editable"), } } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum BuildOutput { /// Send the build backend output to `stderr`. Stderr, /// Send the build backend output to `tracing`. Debug, /// Do not display the build backend output. Quiet, } #[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct BuildOptions { no_binary: NoBinary, no_build: NoBuild, } impl BuildOptions { pub fn new(no_binary: NoBinary, no_build: NoBuild) -> Self { Self { no_binary, no_build, } } #[must_use] pub fn combine(self, no_binary: NoBinary, no_build: NoBuild) -> Self { Self { no_binary: self.no_binary.combine(no_binary), no_build: self.no_build.combine(no_build), } } pub fn no_binary_package(&self, package_name: &PackageName) -> bool { match &self.no_binary { NoBinary::None => false, NoBinary::All => match &self.no_build { // Allow `all` to be overridden by specific build exclusions NoBuild::Packages(packages) => !packages.contains(package_name), _ => true, }, NoBinary::Packages(packages) => packages.contains(package_name), } } pub fn no_build_package(&self, package_name: &PackageName) -> bool { match &self.no_build { NoBuild::All => match &self.no_binary { // Allow `all` to be overridden by specific binary exclusions NoBinary::Packages(packages) => !packages.contains(package_name), _ => true, }, NoBuild::None => false, NoBuild::Packages(packages) => packages.contains(package_name), } } pub fn no_build_requirement(&self, package_name: Option<&PackageName>) -> bool { match package_name { Some(name) => self.no_build_package(name), None => self.no_build_all(), } } pub fn no_binary_requirement(&self, package_name: Option<&PackageName>) -> bool { match package_name { Some(name) => self.no_binary_package(name), None => self.no_binary_all(), } } pub fn no_build_all(&self) -> bool { matches!(self.no_build, NoBuild::All) } pub fn no_binary_all(&self) -> bool { matches!(self.no_binary, NoBinary::All) } /// Return the [`NoBuild`] strategy to use. pub fn no_build(&self) -> &NoBuild { &self.no_build } /// Return the [`NoBinary`] strategy to use. pub fn no_binary(&self) -> &NoBinary { &self.no_binary } } #[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum NoBinary { /// Allow installation of any wheel. #[default] None, /// Do not allow installation from any wheels. All, /// Do not allow installation from the specific wheels. Packages(Vec), } impl NoBinary { /// Determine the binary installation strategy to use for the given arguments. pub fn from_args(no_binary: Option, no_binary_package: Vec) -> Self { match no_binary { Some(true) => Self::All, Some(false) => Self::None, None => { if no_binary_package.is_empty() { Self::None } else { Self::Packages(no_binary_package) } } } } /// Determine the binary installation strategy to use for the given arguments from the pip CLI. pub fn from_pip_args(no_binary: Vec) -> Self { let combined = PackageNameSpecifiers::from_iter(no_binary.into_iter()); match combined { PackageNameSpecifiers::All => Self::All, PackageNameSpecifiers::None => Self::None, PackageNameSpecifiers::Packages(packages) => Self::Packages(packages), } } /// Determine the binary installation strategy to use for the given argument from the pip CLI. pub fn from_pip_arg(no_binary: PackageNameSpecifier) -> Self { Self::from_pip_args(vec![no_binary]) } /// Combine a set of [`NoBinary`] values. #[must_use] pub fn combine(self, other: Self) -> Self { match (self, other) { // If both are `None`, the result is `None`. (Self::None, Self::None) => Self::None, // If either is `All`, the result is `All`. (Self::All, _) | (_, Self::All) => Self::All, // If one is `None`, the result is the other. (Self::Packages(a), Self::None) => Self::Packages(a), (Self::None, Self::Packages(b)) => Self::Packages(b), // If both are `Packages`, the result is the union of the two. (Self::Packages(mut a), Self::Packages(b)) => { a.extend(b); Self::Packages(a) } } } /// Extend a [`NoBinary`] value with another. pub fn extend(&mut self, other: Self) { match (&mut *self, other) { // If either is `All`, the result is `All`. (Self::All, _) | (_, Self::All) => *self = Self::All, // If both are `None`, the result is `None`. (Self::None, Self::None) => { // Nothing to do. } // If one is `None`, the result is the other. (Self::Packages(_), Self::None) => { // Nothing to do. } (Self::None, Self::Packages(b)) => { // Take ownership of `b`. *self = Self::Packages(b); } // If both are `Packages`, the result is the union of the two. (Self::Packages(a), Self::Packages(b)) => { a.extend(b); } } } } impl NoBinary { /// Returns `true` if all wheels are allowed. pub fn is_none(&self) -> bool { matches!(self, Self::None) } } #[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum NoBuild { /// Allow building wheels from any source distribution. #[default] None, /// Do not allow building wheels from any source distribution. All, /// Do not allow building wheels from the given package's source distributions. Packages(Vec), } impl NoBuild { /// Determine the build strategy to use for the given arguments. pub fn from_args(no_build: Option, no_build_package: Vec) -> Self { match no_build { Some(true) => Self::All, Some(false) => Self::None, None => { if no_build_package.is_empty() { Self::None } else { Self::Packages(no_build_package) } } } } /// Determine the build strategy to use for the given arguments from the pip CLI. pub fn from_pip_args(only_binary: Vec, no_build: bool) -> Self { if no_build { Self::All } else { let combined = PackageNameSpecifiers::from_iter(only_binary.into_iter()); match combined { PackageNameSpecifiers::All => Self::All, PackageNameSpecifiers::None => Self::None, PackageNameSpecifiers::Packages(packages) => Self::Packages(packages), } } } /// Determine the build strategy to use for the given argument from the pip CLI. pub fn from_pip_arg(no_build: PackageNameSpecifier) -> Self { Self::from_pip_args(vec![no_build], false) } /// Combine a set of [`NoBuild`] values. #[must_use] pub fn combine(self, other: Self) -> Self { match (self, other) { // If both are `None`, the result is `None`. (Self::None, Self::None) => Self::None, // If either is `All`, the result is `All`. (Self::All, _) | (_, Self::All) => Self::All, // If one is `None`, the result is the other. (Self::Packages(a), Self::None) => Self::Packages(a), (Self::None, Self::Packages(b)) => Self::Packages(b), // If both are `Packages`, the result is the union of the two. (Self::Packages(mut a), Self::Packages(b)) => { a.extend(b); Self::Packages(a) } } } /// Extend a [`NoBuild`] value with another. pub fn extend(&mut self, other: Self) { match (&mut *self, other) { // If either is `All`, the result is `All`. (Self::All, _) | (_, Self::All) => *self = Self::All, // If both are `None`, the result is `None`. (Self::None, Self::None) => { // Nothing to do. } // If one is `None`, the result is the other. (Self::Packages(_), Self::None) => { // Nothing to do. } (Self::None, Self::Packages(b)) => { // Take ownership of `b`. *self = Self::Packages(b); } // If both are `Packages`, the result is the union of the two. (Self::Packages(a), Self::Packages(b)) => { a.extend(b); } } } } impl NoBuild { /// Returns `true` if all builds are allowed. pub fn is_none(&self) -> bool { matches!(self, Self::None) } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum IndexStrategy { /// Only use results from the first index that returns a match for a given package name. /// /// While this differs from pip's behavior, it's the default index strategy as it's the most /// secure. #[default] #[cfg_attr(feature = "clap", clap(alias = "first-match"))] FirstIndex, /// Search for every package name across all indexes, exhausting the versions from the first /// index before moving on to the next. /// /// In this strategy, we look for every package across all indexes. When resolving, we attempt /// to use versions from the indexes in order, such that we exhaust all available versions from /// the first index before moving on to the next. Further, if a version is found to be /// incompatible in the first index, we do not reconsider that version in subsequent indexes, /// even if the secondary index might contain compatible versions (e.g., variants of the same /// versions with different ABI tags or Python version constraints). /// /// See: #[cfg_attr(feature = "clap", clap(alias = "unsafe-any-match"))] #[serde(alias = "unsafe-any-match")] UnsafeFirstMatch, /// Search for every package name across all indexes, preferring the "best" version found. If a /// package version is in multiple indexes, only look at the entry for the first index. /// /// In this strategy, we look for every package across all indexes. When resolving, we consider /// all versions from all indexes, choosing the "best" version found (typically, the highest /// compatible version). /// /// This most closely matches pip's behavior, but exposes the resolver to "dependency confusion" /// attacks whereby malicious actors can publish packages to public indexes with the same name /// as internal packages, causing the resolver to install the malicious package in lieu of /// the intended internal package. /// /// See: UnsafeBestMatch, } #[cfg(test)] mod tests { use std::str::FromStr; use anyhow::Error; use super::*; #[test] fn no_build_from_args() -> Result<(), Error> { assert_eq!( NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":all:")?], false), NoBuild::All, ); assert_eq!( NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":all:")?], true), NoBuild::All, ); assert_eq!( NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":none:")?], true), NoBuild::All, ); assert_eq!( NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":none:")?], false), NoBuild::None, ); assert_eq!( NoBuild::from_pip_args( vec![ PackageNameSpecifier::from_str("foo")?, PackageNameSpecifier::from_str("bar")? ], false ), NoBuild::Packages(vec![ PackageName::from_str("foo")?, PackageName::from_str("bar")? ]), ); assert_eq!( NoBuild::from_pip_args( vec![ PackageNameSpecifier::from_str("test")?, PackageNameSpecifier::All ], false ), NoBuild::All, ); assert_eq!( NoBuild::from_pip_args( vec![ PackageNameSpecifier::from_str("foo")?, PackageNameSpecifier::from_str(":none:")?, PackageNameSpecifier::from_str("bar")? ], false ), NoBuild::Packages(vec![PackageName::from_str("bar")?]), ); Ok(()) } } uv-0.9.17+ds1/crates/uv-configuration/src/concurrency.rs000066400000000000000000000020021520155276700232000ustar00rootroot00000000000000use std::num::NonZeroUsize; /// Concurrency limit settings. #[derive(Copy, Clone, Debug)] pub struct Concurrency { /// The maximum number of concurrent downloads. /// /// Note this value must be non-zero. pub downloads: usize, /// The maximum number of concurrent builds. /// /// Note this value must be non-zero. pub builds: usize, /// The maximum number of concurrent installs. /// /// Note this value must be non-zero. pub installs: usize, } impl Default for Concurrency { fn default() -> Self { Self { downloads: Self::DEFAULT_DOWNLOADS, builds: Self::threads(), installs: Self::threads(), } } } impl Concurrency { // The default concurrent downloads limit. pub const DEFAULT_DOWNLOADS: usize = 50; // The default concurrent builds and install limit. pub fn threads() -> usize { std::thread::available_parallelism() .map(NonZeroUsize::get) .unwrap_or(1) } } uv-0.9.17+ds1/crates/uv-configuration/src/constraints.rs000066400000000000000000000066371520155276700232370ustar00rootroot00000000000000use std::borrow::Cow; use either::Either; use rustc_hash::FxHashMap; use uv_distribution_types::{Requirement, RequirementSource}; use uv_normalize::PackageName; use uv_pep508::MarkerTree; /// A set of constraints for a set of requirements. #[derive(Debug, Default, Clone)] pub struct Constraints(FxHashMap>); impl Constraints { /// Create a new set of constraints from a set of requirements. pub fn from_requirements(requirements: impl Iterator) -> Self { let mut constraints: FxHashMap> = FxHashMap::default(); for requirement in requirements { // Skip empty constraints. if let RequirementSource::Registry { specifier, .. } = &requirement.source { if specifier.is_empty() { continue; } } constraints .entry(requirement.name.clone()) .or_default() .push(Requirement { // We add and apply constraints independent of their extras. extras: Box::new([]), ..requirement }); } Self(constraints) } /// Return an iterator over all [`Requirement`]s in the constraint set. pub fn requirements(&self) -> impl Iterator { self.0.values().flat_map(|requirements| requirements.iter()) } /// Get the constraints for a package. pub fn get(&self, name: &PackageName) -> Option<&Vec> { self.0.get(name) } /// Apply the constraints to a set of requirements. /// /// NB: Change this method together with [`Overrides::apply`]. pub fn apply<'a>( &'a self, requirements: impl IntoIterator>, ) -> impl Iterator> { requirements.into_iter().flat_map(|requirement| { let Some(constraints) = self.get(&requirement.name) else { // Case 1: No constraint(s). return Either::Left(std::iter::once(requirement)); }; // ASSUMPTION: There is one `extra = "..."`, and it's either the only marker or part // of the main conjunction. let Some(extra_expression) = requirement.marker.top_level_extra() else { // Case 2: A non-optional dependency with constraint(s). return Either::Right(Either::Right( std::iter::once(requirement).chain(constraints.iter().map(Cow::Borrowed)), )); }; // Case 3: An optional dependency with constraint(s). // // When the original requirement is an optional dependency, the constraint(s) need to // be optional for the same extra, otherwise we activate extras that should be inactive. Either::Right(Either::Left(std::iter::once(requirement).chain( constraints.iter().cloned().map(move |constraint| { // Add the extra to the override marker. let mut joint_marker = MarkerTree::expression(extra_expression.clone()); joint_marker.and(constraint.marker); Cow::Owned(Requirement { marker: joint_marker, ..constraint }) }), ))) }) } } uv-0.9.17+ds1/crates/uv-configuration/src/dependency_groups.rs000066400000000000000000000323501520155276700243740ustar00rootroot00000000000000use std::{borrow::Cow, sync::Arc}; use uv_normalize::{DEV_DEPENDENCIES, DefaultGroups, GroupName}; /// Manager of all dependency-group decisions and settings history. /// /// This is an Arc mostly just to avoid size bloat on things that contain these. #[derive(Debug, Default, Clone)] pub struct DependencyGroups(Arc); /// Manager of all dependency-group decisions and settings history. #[derive(Debug, Default, Clone)] pub struct DependencyGroupsInner { /// Groups to include. include: IncludeGroups, /// Groups to exclude (always wins over include). exclude: Vec, /// Whether an `--only` flag was passed. /// /// If true, users of this API should refrain from looking at packages /// that *aren't* specified by the dependency-groups. This is exposed /// via [`DependencyGroupsInner::prod`][]. only_groups: bool, /// The "raw" flags/settings we were passed for diagnostics. history: DependencyGroupsHistory, } impl DependencyGroups { /// Create from history. /// /// This is the "real" constructor, it's basically taking raw CLI flags but in /// a way that's a bit nicer for other constructors to use. fn from_history(history: DependencyGroupsHistory) -> Self { let DependencyGroupsHistory { dev_mode, mut group, mut only_group, mut no_group, all_groups, no_default_groups, mut defaults, } = history.clone(); // First desugar --dev flags match dev_mode { Some(DevMode::Include) => group.push(DEV_DEPENDENCIES.clone()), Some(DevMode::Only) => only_group.push(DEV_DEPENDENCIES.clone()), Some(DevMode::Exclude) => no_group.push(DEV_DEPENDENCIES.clone()), None => {} } // `group` and `only_group` actually have the same meanings: packages to include. // But if `only_group` is non-empty then *other* packages should be excluded. // So we just record whether it was and then treat the two lists as equivalent. let only_groups = !only_group.is_empty(); // --only flags imply --no-default-groups let default_groups = !no_default_groups && !only_groups; let include = if all_groups { // If this is set we can ignore group/only_group/defaults as irrelevant // (`--all-groups --only-*` is rejected at the CLI level, don't worry about it). IncludeGroups::All } else { // Merge all these lists, they're equivalent now group.append(&mut only_group); // Resolve default groups potentially also setting All if default_groups { match &mut defaults { DefaultGroups::All => IncludeGroups::All, DefaultGroups::List(defaults) => { group.append(defaults); IncludeGroups::Some(group) } } } else { IncludeGroups::Some(group) } }; Self(Arc::new(DependencyGroupsInner { include, exclude: no_group, only_groups, history, })) } /// Create from raw CLI args #[allow(clippy::fn_params_excessive_bools)] pub fn from_args( dev: bool, no_dev: bool, only_dev: bool, group: Vec, no_group: Vec, no_default_groups: bool, only_group: Vec, all_groups: bool, ) -> Self { // Lower the --dev flags into a single dev mode. // // In theory only one of these 3 flags should be set (enforced by CLI), // but we explicitly allow `--dev` and `--only-dev` to both be set, // and "saturate" that to `--only-dev`. let dev_mode = if only_dev { Some(DevMode::Only) } else if no_dev { Some(DevMode::Exclude) } else if dev { Some(DevMode::Include) } else { None }; Self::from_history(DependencyGroupsHistory { dev_mode, group, only_group, no_group, all_groups, no_default_groups, // This is unknown at CLI-time, use `.with_defaults(...)` to apply this later! defaults: DefaultGroups::default(), }) } /// Helper to make a spec from just a --dev flag pub fn from_dev_mode(dev_mode: DevMode) -> Self { Self::from_history(DependencyGroupsHistory { dev_mode: Some(dev_mode), ..Default::default() }) } /// Helper to make a spec from just a --group pub fn from_group(group: GroupName) -> Self { Self::from_history(DependencyGroupsHistory { group: vec![group], ..Default::default() }) } /// Apply defaults to a base [`DependencyGroups`]. /// /// This is appropriate in projects, where the `dev` group is synced by default. pub fn with_defaults(&self, defaults: DefaultGroups) -> DependencyGroupsWithDefaults { // Explicitly clone the inner history and set the defaults, then remake the result. let mut history = self.0.history.clone(); history.defaults = defaults; DependencyGroupsWithDefaults { cur: Self::from_history(history), prev: self.clone(), } } } impl std::ops::Deref for DependencyGroups { type Target = DependencyGroupsInner; fn deref(&self) -> &Self::Target { &self.0 } } impl DependencyGroupsInner { /// Returns `true` if packages other than the ones referenced by these /// dependency-groups should be considered. /// /// That is, if I tell you to install a project and this is false, /// you should ignore the project itself and all its dependencies, /// and instead just install the dependency-groups. /// /// (This is really just asking if an --only flag was passed.) pub fn prod(&self) -> bool { !self.only_groups } /// Returns `true` if the specification includes the given group. pub fn contains(&self, group: &GroupName) -> bool { // exclude always trumps include !self.exclude.contains(group) && self.include.contains(group) } /// Iterate over all groups that we think should exist. pub fn desugarred_names(&self) -> impl Iterator { self.include.names().chain(&self.exclude) } /// Returns an iterator over all groups that are included in the specification, /// assuming `all_names` is an iterator over all groups. pub fn group_names<'a, Names>( &'a self, all_names: Names, ) -> impl Iterator + 'a where Names: Iterator + 'a, { all_names.filter(move |name| self.contains(name)) } /// Iterate over all groups the user explicitly asked for on the CLI pub fn explicit_names(&self) -> impl Iterator { let DependencyGroupsHistory { // Strictly speaking this is an explicit reference to "dev" // but we're currently tolerant of dev not existing when referenced with // these flags, since it kinda implicitly always exists even if // it's not properly defined in a config file. dev_mode: _, group, only_group, no_group, // These reference no groups explicitly all_groups: _, no_default_groups: _, // This doesn't include defaults because the `dev` group may not be defined // but gets implicitly added as a default sometimes! defaults: _, } = self.history(); group.iter().chain(no_group).chain(only_group) } /// Returns `true` if the specification will have no effect. pub fn is_empty(&self) -> bool { self.prod() && self.exclude.is_empty() && self.include.is_empty() } /// Get the raw history for diagnostics pub fn history(&self) -> &DependencyGroupsHistory { &self.history } } /// Context about a [`DependencyGroups`][] that we've preserved for diagnostics #[derive(Debug, Default, Clone)] pub struct DependencyGroupsHistory { pub dev_mode: Option, pub group: Vec, pub only_group: Vec, pub no_group: Vec, pub all_groups: bool, pub no_default_groups: bool, pub defaults: DefaultGroups, } impl DependencyGroupsHistory { /// Returns all the CLI flags that this represents. /// /// If a flag was provided multiple times (e.g. `--group A --group B`) this will /// elide the arguments and just show the flag once (e.g. just yield "--group"). /// /// Conceptually this being an empty list should be equivalent to /// [`DependencyGroups::is_empty`][] when there aren't any defaults set. /// When there are defaults the two will disagree, and rightfully so! pub fn as_flags_pretty(&self) -> Vec> { let Self { dev_mode, group, only_group, no_group, all_groups, no_default_groups, // defaults aren't CLI flags! defaults: _, } = self; let mut flags = vec![]; if *all_groups { flags.push(Cow::Borrowed("--all-groups")); } if *no_default_groups { flags.push(Cow::Borrowed("--no-default-groups")); } if let Some(dev_mode) = dev_mode { flags.push(Cow::Borrowed(dev_mode.as_flag())); } match &**group { [] => {} [group] => flags.push(Cow::Owned(format!("--group {group}"))), [..] => flags.push(Cow::Borrowed("--group")), } match &**only_group { [] => {} [group] => flags.push(Cow::Owned(format!("--only-group {group}"))), [..] => flags.push(Cow::Borrowed("--only-group")), } match &**no_group { [] => {} [group] => flags.push(Cow::Owned(format!("--no-group {group}"))), [..] => flags.push(Cow::Borrowed("--no-group")), } flags } } /// A trivial newtype wrapped around [`DependencyGroups`][] that signifies "defaults applied" /// /// It includes a copy of the previous semantics to provide info on if /// the group being a default actually affected it being enabled, because it's obviously "correct". /// (These are Arcs so it's ~free to hold onto the previous semantics) #[derive(Debug, Clone)] pub struct DependencyGroupsWithDefaults { /// The active semantics cur: DependencyGroups, /// The semantics before defaults were applied prev: DependencyGroups, } impl DependencyGroupsWithDefaults { /// Do not enable any groups /// /// Many places in the code need to know what dependency-groups are active, /// but various commands or subsystems never enable any dependency-groups, /// in which case they want this. pub fn none() -> Self { DependencyGroups::default().with_defaults(DefaultGroups::default()) } /// Returns `true` if the specification was enabled, and *only* because it was a default pub fn contains_because_default(&self, group: &GroupName) -> bool { self.cur.contains(group) && !self.prev.contains(group) } } impl std::ops::Deref for DependencyGroupsWithDefaults { type Target = DependencyGroups; fn deref(&self) -> &Self::Target { &self.cur } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum DevMode { /// Include development dependencies. #[default] Include, /// Exclude development dependencies. Exclude, /// Only include development dependencies, excluding all other dependencies. Only, } impl DevMode { /// Returns the flag that was used to request development dependencies. pub fn as_flag(&self) -> &'static str { match self { Self::Exclude => "--no-dev", Self::Include => "--dev", Self::Only => "--only-dev", } } } #[derive(Debug, Clone)] pub enum IncludeGroups { /// Include dependencies from the specified groups. Some(Vec), /// A marker indicates including dependencies from all groups. All, } impl IncludeGroups { /// Returns `true` if the specification includes the given group. pub fn contains(&self, group: &GroupName) -> bool { match self { Self::Some(groups) => groups.contains(group), Self::All => true, } } /// Returns `true` if the specification will have no effect. pub fn is_empty(&self) -> bool { match self { Self::Some(groups) => groups.is_empty(), // Although technically this is a noop if they have no groups, // conceptually they're *trying* to have an effect, so treat it as one. Self::All => false, } } /// Iterate over all groups referenced in the [`IncludeGroups`]. pub fn names(&self) -> std::slice::Iter<'_, GroupName> { match self { Self::Some(groups) => groups.iter(), Self::All => [].iter(), } } } impl Default for IncludeGroups { fn default() -> Self { Self::Some(Vec::new()) } } uv-0.9.17+ds1/crates/uv-configuration/src/dry_run.rs000066400000000000000000000013701520155276700223370ustar00rootroot00000000000000#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum DryRun { /// The operation should execute in dry run mode. Enabled, /// The operation should execute in dry run mode and check if the current environment is /// synced. Check, /// The operation should execute in normal mode. #[default] Disabled, } impl DryRun { /// Determine the [`DryRun`] setting based on the command-line arguments. pub fn from_args(dry_run: bool) -> Self { if dry_run { Self::Enabled } else { Self::Disabled } } /// Returns `true` if dry run mode is enabled. pub const fn enabled(&self) -> bool { matches!(self, Self::Enabled) || matches!(self, Self::Check) } } uv-0.9.17+ds1/crates/uv-configuration/src/editable.rs000066400000000000000000000004641520155276700224310ustar00rootroot00000000000000#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum EditableMode { #[default] Editable, NonEditable, } impl From for EditableMode { fn from(value: bool) -> Self { if value { Self::Editable } else { Self::NonEditable } } } uv-0.9.17+ds1/crates/uv-configuration/src/env_file.rs000066400000000000000000000075541520155276700224560ustar00rootroot00000000000000use std::path::PathBuf; /// A collection of `.env` file paths. #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct EnvFile(Vec); impl EnvFile { /// Parse the env file paths from command-line arguments. pub fn from_args(env_file: Vec, no_env_file: bool) -> Self { if no_env_file { return Self::default(); } if env_file.is_empty() { return Self::default(); } let mut paths = Vec::new(); // Split on spaces, but respect backslashes. for env_file in env_file { let mut current = String::new(); let mut escape = false; for c in env_file.chars() { if escape { current.push(c); escape = false; } else if c == '\\' { escape = true; } else if c.is_whitespace() { if !current.is_empty() { paths.push(PathBuf::from(current)); current = String::new(); } } else { current.push(c); } } if !current.is_empty() { paths.push(PathBuf::from(current)); } } Self(paths) } /// Iterate over the paths in the env file. pub fn iter(&self) -> impl DoubleEndedIterator { self.0.iter() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_from_args_default() { let env_file = EnvFile::from_args(vec![], false); assert_eq!(env_file, EnvFile::default()); } #[test] fn test_from_args_no_env_file() { let env_file = EnvFile::from_args(vec!["path1 path2".to_string()], true); assert_eq!(env_file, EnvFile::default()); } #[test] fn test_from_args_empty_string() { let env_file = EnvFile::from_args(vec![String::new()], false); assert_eq!(env_file, EnvFile::default()); } #[test] fn test_from_args_whitespace_only() { let env_file = EnvFile::from_args(vec![" ".to_string()], false); assert_eq!(env_file, EnvFile::default()); } #[test] fn test_from_args_single_path() { let env_file = EnvFile::from_args(vec!["path1".to_string()], false); assert_eq!(env_file.0, vec![PathBuf::from("path1")]); } #[test] fn test_from_args_multiple_paths() { let env_file = EnvFile::from_args(vec!["path1 path2 path3".to_string()], false); assert_eq!( env_file.0, vec![ PathBuf::from("path1"), PathBuf::from("path2"), PathBuf::from("path3") ] ); } #[test] fn test_from_args_escaped_spaces() { let env_file = EnvFile::from_args(vec![r"path\ with\ spaces".to_string()], false); assert_eq!(env_file.0, vec![PathBuf::from("path with spaces")]); } #[test] fn test_from_args_mixed_escaped_and_normal() { let env_file = EnvFile::from_args(vec![r"path1 path\ with\ spaces path2".to_string()], false); assert_eq!( env_file.0, vec![ PathBuf::from("path1"), PathBuf::from("path with spaces"), PathBuf::from("path2") ] ); } #[test] fn test_from_args_escaped_backslash() { let env_file = EnvFile::from_args(vec![r"path\\with\\backslashes".to_string()], false); assert_eq!(env_file.0, vec![PathBuf::from(r"path\with\backslashes")]); } #[test] fn test_iter() { let env_file = EnvFile(vec![PathBuf::from("path1"), PathBuf::from("path2")]); let paths: Vec<_> = env_file.iter().collect(); assert_eq!( paths, vec![&PathBuf::from("path1"), &PathBuf::from("path2")] ); } } uv-0.9.17+ds1/crates/uv-configuration/src/excludes.rs000066400000000000000000000012411520155276700224660ustar00rootroot00000000000000use rustc_hash::FxHashSet; use uv_normalize::PackageName; /// A set of packages to exclude from resolution. #[derive(Debug, Default, Clone)] pub struct Excludes(FxHashSet); impl Excludes { /// Return an iterator over all package names in the exclusion set. pub fn iter(&self) -> impl Iterator { self.0.iter() } /// Check if a package is excluded. pub fn contains(&self, name: &PackageName) -> bool { self.0.contains(name) } } impl FromIterator for Excludes { fn from_iter>(iter: I) -> Self { Self(iter.into_iter().collect()) } } uv-0.9.17+ds1/crates/uv-configuration/src/export_format.rs000066400000000000000000000032761520155276700235550ustar00rootroot00000000000000/// The format to use when exporting a `uv.lock` file. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum ExportFormat { /// Export in `requirements.txt` format. #[default] #[serde(rename = "requirements.txt", alias = "requirements-txt")] #[cfg_attr( feature = "clap", clap(name = "requirements.txt", alias = "requirements-txt") )] RequirementsTxt, /// Export in `pylock.toml` format. #[serde(rename = "pylock.toml", alias = "pylock-toml")] #[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))] PylockToml, /// Export in `CycloneDX` v1.5 JSON format. #[serde(rename = "cyclonedx1.5")] #[cfg_attr( feature = "clap", clap(name = "cyclonedx1.5", alias = "cyclonedx1.5+json") )] CycloneDX1_5, } /// The output format to use in `uv pip compile`. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum PipCompileFormat { /// Export in `requirements.txt` format. #[default] #[serde(rename = "requirements.txt", alias = "requirements-txt")] #[cfg_attr( feature = "clap", clap(name = "requirements.txt", alias = "requirements-txt") )] RequirementsTxt, /// Export in `pylock.toml` format. #[serde(rename = "pylock.toml", alias = "pylock-toml")] #[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))] PylockToml, } uv-0.9.17+ds1/crates/uv-configuration/src/extras.rs000066400000000000000000000256341520155276700221740ustar00rootroot00000000000000use std::{borrow::Cow, sync::Arc}; use uv_normalize::{DefaultExtras, ExtraName}; /// Manager of all extra decisions and settings history. /// /// This is an Arc mostly just to avoid size bloat on things that contain these. #[derive(Debug, Default, Clone)] pub struct ExtrasSpecification(Arc); /// Manager of all dependency-group decisions and settings history. #[derive(Debug, Default, Clone)] pub struct ExtrasSpecificationInner { /// Extras to include. include: IncludeExtras, /// Extras to exclude (always wins over include). exclude: Vec, /// Whether an `--only` flag was passed. /// /// If true, users of this API should refrain from looking at packages /// that *aren't* specified by the extras. This is exposed /// via [`ExtrasSpecificationInner::prod`][]. only_extras: bool, /// The "raw" flags/settings we were passed for diagnostics. history: ExtrasSpecificationHistory, } impl ExtrasSpecification { /// Create from history. /// /// This is the "real" constructor, it's basically taking raw CLI flags but in /// a way that's a bit nicer for other constructors to use. fn from_history(history: ExtrasSpecificationHistory) -> Self { let ExtrasSpecificationHistory { mut extra, mut only_extra, no_extra, all_extras, no_default_extras, mut defaults, } = history.clone(); // `extra` and `only_extra` actually have the same meanings: packages to include. // But if `only_extra` is non-empty then *other* packages should be excluded. // So we just record whether it was and then treat the two lists as equivalent. let only_extras = !only_extra.is_empty(); // --only flags imply --no-default-extras let default_extras = !no_default_extras && !only_extras; let include = if all_extras { // If this is set we can ignore extra/only_extra/defaults as irrelevant. IncludeExtras::All } else { // Merge all these lists, they're equivalent now extra.append(&mut only_extra); // Resolve default extras potentially also setting All if default_extras { match &mut defaults { DefaultExtras::All => IncludeExtras::All, DefaultExtras::List(defaults) => { extra.append(defaults); IncludeExtras::Some(extra) } } } else { IncludeExtras::Some(extra) } }; Self(Arc::new(ExtrasSpecificationInner { include, exclude: no_extra, only_extras, history, })) } /// Create from raw CLI args #[allow(clippy::fn_params_excessive_bools)] pub fn from_args( extra: Vec, no_extra: Vec, no_default_extras: bool, only_extra: Vec, all_extras: bool, ) -> Self { Self::from_history(ExtrasSpecificationHistory { extra, only_extra, no_extra, all_extras, no_default_extras, // This is unknown at CLI-time, use `.with_defaults(...)` to apply this later! defaults: DefaultExtras::default(), }) } /// Helper to make a spec from just a --extra pub fn from_extra(extra: Vec) -> Self { Self::from_history(ExtrasSpecificationHistory { extra, ..Default::default() }) } /// Helper to make a spec from just --all-extras pub fn from_all_extras() -> Self { Self::from_history(ExtrasSpecificationHistory { all_extras: true, ..Default::default() }) } /// Apply defaults to a base [`ExtrasSpecification`]. pub fn with_defaults(&self, defaults: DefaultExtras) -> ExtrasSpecificationWithDefaults { // Explicitly clone the inner history and set the defaults, then remake the result. let mut history = self.0.history.clone(); history.defaults = defaults; ExtrasSpecificationWithDefaults { cur: Self::from_history(history), prev: self.clone(), } } } impl std::ops::Deref for ExtrasSpecification { type Target = ExtrasSpecificationInner; fn deref(&self) -> &Self::Target { &self.0 } } impl ExtrasSpecificationInner { /// Returns `true` if packages other than the ones referenced by these /// extras should be considered. /// /// That is, if I tell you to install a project and this is false, /// you should ignore the project itself and all its dependencies, /// and instead just install the extras. /// /// (This is really just asking if an --only flag was passed.) pub fn prod(&self) -> bool { !self.only_extras } /// Returns `true` if the specification includes the given extra. pub fn contains(&self, extra: &ExtraName) -> bool { // exclude always trumps include !self.exclude.contains(extra) && self.include.contains(extra) } /// Iterate over all extras that we think should exist. pub fn desugarred_names(&self) -> impl Iterator { self.include.names().chain(&self.exclude) } /// Returns an iterator over all extras that are included in the specification, /// assuming `all_names` is an iterator over all extras. pub fn extra_names<'a, Names>( &'a self, all_names: Names, ) -> impl Iterator + 'a where Names: Iterator + 'a, { all_names.filter(move |name| self.contains(name)) } /// Iterate over all groups the user explicitly asked for on the CLI pub fn explicit_names(&self) -> impl Iterator { let ExtrasSpecificationHistory { extra, only_extra, no_extra, // These reference no extras explicitly all_extras: _, no_default_extras: _, defaults: _, } = self.history(); extra.iter().chain(no_extra).chain(only_extra) } /// Returns `true` if the specification will have no effect. pub fn is_empty(&self) -> bool { self.prod() && self.exclude.is_empty() && self.include.is_empty() } /// Get the raw history for diagnostics pub fn history(&self) -> &ExtrasSpecificationHistory { &self.history } } /// Context about a [`ExtrasSpecification`][] that we've preserved for diagnostics #[derive(Debug, Default, Clone)] pub struct ExtrasSpecificationHistory { pub extra: Vec, pub only_extra: Vec, pub no_extra: Vec, pub all_extras: bool, pub no_default_extras: bool, pub defaults: DefaultExtras, } impl ExtrasSpecificationHistory { /// Returns all the CLI flags that this represents. /// /// If a flag was provided multiple times (e.g. `--extra A --extra B`) this will /// elide the arguments and just show the flag once (e.g. just yield "--extra"). /// /// Conceptually this being an empty list should be equivalent to /// [`ExtrasSpecification::is_empty`][] when there aren't any defaults set. /// When there are defaults the two will disagree, and rightfully so! pub fn as_flags_pretty(&self) -> Vec> { let Self { extra, no_extra, all_extras, only_extra, no_default_extras, // defaults aren't CLI flags! defaults: _, } = self; let mut flags = vec![]; if *all_extras { flags.push(Cow::Borrowed("--all-extras")); } if *no_default_extras { flags.push(Cow::Borrowed("--no-default-extras")); } match &**extra { [] => {} [extra] => flags.push(Cow::Owned(format!("--extra {extra}"))), [..] => flags.push(Cow::Borrowed("--extra")), } match &**only_extra { [] => {} [extra] => flags.push(Cow::Owned(format!("--only-extra {extra}"))), [..] => flags.push(Cow::Borrowed("--only-extra")), } match &**no_extra { [] => {} [extra] => flags.push(Cow::Owned(format!("--no-extra {extra}"))), [..] => flags.push(Cow::Borrowed("--no-extra")), } flags } } /// A trivial newtype wrapped around [`ExtrasSpecification`][] that signifies "defaults applied" /// /// It includes a copy of the previous semantics to provide info on if /// the group being a default actually affected it being enabled, because it's obviously "correct". /// (These are Arcs so it's ~free to hold onto the previous semantics) #[derive(Debug, Clone)] pub struct ExtrasSpecificationWithDefaults { /// The active semantics cur: ExtrasSpecification, /// The semantics before defaults were applied prev: ExtrasSpecification, } impl ExtrasSpecificationWithDefaults { /// Do not enable any extras /// /// Many places in the code need to know what extras are active, /// but various commands or subsystems never enable any extras, /// in which case they want this. pub fn none() -> Self { ExtrasSpecification::default().with_defaults(DefaultExtras::default()) } /// Returns `true` if the specification was enabled, and *only* because it was a default pub fn contains_because_default(&self, extra: &ExtraName) -> bool { self.cur.contains(extra) && !self.prev.contains(extra) } } impl std::ops::Deref for ExtrasSpecificationWithDefaults { type Target = ExtrasSpecification; fn deref(&self) -> &Self::Target { &self.cur } } #[derive(Debug, Clone)] pub enum IncludeExtras { /// Include dependencies from the specified extras. Some(Vec), /// A marker indicates including dependencies from all extras. All, } impl IncludeExtras { /// Returns `true` if the specification includes the given extra. pub fn contains(&self, extra: &ExtraName) -> bool { match self { Self::Some(extras) => extras.contains(extra), Self::All => true, } } /// Returns `true` if the specification will have no effect. pub fn is_empty(&self) -> bool { match self { Self::Some(extras) => extras.is_empty(), // Although technically this is a noop if they have no extras, // conceptually they're *trying* to have an effect, so treat it as one. Self::All => false, } } /// Iterate over all extras referenced in the [`IncludeExtras`]. pub fn names(&self) -> std::slice::Iter<'_, ExtraName> { match self { Self::Some(extras) => extras.iter(), Self::All => [].iter(), } } } impl Default for IncludeExtras { fn default() -> Self { Self::Some(Vec::new()) } } uv-0.9.17+ds1/crates/uv-configuration/src/hash.rs000066400000000000000000000037351520155276700216070ustar00rootroot00000000000000#[derive(Debug, Copy, Clone)] pub enum HashCheckingMode { /// Hashes should be validated against a pre-defined list of hashes. Every requirement must /// itself be hashable (e.g., Git dependencies are forbidden) _and_ have a hash in the lockfile. Require, /// Hashes should be validated, if present, but ignored if absent. Verify, } impl HashCheckingMode { /// Return the [`HashCheckingMode`] from the command-line arguments, if any. /// /// By default, the hash checking mode is [`HashCheckingMode::Verify`]. If `--require-hashes` is /// passed, the hash checking mode is [`HashCheckingMode::Require`]. If `--no-verify-hashes` is /// passed, then no hash checking is performed. pub fn from_args(require_hashes: Option, verify_hashes: Option) -> Option { if require_hashes == Some(true) { // Given `--require-hashes`, always require hashes, regardless of any other flags. Some(Self::Require) } else if verify_hashes == Some(true) { // Given `--verify-hashes`, always verify hashes, regardless of any other flags. Some(Self::Verify) } else if verify_hashes == Some(false) { // Given `--no-verify-hashes` (without `--require-hashes`), do not verify hashes. None } else if require_hashes == Some(false) { // Given `--no-require-hashes` (without `--verify-hashes`), do not require hashes. None } else { // By default, verify hashes. Some(Self::Verify) } } /// Returns `true` if the hash checking mode is `Require`. pub fn is_require(&self) -> bool { matches!(self, Self::Require) } } impl std::fmt::Display for HashCheckingMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Require => write!(f, "--require-hashes"), Self::Verify => write!(f, "--verify-hashes"), } } } uv-0.9.17+ds1/crates/uv-configuration/src/install_options.rs000066400000000000000000000136131520155276700241010ustar00rootroot00000000000000use std::collections::BTreeSet; use tracing::debug; use uv_normalize::PackageName; /// Minimal view of a package used to apply install filters. #[derive(Debug, Clone, Copy)] pub struct InstallTarget<'a> { /// The package name. pub name: &'a PackageName, /// Whether the package refers to a local source (path, directory, editable, etc.). pub is_local: bool, } #[derive(Debug, Clone, Default)] pub struct InstallOptions { /// Omit the project itself from the resolution. pub no_install_project: bool, /// Include only the project itself in the resolution. pub only_install_project: bool, /// Omit all workspace members (including the project itself) from the resolution. pub no_install_workspace: bool, /// Include only workspace members (including the project itself) in the resolution. pub only_install_workspace: bool, /// Omit all local packages from the resolution. pub no_install_local: bool, /// Include only local packages in the resolution. pub only_install_local: bool, /// Omit the specified packages from the resolution. pub no_install_package: Vec, /// Include only the specified packages in the resolution. pub only_install_package: Vec, } impl InstallOptions { #[allow(clippy::fn_params_excessive_bools)] pub fn new( no_install_project: bool, only_install_project: bool, no_install_workspace: bool, only_install_workspace: bool, no_install_local: bool, only_install_local: bool, no_install_package: Vec, only_install_package: Vec, ) -> Self { Self { no_install_project, only_install_project, no_install_workspace, only_install_workspace, no_install_local, only_install_local, no_install_package, only_install_package, } } /// Returns `true` if a package passes the install filters. pub fn include_package( &self, target: InstallTarget<'_>, project_name: Option<&PackageName>, members: &BTreeSet, ) -> bool { let package_name = target.name; // If `--only-install-package` is set, only include specified packages. if !self.only_install_package.is_empty() { if self.only_install_package.contains(package_name) { return true; } debug!("Omitting `{package_name}` from resolution due to `--only-install-package`"); return false; } // If `--only-install-local` is set, only include local packages. if self.only_install_local { if target.is_local { return true; } debug!("Omitting `{package_name}` from resolution due to `--only-install-local`"); return false; } // If `--only-install-workspace` is set, only include the project and workspace members. if self.only_install_workspace { // Check if it's the project itself if let Some(project_name) = project_name { if package_name == project_name { return true; } } // Check if it's a workspace member if members.contains(package_name) { return true; } // Otherwise, exclude it debug!("Omitting `{package_name}` from resolution due to `--only-install-workspace`"); return false; } // If `--only-install-project` is set, only include the project itself. if self.only_install_project { if let Some(project_name) = project_name { if package_name == project_name { return true; } } debug!("Omitting `{package_name}` from resolution due to `--only-install-project`"); return false; } // If `--no-install-project` is set, remove the project itself. if self.no_install_project { if let Some(project_name) = project_name { if package_name == project_name { debug!( "Omitting `{package_name}` from resolution due to `--no-install-project`" ); return false; } } } // If `--no-install-workspace` is set, remove the project and any workspace members. if self.no_install_workspace { // In some cases, the project root might be omitted from the list of workspace members // encoded in the lockfile. (But we already checked this above if `--no-install-project` // is set.) if !self.no_install_project { if let Some(project_name) = project_name { if package_name == project_name { debug!( "Omitting `{package_name}` from resolution due to `--no-install-workspace`" ); return false; } } } if members.contains(package_name) { debug!("Omitting `{package_name}` from resolution due to `--no-install-workspace`"); return false; } } // If `--no-install-local` is set, remove local packages. if self.no_install_local { if target.is_local { debug!("Omitting `{package_name}` from resolution due to `--no-install-local`"); return false; } } // If `--no-install-package` is provided, remove the requested packages. if self.no_install_package.contains(package_name) { debug!("Omitting `{package_name}` from resolution due to `--no-install-package`"); return false; } true } } uv-0.9.17+ds1/crates/uv-configuration/src/lib.rs000066400000000000000000000017531520155276700214300ustar00rootroot00000000000000pub use authentication::*; pub use build_options::*; pub use concurrency::*; pub use constraints::*; pub use dependency_groups::*; pub use dry_run::*; pub use editable::*; pub use env_file::*; pub use excludes::*; pub use export_format::*; pub use extras::*; pub use hash::*; pub use install_options::*; pub use name_specifiers::*; pub use overrides::*; pub use package_options::*; pub use project_build_backend::*; pub use required_version::*; pub use sources::*; pub use target_triple::*; pub use threading::*; pub use trusted_host::*; pub use trusted_publishing::*; pub use vcs::*; mod authentication; mod build_options; mod concurrency; mod constraints; mod dependency_groups; mod dry_run; mod editable; mod env_file; mod excludes; mod export_format; mod extras; mod hash; mod install_options; mod name_specifiers; mod overrides; mod package_options; mod project_build_backend; mod required_version; mod sources; mod target_triple; mod threading; mod trusted_host; mod trusted_publishing; mod vcs; uv-0.9.17+ds1/crates/uv-configuration/src/name_specifiers.rs000066400000000000000000000071251520155276700240150ustar00rootroot00000000000000#[cfg(feature = "schemars")] use std::borrow::Cow; use std::str::FromStr; use uv_normalize::PackageName; /// A specifier used for (e.g.) pip's `--no-binary` flag. /// /// This is a superset of the package name format, allowing for special values `:all:` and `:none:`. #[derive(Debug, Clone)] pub enum PackageNameSpecifier { All, None, Package(PackageName), } impl FromStr for PackageNameSpecifier { type Err = uv_normalize::InvalidNameError; fn from_str(name: &str) -> Result { match name { ":all:" => Ok(Self::All), ":none:" => Ok(Self::None), _ => Ok(Self::Package(PackageName::from_str(name)?)), } } } impl<'de> serde::Deserialize<'de> for PackageNameSpecifier { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct Visitor; impl serde::de::Visitor<'_> for Visitor { type Value = PackageNameSpecifier; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a package name or `:all:` or `:none:`") } fn visit_str(self, value: &str) -> Result where E: serde::de::Error, { // Accept the special values `:all:` and `:none:`. match value { ":all:" => Ok(PackageNameSpecifier::All), ":none:" => Ok(PackageNameSpecifier::None), _ => { // Otherwise, parse the value as a package name. match PackageName::from_str(value) { Ok(name) => Ok(PackageNameSpecifier::Package(name)), Err(err) => Err(E::custom(err)), } } } } } deserializer.deserialize_str(Visitor) } } #[cfg(feature = "schemars")] impl schemars::JsonSchema for PackageNameSpecifier { fn schema_name() -> Cow<'static, str> { Cow::Borrowed("PackageNameSpecifier") } fn json_schema(_gen: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { schemars::json_schema!({ "type": "string", "pattern": r"^(:none:|:all:|([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]))$", "description": "The name of a package, or `:all:` or `:none:` to select or omit all packages, respectively.", }) } } /// A repeated specifier used for (e.g.) pip's `--no-binary` flag. /// /// This is a superset of the package name format, allowing for special values `:all:` and `:none:`. #[derive(Debug, Clone)] pub enum PackageNameSpecifiers { All, None, Packages(Vec), } impl PackageNameSpecifiers { pub(crate) fn from_iter(specifiers: impl Iterator) -> Self { let mut packages = Vec::new(); let mut all: bool = false; for specifier in specifiers { match specifier { PackageNameSpecifier::None => { packages.clear(); all = false; } PackageNameSpecifier::All => { all = true; } PackageNameSpecifier::Package(name) => { packages.push(name); } } } if all { Self::All } else if packages.is_empty() { Self::None } else { Self::Packages(packages) } } } uv-0.9.17+ds1/crates/uv-configuration/src/overrides.rs000066400000000000000000000061771520155276700226710ustar00rootroot00000000000000use std::borrow::Cow; use either::Either; use rustc_hash::{FxBuildHasher, FxHashMap}; use uv_distribution_types::Requirement; use uv_normalize::PackageName; use uv_pep508::MarkerTree; /// A set of overrides for a set of requirements. #[derive(Debug, Default, Clone)] pub struct Overrides(FxHashMap>); impl Overrides { /// Create a new set of overrides from a set of requirements. pub fn from_requirements(requirements: Vec) -> Self { let mut overrides: FxHashMap> = FxHashMap::with_capacity_and_hasher(requirements.len(), FxBuildHasher); for requirement in requirements { overrides .entry(requirement.name.clone()) .or_default() .push(requirement); } Self(overrides) } /// Return an iterator over all [`Requirement`]s in the override set. pub fn requirements(&self) -> impl Iterator { self.0.values().flat_map(|requirements| requirements.iter()) } /// Get the overrides for a package. pub fn get(&self, name: &PackageName) -> Option<&Vec> { self.0.get(name) } /// Apply the overrides to a set of requirements. /// /// NB: Change this method together with [`Constraints::apply`]. pub fn apply<'a>( &'a self, requirements: impl IntoIterator, ) -> impl Iterator> { if self.0.is_empty() { // Fast path: There are no overrides. return Either::Left(requirements.into_iter().map(Cow::Borrowed)); } Either::Right(requirements.into_iter().flat_map(|requirement| { let Some(overrides) = self.get(&requirement.name) else { // Case 1: No override(s). return Either::Left(std::iter::once(Cow::Borrowed(requirement))); }; // ASSUMPTION: There is one `extra = "..."`, and it's either the only marker or part // of the main conjunction. let Some(extra_expression) = requirement.marker.top_level_extra() else { // Case 2: A non-optional dependency with override(s). return Either::Right(Either::Right(overrides.iter().map(Cow::Borrowed))); }; // Case 3: An optional dependency with override(s). // // When the original requirement is an optional dependency, the override(s) need to // be optional for the same extra, otherwise we activate extras that should be inactive. Either::Right(Either::Left(overrides.iter().map( move |override_requirement| { // Add the extra to the override marker. let mut joint_marker = MarkerTree::expression(extra_expression.clone()); joint_marker.and(override_requirement.marker); Cow::Owned(Requirement { marker: joint_marker, ..override_requirement.clone() }) }, ))) })) } } uv-0.9.17+ds1/crates/uv-configuration/src/package_options.rs000066400000000000000000000257631520155276700240370ustar00rootroot00000000000000use std::path::Path; use either::Either; use rustc_hash::FxHashMap; use uv_cache::Refresh; use uv_cache_info::Timestamp; use uv_distribution_types::Requirement; use uv_normalize::PackageName; /// Whether to reinstall packages. #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum Reinstall { /// Don't reinstall any packages; respect the existing installation. #[default] None, /// Reinstall all packages in the plan. All, /// Reinstall only the specified packages. Packages(Vec, Vec>), } impl Reinstall { /// Determine the reinstall strategy to use. pub fn from_args(reinstall: Option, reinstall_package: Vec) -> Option { match reinstall { Some(true) => Some(Self::All), Some(false) => Some(Self::None), None if reinstall_package.is_empty() => None, None => Some(Self::Packages(reinstall_package, Vec::new())), } } /// Returns `true` if no packages should be reinstalled. pub fn is_none(&self) -> bool { matches!(self, Self::None) } /// Returns `true` if all packages should be reinstalled. pub fn is_all(&self) -> bool { matches!(self, Self::All) } /// Returns `true` if the specified package should be reinstalled. pub fn contains_package(&self, package_name: &PackageName) -> bool { match self { Self::None => false, Self::All => true, Self::Packages(packages, ..) => packages.contains(package_name), } } /// Returns `true` if the specified path should be reinstalled. pub fn contains_path(&self, path: &Path) -> bool { match self { Self::None => false, Self::All => true, Self::Packages(.., paths) => paths .iter() .any(|target| same_file::is_same_file(path, target).unwrap_or(false)), } } /// Combine a set of [`Reinstall`] values. #[must_use] pub fn combine(self, other: Self) -> Self { match self { // Setting `--reinstall` or `--no-reinstall` should clear previous `--reinstall-package` selections. Self::All | Self::None => self, Self::Packages(self_packages, self_paths) => match other { // If `--reinstall` was enabled previously, `--reinstall-package` is subsumed by reinstalling all packages. Self::All => other, // If `--no-reinstall` was enabled previously, then `--reinstall-package` enables an explicit reinstall of those packages. Self::None => Self::Packages(self_packages, self_paths), // If `--reinstall-package` was included twice, combine the requirements. Self::Packages(other_packages, other_paths) => { let mut combined_packages = self_packages; combined_packages.extend(other_packages); let mut combined_paths = self_paths; combined_paths.extend(other_paths); Self::Packages(combined_packages, combined_paths) } }, } } /// Add a [`Box`] to the [`Reinstall`] policy. #[must_use] pub fn with_path(self, path: Box) -> Self { match self { Self::None => Self::Packages(vec![], vec![path]), Self::All => Self::All, Self::Packages(packages, mut paths) => { paths.push(path); Self::Packages(packages, paths) } } } /// Add a [`Package`] to the [`Reinstall`] policy. #[must_use] pub fn with_package(self, package_name: PackageName) -> Self { match self { Self::None => Self::Packages(vec![package_name], vec![]), Self::All => Self::All, Self::Packages(mut packages, paths) => { packages.push(package_name); Self::Packages(packages, paths) } } } /// Create a [`Reinstall`] strategy to reinstall a single package. pub fn package(package_name: PackageName) -> Self { Self::Packages(vec![package_name], vec![]) } } /// Create a [`Refresh`] policy by integrating the [`Reinstall`] policy. impl From for Refresh { fn from(value: Reinstall) -> Self { match value { Reinstall::None => Self::None(Timestamp::now()), Reinstall::All => Self::All(Timestamp::now()), Reinstall::Packages(packages, paths) => { Self::Packages(packages, paths, Timestamp::now()) } } } } /// Whether to allow package upgrades. #[derive(Debug, Default, Clone)] pub enum Upgrade { /// Prefer pinned versions from the existing lockfile, if possible. #[default] None, /// Allow package upgrades for all packages, ignoring the existing lockfile. All, /// Allow package upgrades, but only for the specified packages. Packages(FxHashMap>), } impl Upgrade { /// Determine the upgrade selection strategy from the command-line arguments. pub fn from_args(upgrade: Option, upgrade_package: Vec) -> Option { match upgrade { Some(true) => Some(Self::All), // TODO(charlie): `--no-upgrade` with `--upgrade-package` should allow the specified // packages to be upgraded. Right now, `--upgrade-package` is silently ignored. Some(false) => Some(Self::None), None if upgrade_package.is_empty() => None, None => Some(Self::Packages(upgrade_package.into_iter().fold( FxHashMap::default(), |mut map, requirement| { map.entry(requirement.name.clone()) .or_default() .push(requirement); map }, ))), } } /// Create an [`Upgrade`] strategy to upgrade a single package. pub fn package(package_name: PackageName) -> Self { Self::Packages({ let mut map = FxHashMap::default(); map.insert(package_name, vec![]); map }) } /// Returns `true` if no packages should be upgraded. pub fn is_none(&self) -> bool { matches!(self, Self::None) } /// Returns `true` if all packages should be upgraded. pub fn is_all(&self) -> bool { matches!(self, Self::All) } /// Returns `true` if the specified package should be upgraded. pub fn contains(&self, package_name: &PackageName) -> bool { match self { Self::None => false, Self::All => true, Self::Packages(packages) => packages.contains_key(package_name), } } /// Returns an iterator over the constraints. /// /// When upgrading, users can provide bounds on the upgrade (e.g., `--upgrade-package flask<3`). pub fn constraints(&self) -> impl Iterator { if let Self::Packages(packages) = self { Either::Right( packages .values() .flat_map(|requirements| requirements.iter()), ) } else { Either::Left(std::iter::empty()) } } /// Combine a set of [`Upgrade`] values. #[must_use] pub fn combine(self, other: Self) -> Self { match self { // Setting `--upgrade` or `--no-upgrade` should clear previous `--upgrade-package` selections. Self::All | Self::None => self, Self::Packages(self_packages) => match other { // If `--upgrade` was enabled previously, `--upgrade-package` is subsumed by upgrading all packages. Self::All => other, // If `--no-upgrade` was enabled previously, then `--upgrade-package` enables an explicit upgrade of those packages. Self::None => Self::Packages(self_packages), // If `--upgrade-package` was included twice, combine the requirements. Self::Packages(other_packages) => { let mut combined = self_packages; for (package, requirements) in other_packages { combined.entry(package).or_default().extend(requirements); } Self::Packages(combined) } }, } } } /// Create a [`Refresh`] policy by integrating the [`Upgrade`] policy. impl From for Refresh { fn from(value: Upgrade) -> Self { match value { Upgrade::None => Self::None(Timestamp::now()), Upgrade::All => Self::All(Timestamp::now()), Upgrade::Packages(packages) => Self::Packages( packages.into_keys().collect::>(), Vec::new(), Timestamp::now(), ), } } } /// Whether to isolate builds. #[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum BuildIsolation { /// Isolate all builds. #[default] Isolate, /// Do not isolate any builds. Shared, /// Do not isolate builds for the specified packages. SharedPackage(Vec), } impl BuildIsolation { /// Determine the build isolation strategy from the command-line arguments. pub fn from_args( no_build_isolation: Option, no_build_isolation_package: Vec, ) -> Option { match no_build_isolation { Some(true) => Some(Self::Shared), Some(false) => Some(Self::Isolate), None if no_build_isolation_package.is_empty() => None, None => Some(Self::SharedPackage(no_build_isolation_package)), } } /// Combine a set of [`BuildIsolation`] values. #[must_use] pub fn combine(self, other: Self) -> Self { match self { // Setting `--build-isolation` or `--no-build-isolation` should clear previous `--no-build-isolation-package` selections. Self::Isolate | Self::Shared => self, Self::SharedPackage(self_packages) => match other { // If `--no-build-isolation` was enabled previously, `--no-build-isolation-package` is subsumed by sharing all builds. Self::Shared => other, // If `--build-isolation` was enabled previously, then `--no-build-isolation-package` enables specific packages to be shared. Self::Isolate => Self::SharedPackage(self_packages), // If `--no-build-isolation-package` was included twice, combine the packages. Self::SharedPackage(other_packages) => { let mut combined = self_packages; combined.extend(other_packages); Self::SharedPackage(combined) } }, } } } uv-0.9.17+ds1/crates/uv-configuration/src/project_build_backend.rs000066400000000000000000000033041520155276700251500ustar00rootroot00000000000000/// Available project build backends for use in `pyproject.toml`. #[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum ProjectBuildBackend { #[cfg_attr(feature = "clap", value(alias = "uv-build", alias = "uv_build"))] /// Use uv as the project build backend. Uv, #[serde(alias = "hatchling")] #[cfg_attr(feature = "clap", value(alias = "hatchling"))] /// Use [hatchling](https://pypi.org/project/hatchling) as the project build backend. Hatch, /// Use [flit-core](https://pypi.org/project/flit-core) as the project build backend. #[serde(alias = "flit-core")] #[cfg_attr(feature = "clap", value(alias = "flit-core"))] Flit, /// Use [pdm-backend](https://pypi.org/project/pdm-backend) as the project build backend. #[serde(alias = "pdm-backend")] #[cfg_attr(feature = "clap", value(alias = "pdm-backend"))] PDM, /// Use [poetry-core](https://pypi.org/project/poetry-core) as the project build backend. #[serde(alias = "poetry-core")] #[cfg_attr(feature = "clap", value(alias = "poetry-core", alias = "poetry_core"))] Poetry, /// Use [setuptools](https://pypi.org/project/setuptools) as the project build backend. Setuptools, /// Use [maturin](https://pypi.org/project/maturin) as the project build backend. Maturin, /// Use [scikit-build-core](https://pypi.org/project/scikit-build-core) as the project build backend. #[serde(alias = "scikit-build-core")] #[cfg_attr(feature = "clap", value(alias = "scikit-build-core"))] Scikit, } uv-0.9.17+ds1/crates/uv-configuration/src/required_version.rs000066400000000000000000000044751520155276700242530ustar00rootroot00000000000000#[cfg(feature = "schemars")] use std::borrow::Cow; use std::{fmt::Formatter, str::FromStr}; use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers, VersionSpecifiersParseError}; /// A required version of uv, represented as a version specifier (e.g. `>=0.5.0`). #[derive(Clone, Debug, PartialEq, Eq)] pub struct RequiredVersion(VersionSpecifiers); impl RequiredVersion { /// Return `true` if the given version is required. pub fn contains(&self, version: &Version) -> bool { self.0.contains(version) } /// Returns the underlying [`VersionSpecifiers`]. pub fn specifiers(&self) -> &VersionSpecifiers { &self.0 } } impl FromStr for RequiredVersion { type Err = VersionSpecifiersParseError; fn from_str(s: &str) -> Result { // Treat `0.5.0` as `==0.5.0`, for backwards compatibility. if let Ok(version) = Version::from_str(s) { Ok(Self(VersionSpecifiers::from( VersionSpecifier::equals_version(version), ))) } else { Ok(Self(VersionSpecifiers::from_str(s)?)) } } } #[cfg(feature = "schemars")] impl schemars::JsonSchema for RequiredVersion { fn schema_name() -> Cow<'static, str> { Cow::Borrowed("RequiredVersion") } fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { schemars::json_schema!({ "type": "string", "description": "A version specifier, e.g. `>=0.5.0` or `==0.5.0`." }) } } impl<'de> serde::Deserialize<'de> for RequiredVersion { fn deserialize>(deserializer: D) -> Result { struct Visitor; impl serde::de::Visitor<'_> for Visitor { type Value = RequiredVersion; fn expecting(&self, f: &mut Formatter) -> std::fmt::Result { f.write_str("a string") } fn visit_str(self, v: &str) -> Result { RequiredVersion::from_str(v).map_err(serde::de::Error::custom) } } deserializer.deserialize_str(Visitor) } } impl std::fmt::Display for RequiredVersion { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(&self.0, f) } } uv-0.9.17+ds1/crates/uv-configuration/src/sources.rs000066400000000000000000000011501520155276700223340ustar00rootroot00000000000000#[derive( Debug, Default, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, )] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum SourceStrategy { /// Use `tool.uv.sources` when resolving dependencies. #[default] Enabled, /// Ignore `tool.uv.sources` when resolving dependencies. Disabled, } impl SourceStrategy { /// Return the [`SourceStrategy`] from the command-line arguments, if any. pub fn from_args(no_sources: bool) -> Self { if no_sources { Self::Disabled } else { Self::Enabled } } } uv-0.9.17+ds1/crates/uv-configuration/src/target_triple.rs000066400000000000000000001150641520155276700235300ustar00rootroot00000000000000use tracing::debug; use uv_pep508::MarkerEnvironment; use uv_platform_tags::{Arch, Os, Platform}; use uv_static::EnvVars; /// The supported target triples. Each triple consists of an architecture, vendor, and operating /// system. /// /// See: #[derive(Debug, Clone, Copy, Eq, PartialEq, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum TargetTriple { /// An alias for `x86_64-pc-windows-msvc`, the default target for Windows. Windows, /// An alias for `x86_64-unknown-linux-gnu`, the default target for Linux. Linux, /// An alias for `aarch64-apple-darwin`, the default target for macOS. Macos, /// A 64-bit x86 Windows target. #[cfg_attr(feature = "clap", value(name = "x86_64-pc-windows-msvc"))] #[serde(rename = "x86_64-pc-windows-msvc")] #[serde(alias = "x8664-pc-windows-msvc")] X8664PcWindowsMsvc, /// An ARM64 Windows target. #[cfg_attr(feature = "clap", value(name = "aarch64-pc-windows-msvc"))] #[serde(rename = "aarch64-pc-windows-msvc")] #[serde(alias = "arm64-pc-windows-msvc")] Aarch64PcWindowsMsvc, /// A 32-bit x86 Windows target. #[cfg_attr(feature = "clap", value(name = "i686-pc-windows-msvc"))] #[serde(rename = "i686-pc-windows-msvc")] I686PcWindowsMsvc, /// An x86 Linux target. Equivalent to `x86_64-manylinux_2_28`. #[cfg_attr(feature = "clap", value(name = "x86_64-unknown-linux-gnu"))] #[serde(rename = "x86_64-unknown-linux-gnu")] #[serde(alias = "x8664-unknown-linux-gnu")] X8664UnknownLinuxGnu, /// An ARM-based macOS target, as seen on Apple Silicon devices /// /// By default, assumes the least-recent, non-EOL macOS version (13.0), but respects /// the `MACOSX_DEPLOYMENT_TARGET` environment variable if set. #[cfg_attr(feature = "clap", value(name = "aarch64-apple-darwin"))] #[serde(rename = "aarch64-apple-darwin")] Aarch64AppleDarwin, /// An x86 macOS target. /// /// By default, assumes the least-recent, non-EOL macOS version (13.0), but respects /// the `MACOSX_DEPLOYMENT_TARGET` environment variable if set. #[cfg_attr(feature = "clap", value(name = "x86_64-apple-darwin"))] #[serde(rename = "x86_64-apple-darwin")] #[serde(alias = "x8664-apple-darwin")] X8664AppleDarwin, /// An ARM64 Linux target. Equivalent to `aarch64-manylinux_2_28`. #[cfg_attr(feature = "clap", value(name = "aarch64-unknown-linux-gnu"))] #[serde(rename = "aarch64-unknown-linux-gnu")] Aarch64UnknownLinuxGnu, /// An ARM64 Linux target. #[cfg_attr(feature = "clap", value(name = "aarch64-unknown-linux-musl"))] #[serde(rename = "aarch64-unknown-linux-musl")] Aarch64UnknownLinuxMusl, /// An `x86_64` Linux target. #[cfg_attr(feature = "clap", value(name = "x86_64-unknown-linux-musl"))] #[serde(rename = "x86_64-unknown-linux-musl")] #[serde(alias = "x8664-unknown-linux-musl")] X8664UnknownLinuxMusl, /// A RISCV64 Linux target. #[cfg_attr(feature = "clap", value(name = "riscv64-unknown-linux"))] #[serde(rename = "riscv64-unknown-linux")] Riscv64UnknownLinuxGnu, /// An `x86_64` target for the `manylinux2014` platform. Equivalent to `x86_64-manylinux_2_17`. #[cfg_attr(feature = "clap", value(name = "x86_64-manylinux2014"))] #[serde(rename = "x86_64-manylinux2014")] #[serde(alias = "x8664-manylinux2014")] X8664Manylinux2014, /// An `x86_64` target for the `manylinux_2_17` platform. #[cfg_attr(feature = "clap", value(name = "x86_64-manylinux_2_17"))] #[serde(rename = "x86_64-manylinux_2_17")] #[serde(alias = "x8664-manylinux217")] X8664Manylinux217, /// An `x86_64` target for the `manylinux_2_28` platform. #[cfg_attr(feature = "clap", value(name = "x86_64-manylinux_2_28"))] #[serde(rename = "x86_64-manylinux_2_28")] #[serde(alias = "x8664-manylinux228")] X8664Manylinux228, /// An `x86_64` target for the `manylinux_2_31` platform. #[cfg_attr(feature = "clap", value(name = "x86_64-manylinux_2_31"))] #[serde(rename = "x86_64-manylinux_2_31")] #[serde(alias = "x8664-manylinux231")] X8664Manylinux231, /// An `x86_64` target for the `manylinux_2_32` platform. #[cfg_attr(feature = "clap", value(name = "x86_64-manylinux_2_32"))] #[serde(rename = "x86_64-manylinux_2_32")] #[serde(alias = "x8664-manylinux232")] X8664Manylinux232, /// An `x86_64` target for the `manylinux_2_33` platform. #[cfg_attr(feature = "clap", value(name = "x86_64-manylinux_2_33"))] #[serde(rename = "x86_64-manylinux_2_33")] #[serde(alias = "x8664-manylinux233")] X8664Manylinux233, /// An `x86_64` target for the `manylinux_2_34` platform. #[cfg_attr(feature = "clap", value(name = "x86_64-manylinux_2_34"))] #[serde(rename = "x86_64-manylinux_2_34")] #[serde(alias = "x8664-manylinux234")] X8664Manylinux234, /// An `x86_64` target for the `manylinux_2_35` platform. #[cfg_attr(feature = "clap", value(name = "x86_64-manylinux_2_35"))] #[serde(rename = "x86_64-manylinux_2_35")] #[serde(alias = "x8664-manylinux235")] X8664Manylinux235, /// An `x86_64` target for the `manylinux_2_36` platform. #[cfg_attr(feature = "clap", value(name = "x86_64-manylinux_2_36"))] #[serde(rename = "x86_64-manylinux_2_36")] #[serde(alias = "x8664-manylinux236")] X8664Manylinux236, /// An `x86_64` target for the `manylinux_2_37` platform. #[cfg_attr(feature = "clap", value(name = "x86_64-manylinux_2_37"))] #[serde(rename = "x86_64-manylinux_2_37")] #[serde(alias = "x8664-manylinux237")] X8664Manylinux237, /// An `x86_64` target for the `manylinux_2_38` platform. #[cfg_attr(feature = "clap", value(name = "x86_64-manylinux_2_38"))] #[serde(rename = "x86_64-manylinux_2_38")] #[serde(alias = "x8664-manylinux238")] X8664Manylinux238, /// An `x86_64` target for the `manylinux_2_39` platform. #[cfg_attr(feature = "clap", value(name = "x86_64-manylinux_2_39"))] #[serde(rename = "x86_64-manylinux_2_39")] #[serde(alias = "x8664-manylinux239")] X8664Manylinux239, /// An `x86_64` target for the `manylinux_2_40` platform. #[cfg_attr(feature = "clap", value(name = "x86_64-manylinux_2_40"))] #[serde(rename = "x86_64-manylinux_2_40")] #[serde(alias = "x8664-manylinux240")] X8664Manylinux240, /// An ARM64 target for the `manylinux2014` platform. Equivalent to `aarch64-manylinux_2_17`. #[cfg_attr(feature = "clap", value(name = "aarch64-manylinux2014"))] #[serde(rename = "aarch64-manylinux2014")] Aarch64Manylinux2014, /// An ARM64 target for the `manylinux_2_17` platform. #[cfg_attr(feature = "clap", value(name = "aarch64-manylinux_2_17"))] #[serde(rename = "aarch64-manylinux_2_17")] #[serde(alias = "aarch64-manylinux217")] Aarch64Manylinux217, /// An ARM64 target for the `manylinux_2_28` platform. #[cfg_attr(feature = "clap", value(name = "aarch64-manylinux_2_28"))] #[serde(rename = "aarch64-manylinux_2_28")] #[serde(alias = "aarch64-manylinux228")] Aarch64Manylinux228, /// An ARM64 target for the `manylinux_2_31` platform. #[cfg_attr(feature = "clap", value(name = "aarch64-manylinux_2_31"))] #[serde(rename = "aarch64-manylinux_2_31")] #[serde(alias = "aarch64-manylinux231")] Aarch64Manylinux231, /// An ARM64 target for the `manylinux_2_32` platform. #[cfg_attr(feature = "clap", value(name = "aarch64-manylinux_2_32"))] #[serde(rename = "aarch64-manylinux_2_32")] #[serde(alias = "aarch64-manylinux232")] Aarch64Manylinux232, /// An ARM64 target for the `manylinux_2_33` platform. #[cfg_attr(feature = "clap", value(name = "aarch64-manylinux_2_33"))] #[serde(rename = "aarch64-manylinux_2_33")] #[serde(alias = "aarch64-manylinux233")] Aarch64Manylinux233, /// An ARM64 target for the `manylinux_2_34` platform. #[cfg_attr(feature = "clap", value(name = "aarch64-manylinux_2_34"))] #[serde(rename = "aarch64-manylinux_2_34")] #[serde(alias = "aarch64-manylinux234")] Aarch64Manylinux234, /// An ARM64 target for the `manylinux_2_35` platform. #[cfg_attr(feature = "clap", value(name = "aarch64-manylinux_2_35"))] #[serde(rename = "aarch64-manylinux_2_35")] #[serde(alias = "aarch64-manylinux235")] Aarch64Manylinux235, /// An ARM64 target for the `manylinux_2_36` platform. #[cfg_attr(feature = "clap", value(name = "aarch64-manylinux_2_36"))] #[serde(rename = "aarch64-manylinux_2_36")] #[serde(alias = "aarch64-manylinux236")] Aarch64Manylinux236, /// An ARM64 target for the `manylinux_2_37` platform. #[cfg_attr(feature = "clap", value(name = "aarch64-manylinux_2_37"))] #[serde(rename = "aarch64-manylinux_2_37")] #[serde(alias = "aarch64-manylinux237")] Aarch64Manylinux237, /// An ARM64 target for the `manylinux_2_38` platform. #[cfg_attr(feature = "clap", value(name = "aarch64-manylinux_2_38"))] #[serde(rename = "aarch64-manylinux_2_38")] #[serde(alias = "aarch64-manylinux238")] Aarch64Manylinux238, /// An ARM64 target for the `manylinux_2_39` platform. #[cfg_attr(feature = "clap", value(name = "aarch64-manylinux_2_39"))] #[serde(rename = "aarch64-manylinux_2_39")] #[serde(alias = "aarch64-manylinux239")] Aarch64Manylinux239, /// An ARM64 target for the `manylinux_2_40` platform. #[cfg_attr(feature = "clap", value(name = "aarch64-manylinux_2_40"))] #[serde(rename = "aarch64-manylinux_2_40")] #[serde(alias = "aarch64-manylinux240")] Aarch64Manylinux240, /// An ARM64 Android target. /// /// By default uses Android API level 24, but respects /// the `ANDROID_API_LEVEL` environment variable if set. #[cfg_attr(feature = "clap", value(name = "aarch64-linux-android"))] #[serde(rename = "aarch64-linux-android")] Aarch64LinuxAndroid, /// An `x86_64` Android target. /// /// By default uses Android API level 24, but respects /// the `ANDROID_API_LEVEL` environment variable if set. #[cfg_attr(feature = "clap", value(name = "x86_64-linux-android"))] #[serde(rename = "x86_64-linux-android")] X8664LinuxAndroid, /// A wasm32 target using the Pyodide 2024 platform. Meant for use with Python 3.12. #[cfg_attr(feature = "clap", value(name = "wasm32-pyodide2024"))] Wasm32Pyodide2024, /// An ARM64 target for iOS device /// /// By default, iOS 13.0 is used, but respects the `IPHONEOS_DEPLOYMENT_TARGET` /// environment variable if set. #[cfg_attr(feature = "clap", value(name = "arm64-apple-ios"))] #[serde(rename = "arm64-apple-ios")] Arm64Ios, /// An ARM64 target for iOS simulator /// /// By default, iOS 13.0 is used, but respects the `IPHONEOS_DEPLOYMENT_TARGET` /// environment variable if set. #[cfg_attr(feature = "clap", value(name = "arm64-apple-ios-simulator"))] #[serde(rename = "arm64-apple-ios-simulator")] Arm64IosSimulator, /// An `x86_64` target for iOS simulator /// /// By default, iOS 13.0 is used, but respects the `IPHONEOS_DEPLOYMENT_TARGET` /// environment variable if set. #[cfg_attr(feature = "clap", value(name = "x86_64-apple-ios-simulator"))] #[serde(rename = "x86_64-apple-ios-simulator")] X8664IosSimulator, } impl TargetTriple { /// Return the [`Platform`] for the target. pub fn platform(self) -> Platform { match self { Self::Windows | Self::X8664PcWindowsMsvc => Platform::new(Os::Windows, Arch::X86_64), Self::Aarch64PcWindowsMsvc => Platform::new(Os::Windows, Arch::Aarch64), Self::Linux | Self::X8664UnknownLinuxGnu => Platform::new( Os::Manylinux { major: 2, minor: 28, }, Arch::X86_64, ), Self::Macos | Self::Aarch64AppleDarwin => { let (major, minor) = macos_deployment_target().map_or((13, 0), |(major, minor)| { debug!("Found macOS deployment target: {}.{}", major, minor); (major, minor) }); Platform::new(Os::Macos { major, minor }, Arch::Aarch64) } Self::I686PcWindowsMsvc => Platform::new(Os::Windows, Arch::X86), Self::X8664AppleDarwin => { let (major, minor) = macos_deployment_target().map_or((13, 0), |(major, minor)| { debug!("Found macOS deployment target: {}.{}", major, minor); (major, minor) }); Platform::new(Os::Macos { major, minor }, Arch::X86_64) } Self::Aarch64UnknownLinuxGnu => Platform::new( Os::Manylinux { major: 2, minor: 28, }, Arch::Aarch64, ), Self::Riscv64UnknownLinuxGnu => Platform::new( Os::Manylinux { major: 2, minor: 39, }, Arch::Riscv64, ), Self::Aarch64UnknownLinuxMusl => { Platform::new(Os::Musllinux { major: 1, minor: 2 }, Arch::Aarch64) } Self::X8664UnknownLinuxMusl => { Platform::new(Os::Musllinux { major: 1, minor: 2 }, Arch::X86_64) } Self::X8664Manylinux2014 => Platform::new( Os::Manylinux { major: 2, minor: 17, }, Arch::X86_64, ), Self::X8664Manylinux217 => Platform::new( Os::Manylinux { major: 2, minor: 17, }, Arch::X86_64, ), Self::X8664Manylinux228 => Platform::new( Os::Manylinux { major: 2, minor: 28, }, Arch::X86_64, ), Self::X8664Manylinux231 => Platform::new( Os::Manylinux { major: 2, minor: 31, }, Arch::X86_64, ), Self::X8664Manylinux232 => Platform::new( Os::Manylinux { major: 2, minor: 32, }, Arch::X86_64, ), Self::X8664Manylinux233 => Platform::new( Os::Manylinux { major: 2, minor: 33, }, Arch::X86_64, ), Self::X8664Manylinux234 => Platform::new( Os::Manylinux { major: 2, minor: 34, }, Arch::X86_64, ), Self::X8664Manylinux235 => Platform::new( Os::Manylinux { major: 2, minor: 35, }, Arch::X86_64, ), Self::X8664Manylinux236 => Platform::new( Os::Manylinux { major: 2, minor: 36, }, Arch::X86_64, ), Self::X8664Manylinux237 => Platform::new( Os::Manylinux { major: 2, minor: 37, }, Arch::X86_64, ), Self::X8664Manylinux238 => Platform::new( Os::Manylinux { major: 2, minor: 38, }, Arch::X86_64, ), Self::X8664Manylinux239 => Platform::new( Os::Manylinux { major: 2, minor: 39, }, Arch::X86_64, ), Self::X8664Manylinux240 => Platform::new( Os::Manylinux { major: 2, minor: 40, }, Arch::X86_64, ), Self::Aarch64Manylinux2014 => Platform::new( Os::Manylinux { major: 2, minor: 17, }, Arch::Aarch64, ), Self::Aarch64Manylinux217 => Platform::new( Os::Manylinux { major: 2, minor: 17, }, Arch::Aarch64, ), Self::Aarch64Manylinux228 => Platform::new( Os::Manylinux { major: 2, minor: 28, }, Arch::Aarch64, ), Self::Aarch64Manylinux231 => Platform::new( Os::Manylinux { major: 2, minor: 31, }, Arch::Aarch64, ), Self::Aarch64Manylinux232 => Platform::new( Os::Manylinux { major: 2, minor: 32, }, Arch::Aarch64, ), Self::Aarch64Manylinux233 => Platform::new( Os::Manylinux { major: 2, minor: 33, }, Arch::Aarch64, ), Self::Aarch64Manylinux234 => Platform::new( Os::Manylinux { major: 2, minor: 34, }, Arch::Aarch64, ), Self::Aarch64Manylinux235 => Platform::new( Os::Manylinux { major: 2, minor: 35, }, Arch::Aarch64, ), Self::Aarch64Manylinux236 => Platform::new( Os::Manylinux { major: 2, minor: 36, }, Arch::Aarch64, ), Self::Aarch64Manylinux237 => Platform::new( Os::Manylinux { major: 2, minor: 37, }, Arch::Aarch64, ), Self::Aarch64Manylinux238 => Platform::new( Os::Manylinux { major: 2, minor: 38, }, Arch::Aarch64, ), Self::Aarch64Manylinux239 => Platform::new( Os::Manylinux { major: 2, minor: 39, }, Arch::Aarch64, ), Self::Aarch64Manylinux240 => Platform::new( Os::Manylinux { major: 2, minor: 40, }, Arch::Aarch64, ), Self::Wasm32Pyodide2024 => Platform::new( Os::Pyodide { major: 2024, minor: 0, }, Arch::Wasm32, ), Self::Aarch64LinuxAndroid => { let api_level = android_api_level().map_or(24, |api_level| { debug!("Found Android API level: {}", api_level); api_level }); Platform::new(Os::Android { api_level }, Arch::Aarch64) } Self::X8664LinuxAndroid => { let api_level = android_api_level().map_or(24, |api_level| { debug!("Found Android API level: {}", api_level); api_level }); Platform::new(Os::Android { api_level }, Arch::X86_64) } Self::Arm64Ios => { let (major, minor) = ios_deployment_target().map_or((13, 0), |(major, minor)| { debug!("Found iOS deployment target: {}.{}", major, minor); (major, minor) }); Platform::new( Os::Ios { major, minor, simulator: false, }, Arch::Aarch64, ) } Self::Arm64IosSimulator => { let (major, minor) = ios_deployment_target().map_or((13, 0), |(major, minor)| { debug!("Found iOS deployment target: {}.{}", major, minor); (major, minor) }); Platform::new( Os::Ios { major, minor, simulator: true, }, Arch::Aarch64, ) } Self::X8664IosSimulator => { let (major, minor) = ios_deployment_target().map_or((13, 0), |(major, minor)| { debug!("Found iOS deployment target: {}.{}", major, minor); (major, minor) }); Platform::new( Os::Ios { major, minor, simulator: true, }, Arch::X86_64, ) } } } /// Return the `platform_machine` value for the target. pub fn platform_machine(self) -> &'static str { match self { Self::Windows | Self::X8664PcWindowsMsvc => "x86_64", Self::Aarch64PcWindowsMsvc => "ARM64", Self::Linux | Self::X8664UnknownLinuxGnu => "x86_64", Self::Macos | Self::Aarch64AppleDarwin => "arm64", Self::I686PcWindowsMsvc => "x86", Self::X8664AppleDarwin => "x86_64", Self::Aarch64UnknownLinuxGnu => "aarch64", Self::Aarch64UnknownLinuxMusl => "aarch64", Self::X8664UnknownLinuxMusl => "x86_64", Self::Riscv64UnknownLinuxGnu => "riscv64", Self::X8664Manylinux2014 => "x86_64", Self::X8664Manylinux217 => "x86_64", Self::X8664Manylinux228 => "x86_64", Self::X8664Manylinux231 => "x86_64", Self::X8664Manylinux232 => "x86_64", Self::X8664Manylinux233 => "x86_64", Self::X8664Manylinux234 => "x86_64", Self::X8664Manylinux235 => "x86_64", Self::X8664Manylinux236 => "x86_64", Self::X8664Manylinux237 => "x86_64", Self::X8664Manylinux238 => "x86_64", Self::X8664Manylinux239 => "x86_64", Self::X8664Manylinux240 => "x86_64", Self::Aarch64Manylinux2014 => "aarch64", Self::Aarch64Manylinux217 => "aarch64", Self::Aarch64Manylinux228 => "aarch64", Self::Aarch64Manylinux231 => "aarch64", Self::Aarch64Manylinux232 => "aarch64", Self::Aarch64Manylinux233 => "aarch64", Self::Aarch64Manylinux234 => "aarch64", Self::Aarch64Manylinux235 => "aarch64", Self::Aarch64Manylinux236 => "aarch64", Self::Aarch64Manylinux237 => "aarch64", Self::Aarch64Manylinux238 => "aarch64", Self::Aarch64Manylinux239 => "aarch64", Self::Aarch64Manylinux240 => "aarch64", Self::Aarch64LinuxAndroid => "aarch64", Self::X8664LinuxAndroid => "x86_64", Self::Wasm32Pyodide2024 => "wasm32", Self::Arm64Ios => "arm64", Self::Arm64IosSimulator => "arm64", Self::X8664IosSimulator => "x86_64", } } /// Return the `platform_system` value for the target. pub fn platform_system(self) -> &'static str { match self { Self::Windows | Self::X8664PcWindowsMsvc => "Windows", Self::Aarch64PcWindowsMsvc => "Windows", Self::Linux | Self::X8664UnknownLinuxGnu => "Linux", Self::Macos | Self::Aarch64AppleDarwin => "Darwin", Self::I686PcWindowsMsvc => "Windows", Self::X8664AppleDarwin => "Darwin", Self::Aarch64UnknownLinuxGnu => "Linux", Self::Aarch64UnknownLinuxMusl => "Linux", Self::X8664UnknownLinuxMusl => "Linux", Self::Riscv64UnknownLinuxGnu => "Linux", Self::X8664Manylinux2014 => "Linux", Self::X8664Manylinux217 => "Linux", Self::X8664Manylinux228 => "Linux", Self::X8664Manylinux231 => "Linux", Self::X8664Manylinux232 => "Linux", Self::X8664Manylinux233 => "Linux", Self::X8664Manylinux234 => "Linux", Self::X8664Manylinux235 => "Linux", Self::X8664Manylinux236 => "Linux", Self::X8664Manylinux237 => "Linux", Self::X8664Manylinux238 => "Linux", Self::X8664Manylinux239 => "Linux", Self::X8664Manylinux240 => "Linux", Self::Aarch64Manylinux2014 => "Linux", Self::Aarch64Manylinux217 => "Linux", Self::Aarch64Manylinux228 => "Linux", Self::Aarch64Manylinux231 => "Linux", Self::Aarch64Manylinux232 => "Linux", Self::Aarch64Manylinux233 => "Linux", Self::Aarch64Manylinux234 => "Linux", Self::Aarch64Manylinux235 => "Linux", Self::Aarch64Manylinux236 => "Linux", Self::Aarch64Manylinux237 => "Linux", Self::Aarch64Manylinux238 => "Linux", Self::Aarch64Manylinux239 => "Linux", Self::Aarch64Manylinux240 => "Linux", Self::Aarch64LinuxAndroid => "Android", Self::X8664LinuxAndroid => "Android", Self::Wasm32Pyodide2024 => "Emscripten", Self::Arm64Ios => "iOS", Self::Arm64IosSimulator => "iOS", Self::X8664IosSimulator => "iOS", } } /// Return the `platform_version` value for the target. pub fn platform_version(self) -> &'static str { match self { Self::Windows | Self::X8664PcWindowsMsvc => "", Self::Aarch64PcWindowsMsvc => "", Self::Linux | Self::X8664UnknownLinuxGnu => "", Self::Macos | Self::Aarch64AppleDarwin => "", Self::I686PcWindowsMsvc => "", Self::X8664AppleDarwin => "", Self::Aarch64UnknownLinuxGnu => "", Self::Aarch64UnknownLinuxMusl => "", Self::X8664UnknownLinuxMusl => "", Self::Riscv64UnknownLinuxGnu => "", Self::X8664Manylinux2014 => "", Self::X8664Manylinux217 => "", Self::X8664Manylinux228 => "", Self::X8664Manylinux231 => "", Self::X8664Manylinux232 => "", Self::X8664Manylinux233 => "", Self::X8664Manylinux234 => "", Self::X8664Manylinux235 => "", Self::X8664Manylinux236 => "", Self::X8664Manylinux237 => "", Self::X8664Manylinux238 => "", Self::X8664Manylinux239 => "", Self::X8664Manylinux240 => "", Self::Aarch64Manylinux2014 => "", Self::Aarch64Manylinux217 => "", Self::Aarch64Manylinux228 => "", Self::Aarch64Manylinux231 => "", Self::Aarch64Manylinux232 => "", Self::Aarch64Manylinux233 => "", Self::Aarch64Manylinux234 => "", Self::Aarch64Manylinux235 => "", Self::Aarch64Manylinux236 => "", Self::Aarch64Manylinux237 => "", Self::Aarch64Manylinux238 => "", Self::Aarch64Manylinux239 => "", Self::Aarch64Manylinux240 => "", Self::Aarch64LinuxAndroid => "", Self::X8664LinuxAndroid => "", // This is the value Emscripten gives for its version: // https://github.com/emscripten-core/emscripten/blob/4.0.8/system/lib/libc/emscripten_syscall_stubs.c#L63 // It doesn't really seem to mean anything? But for completeness we include it here. Self::Wasm32Pyodide2024 => "#1", Self::Arm64Ios => "", Self::Arm64IosSimulator => "", Self::X8664IosSimulator => "", } } /// Return the `platform_release` value for the target. pub fn platform_release(self) -> &'static str { match self { Self::Windows | Self::X8664PcWindowsMsvc => "", Self::Aarch64PcWindowsMsvc => "", Self::Linux | Self::X8664UnknownLinuxGnu => "", Self::Macos | Self::Aarch64AppleDarwin => "", Self::I686PcWindowsMsvc => "", Self::X8664AppleDarwin => "", Self::Aarch64UnknownLinuxGnu => "", Self::Aarch64UnknownLinuxMusl => "", Self::X8664UnknownLinuxMusl => "", Self::Riscv64UnknownLinuxGnu => "", Self::X8664Manylinux2014 => "", Self::X8664Manylinux217 => "", Self::X8664Manylinux228 => "", Self::X8664Manylinux231 => "", Self::X8664Manylinux232 => "", Self::X8664Manylinux233 => "", Self::X8664Manylinux234 => "", Self::X8664Manylinux235 => "", Self::X8664Manylinux236 => "", Self::X8664Manylinux237 => "", Self::X8664Manylinux238 => "", Self::X8664Manylinux239 => "", Self::X8664Manylinux240 => "", Self::Aarch64Manylinux2014 => "", Self::Aarch64Manylinux217 => "", Self::Aarch64Manylinux228 => "", Self::Aarch64Manylinux231 => "", Self::Aarch64Manylinux232 => "", Self::Aarch64Manylinux233 => "", Self::Aarch64Manylinux234 => "", Self::Aarch64Manylinux235 => "", Self::Aarch64Manylinux236 => "", Self::Aarch64Manylinux237 => "", Self::Aarch64Manylinux238 => "", Self::Aarch64Manylinux239 => "", Self::Aarch64Manylinux240 => "", Self::Aarch64LinuxAndroid => "", Self::X8664LinuxAndroid => "", // This is the Emscripten compiler version for Pyodide 2024. // See https://pyodide.org/en/stable/development/abi.html#pyodide-2024-0 Self::Wasm32Pyodide2024 => "3.1.58", Self::Arm64Ios => "", Self::Arm64IosSimulator => "", Self::X8664IosSimulator => "", } } /// Return the `os_name` value for the target. pub fn os_name(self) -> &'static str { match self { Self::Windows | Self::X8664PcWindowsMsvc => "nt", Self::Aarch64PcWindowsMsvc => "nt", Self::Linux | Self::X8664UnknownLinuxGnu => "posix", Self::Macos | Self::Aarch64AppleDarwin => "posix", Self::I686PcWindowsMsvc => "nt", Self::X8664AppleDarwin => "posix", Self::Aarch64UnknownLinuxGnu => "posix", Self::Aarch64UnknownLinuxMusl => "posix", Self::X8664UnknownLinuxMusl => "posix", Self::Riscv64UnknownLinuxGnu => "posix", Self::X8664Manylinux2014 => "posix", Self::X8664Manylinux217 => "posix", Self::X8664Manylinux228 => "posix", Self::X8664Manylinux231 => "posix", Self::X8664Manylinux232 => "posix", Self::X8664Manylinux233 => "posix", Self::X8664Manylinux234 => "posix", Self::X8664Manylinux235 => "posix", Self::X8664Manylinux236 => "posix", Self::X8664Manylinux237 => "posix", Self::X8664Manylinux238 => "posix", Self::X8664Manylinux239 => "posix", Self::X8664Manylinux240 => "posix", Self::Aarch64Manylinux2014 => "posix", Self::Aarch64Manylinux217 => "posix", Self::Aarch64Manylinux228 => "posix", Self::Aarch64Manylinux231 => "posix", Self::Aarch64Manylinux232 => "posix", Self::Aarch64Manylinux233 => "posix", Self::Aarch64Manylinux234 => "posix", Self::Aarch64Manylinux235 => "posix", Self::Aarch64Manylinux236 => "posix", Self::Aarch64Manylinux237 => "posix", Self::Aarch64Manylinux238 => "posix", Self::Aarch64Manylinux239 => "posix", Self::Aarch64Manylinux240 => "posix", Self::Aarch64LinuxAndroid => "posix", Self::X8664LinuxAndroid => "posix", Self::Wasm32Pyodide2024 => "posix", Self::Arm64Ios => "posix", Self::Arm64IosSimulator => "posix", Self::X8664IosSimulator => "posix", } } /// Return the `sys_platform` value for the target. pub fn sys_platform(self) -> &'static str { match self { Self::Windows | Self::X8664PcWindowsMsvc => "win32", Self::Aarch64PcWindowsMsvc => "win32", Self::Linux | Self::X8664UnknownLinuxGnu => "linux", Self::Macos | Self::Aarch64AppleDarwin => "darwin", Self::I686PcWindowsMsvc => "win32", Self::X8664AppleDarwin => "darwin", Self::Aarch64UnknownLinuxGnu => "linux", Self::Aarch64UnknownLinuxMusl => "linux", Self::X8664UnknownLinuxMusl => "linux", Self::Riscv64UnknownLinuxGnu => "linux", Self::X8664Manylinux2014 => "linux", Self::X8664Manylinux217 => "linux", Self::X8664Manylinux228 => "linux", Self::X8664Manylinux231 => "linux", Self::X8664Manylinux232 => "linux", Self::X8664Manylinux233 => "linux", Self::X8664Manylinux234 => "linux", Self::X8664Manylinux235 => "linux", Self::X8664Manylinux236 => "linux", Self::X8664Manylinux237 => "linux", Self::X8664Manylinux238 => "linux", Self::X8664Manylinux239 => "linux", Self::X8664Manylinux240 => "linux", Self::Aarch64Manylinux2014 => "linux", Self::Aarch64Manylinux217 => "linux", Self::Aarch64Manylinux228 => "linux", Self::Aarch64Manylinux231 => "linux", Self::Aarch64Manylinux232 => "linux", Self::Aarch64Manylinux233 => "linux", Self::Aarch64Manylinux234 => "linux", Self::Aarch64Manylinux235 => "linux", Self::Aarch64Manylinux236 => "linux", Self::Aarch64Manylinux237 => "linux", Self::Aarch64Manylinux238 => "linux", Self::Aarch64Manylinux239 => "linux", Self::Aarch64Manylinux240 => "linux", Self::Aarch64LinuxAndroid => "android", Self::X8664LinuxAndroid => "android", Self::Wasm32Pyodide2024 => "emscripten", Self::Arm64Ios => "ios", Self::Arm64IosSimulator => "ios", Self::X8664IosSimulator => "ios", } } /// Return `true` if the platform is compatible with manylinux. pub fn manylinux_compatible(self) -> bool { match self { Self::Windows | Self::X8664PcWindowsMsvc => false, Self::Aarch64PcWindowsMsvc => false, Self::Linux | Self::X8664UnknownLinuxGnu => true, Self::Macos | Self::Aarch64AppleDarwin => false, Self::I686PcWindowsMsvc => false, Self::X8664AppleDarwin => false, Self::Aarch64UnknownLinuxGnu => true, Self::Aarch64UnknownLinuxMusl => true, Self::X8664UnknownLinuxMusl => true, Self::Riscv64UnknownLinuxGnu => true, Self::X8664Manylinux2014 => true, Self::X8664Manylinux217 => true, Self::X8664Manylinux228 => true, Self::X8664Manylinux231 => true, Self::X8664Manylinux232 => true, Self::X8664Manylinux233 => true, Self::X8664Manylinux234 => true, Self::X8664Manylinux235 => true, Self::X8664Manylinux236 => true, Self::X8664Manylinux237 => true, Self::X8664Manylinux238 => true, Self::X8664Manylinux239 => true, Self::X8664Manylinux240 => true, Self::Aarch64Manylinux2014 => true, Self::Aarch64Manylinux217 => true, Self::Aarch64Manylinux228 => true, Self::Aarch64Manylinux231 => true, Self::Aarch64Manylinux232 => true, Self::Aarch64Manylinux233 => true, Self::Aarch64Manylinux234 => true, Self::Aarch64Manylinux235 => true, Self::Aarch64Manylinux236 => true, Self::Aarch64Manylinux237 => true, Self::Aarch64Manylinux238 => true, Self::Aarch64Manylinux239 => true, Self::Aarch64Manylinux240 => true, Self::Aarch64LinuxAndroid => false, Self::X8664LinuxAndroid => false, Self::Wasm32Pyodide2024 => false, Self::Arm64Ios => false, Self::Arm64IosSimulator => false, Self::X8664IosSimulator => false, } } /// Return a [`MarkerEnvironment`] compatible with the given [`TargetTriple`], based on /// a base [`MarkerEnvironment`]. /// /// The returned [`MarkerEnvironment`] will preserve the base environment's Python version /// markers, but override its platform markers. pub fn markers(self, base: &MarkerEnvironment) -> MarkerEnvironment { base.clone() .with_os_name(self.os_name()) .with_platform_machine(self.platform_machine()) .with_platform_system(self.platform_system()) .with_sys_platform(self.sys_platform()) .with_platform_release(self.platform_release()) .with_platform_version(self.platform_version()) } } /// Return the macOS deployment target as parsed from the environment. fn macos_deployment_target() -> Option<(u16, u16)> { let version = std::env::var(EnvVars::MACOSX_DEPLOYMENT_TARGET).ok()?; let mut parts = version.split('.'); // Parse the major version (e.g., `12` in `12.0`). let major = parts.next()?.parse::().ok()?; // Parse the minor version (e.g., `0` in `12.0`), with a default of `0`. let minor = parts.next().unwrap_or("0").parse::().ok()?; Some((major, minor)) } /// Return the iOS deployment target as parsed from the environment. fn ios_deployment_target() -> Option<(u16, u16)> { let version = std::env::var(EnvVars::IPHONEOS_DEPLOYMENT_TARGET).ok()?; let mut parts = version.split('.'); // Parse the major version (e.g., `12` in `12.0`). let major = parts.next()?.parse::().ok()?; // Parse the minor version (e.g., `0` in `12.0`), with a default of `0`. let minor = parts.next().unwrap_or("0").parse::().ok()?; Some((major, minor)) } /// Return the Android API level as parsed from the environment. fn android_api_level() -> Option { let api_level_str = std::env::var(EnvVars::ANDROID_API_LEVEL).ok()?; // Parse the api level. let api_level = api_level_str.parse::().ok()?; Some(api_level) } uv-0.9.17+ds1/crates/uv-configuration/src/threading.rs000066400000000000000000000055701520155276700226300ustar00rootroot00000000000000//! Configure rayon and determine thread stack sizes. use std::sync::LazyLock; use std::sync::atomic::{AtomicUsize, Ordering}; use uv_static::EnvVars; /// The default minimum stack size for uv threads. pub const UV_DEFAULT_STACK_SIZE: usize = 4 * 1024 * 1024; /// We don't allow setting a smaller stack size than 1MB. #[allow(clippy::identity_op)] pub const UV_MIN_STACK_SIZE: usize = 1 * 1024 * 1024; /// Running out of stack has been an issue for us. We box types and futures in various places /// to mitigate this. /// /// Main thread stack-size has a BIG variety here across platforms and it's harder to control /// (which is why Rust doesn't by default). Notably on macOS and Linux you will typically get 8MB /// main thread, while on Windows you will typically get 1MB, which is *tiny*: /// /// /// To normalize this we just spawn a new thread called main2 with a size we can set /// ourselves. 2MB is typically too small (especially for our debug builds), while 4MB /// seems fine. This value can be changed with `UV_STACK_SIZE`, with a fallback to reading /// `RUST_MIN_STACK`, to allow checking a larger or smaller stack size. There is a hardcoded stack /// size minimum of 1MB, which is the lowest platform default we observed. /// /// Non-main threads should all have 2MB, as Rust forces platform consistency there, /// but even then stack overflows can occur in release mode /// (), so rayon and tokio get the same stack size, /// with the 4MB default. pub fn min_stack_size() -> usize { let stack_size = if let Some(uv_stack_size) = std::env::var(EnvVars::UV_STACK_SIZE) .ok() .and_then(|var| var.parse::().ok()) { uv_stack_size } else if let Some(uv_stack_size) = std::env::var(EnvVars::RUST_MIN_STACK) .ok() .and_then(|var| var.parse::().ok()) { uv_stack_size } else { UV_DEFAULT_STACK_SIZE }; if stack_size < UV_MIN_STACK_SIZE { return UV_DEFAULT_STACK_SIZE; } stack_size } /// The number of threads for the rayon threadpool. /// /// The default of 0 makes rayon use its default. pub static RAYON_PARALLELISM: AtomicUsize = AtomicUsize::new(0); /// Initialize the threadpool lazily. Always call before using rayon the potentially first time. /// /// The `uv` crate sets [`RAYON_PARALLELISM`] from the user settings, and the extract and install /// code initialize the threadpool lazily only if they are actually used by calling /// `LazyLock::force(&RAYON_INITIALIZE)`. pub static RAYON_INITIALIZE: LazyLock<()> = LazyLock::new(|| { rayon::ThreadPoolBuilder::new() .num_threads(RAYON_PARALLELISM.load(Ordering::Relaxed)) .stack_size(min_stack_size()) .build_global() .expect("failed to initialize global rayon pool"); }); uv-0.9.17+ds1/crates/uv-configuration/src/trusted_host.rs000066400000000000000000000130611520155276700234040ustar00rootroot00000000000000use serde::{Deserialize, Deserializer}; #[cfg(feature = "schemars")] use std::borrow::Cow; use std::str::FromStr; use url::Url; /// A host specification (wildcard, or host, with optional scheme and/or port) for which /// certificates are not verified when making HTTPS requests. #[derive(Debug, Clone, PartialEq, Eq)] pub enum TrustedHost { Wildcard, Host { scheme: Option, host: String, port: Option, }, } impl TrustedHost { /// Returns `true` if the [`Url`] matches this trusted host. pub fn matches(&self, url: &Url) -> bool { match self { Self::Wildcard => true, Self::Host { scheme, host, port } => { if scheme.as_ref().is_some_and(|scheme| scheme != url.scheme()) { return false; } if port.is_some_and(|port| url.port() != Some(port)) { return false; } if Some(host.as_str()) != url.host_str() { return false; } true } } } } impl<'de> Deserialize<'de> for TrustedHost { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] struct Inner { scheme: Option, host: String, port: Option, } serde_untagged::UntaggedEnumVisitor::new() .string(|string| Self::from_str(string).map_err(serde::de::Error::custom)) .map(|map| { map.deserialize::().map(|inner| Self::Host { scheme: inner.scheme, host: inner.host, port: inner.port, }) }) .deserialize(deserializer) } } impl serde::Serialize for TrustedHost { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { let s = self.to_string(); serializer.serialize_str(&s) } } #[derive(Debug, thiserror::Error)] pub enum TrustedHostError { #[error("missing host for `--trusted-host`: `{0}`")] MissingHost(String), #[error("invalid port for `--trusted-host`: `{0}`")] InvalidPort(String), } impl FromStr for TrustedHost { type Err = TrustedHostError; fn from_str(s: &str) -> Result { if s == "*" { return Ok(Self::Wildcard); } // Detect scheme. let (scheme, s) = if let Some(s) = s.strip_prefix("https://") { (Some("https".to_string()), s) } else if let Some(s) = s.strip_prefix("http://") { (Some("http".to_string()), s) } else { (None, s) }; let mut parts = s.splitn(2, ':'); // Detect host. let host = parts .next() .and_then(|host| host.split('/').next()) .map(ToString::to_string) .ok_or_else(|| TrustedHostError::MissingHost(s.to_string()))?; // Detect port. let port = parts .next() .map(str::parse) .transpose() .map_err(|_| TrustedHostError::InvalidPort(s.to_string()))?; Ok(Self::Host { scheme, host, port }) } } impl std::fmt::Display for TrustedHost { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Wildcard => { write!(f, "*")?; } Self::Host { scheme, host, port } => { if let Some(scheme) = &scheme { write!(f, "{scheme}://{host}")?; } else { write!(f, "{host}")?; } if let Some(port) = port { write!(f, ":{port}")?; } } } Ok(()) } } #[cfg(feature = "schemars")] impl schemars::JsonSchema for TrustedHost { fn schema_name() -> Cow<'static, str> { Cow::Borrowed("TrustedHost") } fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { schemars::json_schema!({ "type": "string", "description": "A host or host-port pair." }) } } #[cfg(test)] mod tests { #[test] fn parse() { assert_eq!( "*".parse::().unwrap(), super::TrustedHost::Wildcard ); assert_eq!( "example.com".parse::().unwrap(), super::TrustedHost::Host { scheme: None, host: "example.com".to_string(), port: None } ); assert_eq!( "example.com:8080".parse::().unwrap(), super::TrustedHost::Host { scheme: None, host: "example.com".to_string(), port: Some(8080) } ); assert_eq!( "https://example.com".parse::().unwrap(), super::TrustedHost::Host { scheme: Some("https".to_string()), host: "example.com".to_string(), port: None } ); assert_eq!( "https://example.com/hello/world" .parse::() .unwrap(), super::TrustedHost::Host { scheme: Some("https".to_string()), host: "example.com".to_string(), port: None } ); } } uv-0.9.17+ds1/crates/uv-configuration/src/trusted_publishing.rs000066400000000000000000000011521520155276700245710ustar00rootroot00000000000000use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum TrustedPublishing { /// Attempt trusted publishing when we're in a supported environment, continue if that fails. /// /// Supported environments include GitHub Actions and GitLab CI/CD. #[default] Automatic, // Force trusted publishing. Always, // Never try to get a trusted publishing token. Never, } uv-0.9.17+ds1/crates/uv-configuration/src/vcs.rs000066400000000000000000000057601520155276700214570ustar00rootroot00000000000000use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use serde::Deserialize; use uv_git::GIT; #[derive(Debug, thiserror::Error)] pub enum VersionControlError { #[error("Attempted to initialize a Git repository, but `git` was not found in PATH")] GitNotInstalled, #[error("Failed to initialize Git repository at `{0}`\nstdout: {1}\nstderr: {2}")] GitInit(PathBuf, String, String), #[error("`git` command failed")] GitCommand(#[source] std::io::Error), #[error(transparent)] Io(#[from] std::io::Error), } /// The version control system to use. #[derive(Clone, Copy, Debug, PartialEq, Default, Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum VersionControlSystem { /// Use Git for version control. #[default] Git, /// Do not use any version control system. None, } impl VersionControlSystem { /// Initializes the VCS system based on the provided path. pub fn init(&self, path: &Path) -> Result<(), VersionControlError> { match self { Self::Git => { let Ok(git) = GIT.as_ref() else { return Err(VersionControlError::GitNotInstalled); }; let output = Command::new(git) .arg("init") .current_dir(path) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .map_err(VersionControlError::GitCommand)?; if !output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); return Err(VersionControlError::GitInit( path.to_path_buf(), stdout.to_string(), stderr.to_string(), )); } // Create the `.gitignore`, if it doesn't exist. match fs_err::OpenOptions::new() .write(true) .create_new(true) .open(path.join(".gitignore")) { Ok(mut file) => file.write_all(GITIGNORE.as_bytes())?, Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => (), Err(err) => return Err(err.into()), } Ok(()) } Self::None => Ok(()), } } } impl std::fmt::Display for VersionControlSystem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Git => write!(f, "git"), Self::None => write!(f, "none"), } } } const GITIGNORE: &str = "# Python-generated files __pycache__/ *.py[oc] build/ dist/ wheels/ *.egg-info # Virtual environments .venv "; uv-0.9.17+ds1/crates/uv-console/000077500000000000000000000000001520155276700163125ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-console/Cargo.toml000066400000000000000000000006151520155276700202440ustar00rootroot00000000000000[package] name = "uv-console" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] console = { workspace = true } uv-0.9.17+ds1/crates/uv-console/README.md000066400000000000000000000010271520155276700175710ustar00rootroot00000000000000 # uv-console This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-console). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-console/src/000077500000000000000000000000001520155276700171015ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-console/src/lib.rs000066400000000000000000000264111520155276700202210ustar00rootroot00000000000000use console::{Key, Term, measure_text_width, style}; use std::{cmp::Ordering, iter}; /// Prompt the user for confirmation in the given [`Term`]. /// /// This is a slimmed-down version of `dialoguer::Confirm`, with the post-confirmation report /// enabled. pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result { confirm_inner(message, None, term, default) } /// Prompt the user for confirmation in the given [`Term`], with a hint. pub fn confirm_with_hint( message: &str, hint: &str, term: &Term, default: bool, ) -> std::io::Result { confirm_inner(message, Some(hint), term, default) } fn confirm_inner( message: &str, hint: Option<&str>, term: &Term, default: bool, ) -> std::io::Result { let prompt = format!( "{} {} {} {} {}", style("?".to_string()).for_stderr().yellow(), style(message).for_stderr().bold(), style("[y/n]").for_stderr().black().bright(), style("›").for_stderr().black().bright(), style(if default { "yes" } else { "no" }) .for_stderr() .cyan(), ); term.write_str(&prompt)?; if let Some(hint) = hint { term.write_str(&format!( "\n\n{}{} {hint}", style("hint").for_stderr().bold().cyan(), style(":").for_stderr().bold() ))?; } term.hide_cursor()?; term.flush()?; // Match continuously on every keystroke, and do not wait for user to hit the // `Enter` key. let response = loop { let input = term.read_key_raw()?; match input { Key::Char('y' | 'Y') => break true, Key::Char('n' | 'N') => break false, Key::Enter => break default, Key::CtrlC => { let term = Term::stderr(); term.show_cursor()?; term.write_str("\n")?; term.flush()?; #[allow(clippy::exit, clippy::cast_possible_wrap)] std::process::exit(if cfg!(windows) { 0xC000_013A_u32 as i32 } else { 130 }); } _ => {} } }; let report = format!( "{} {} {} {}", style("✔".to_string()).for_stderr().green(), style(message).for_stderr().bold(), style("·").for_stderr().black().bright(), style(if response { "yes" } else { "no" }) .for_stderr() .cyan(), ); if hint.is_some() { term.clear_last_lines(2)?; // It's not clear why we need to clear to the end of the screen here, but it fixes lingering // display of the hint on `bash` (the issue did not reproduce on `zsh`). term.clear_to_end_of_screen()?; } else { term.clear_line()?; } term.write_line(&report)?; term.show_cursor()?; term.flush()?; Ok(response) } /// Prompt the user for password in the given [`Term`]. /// /// This is a slimmed-down version of `dialoguer::Password`. pub fn password(prompt: &str, term: &Term) -> std::io::Result { term.write_str(prompt)?; term.show_cursor()?; term.flush()?; let input = term.read_secure_line()?; term.clear_line()?; Ok(input) } /// Prompt the user for username in the given [`Term`]. pub fn username(prompt: &str, term: &Term) -> std::io::Result { term.write_str(prompt)?; term.show_cursor()?; term.flush()?; let input = term.read_line()?; term.clear_line()?; Ok(input) } /// Prompt the user for input text in the given [`Term`]. /// /// This is a slimmed-down version of `dialoguer::Input`. #[allow( // Suppress Clippy lints triggered by `dialoguer::Input`. clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss )] pub fn input(prompt: &str, term: &Term) -> std::io::Result { term.write_str(prompt)?; term.show_cursor()?; term.flush()?; let prompt_len = measure_text_width(prompt); let mut chars: Vec = Vec::new(); let mut position = 0; loop { match term.read_key()? { Key::Backspace if position > 0 => { position -= 1; chars.remove(position); let line_size = term.size().1 as usize; // Case we want to delete last char of a line so the cursor is at the beginning of the next line if (position + prompt_len).is_multiple_of(line_size - 1) { term.clear_line()?; term.move_cursor_up(1)?; term.move_cursor_right(line_size + 1)?; } else { term.clear_chars(1)?; } let tail: String = chars[position..].iter().collect(); if !tail.is_empty() { term.write_str(&tail)?; let total = position + prompt_len + tail.chars().count(); let total_line = total / line_size; let line_cursor = (position + prompt_len) / line_size; term.move_cursor_up(total_line - line_cursor)?; term.move_cursor_left(line_size)?; term.move_cursor_right((position + prompt_len) % line_size)?; } term.flush()?; } Key::Char(chr) if !chr.is_ascii_control() => { chars.insert(position, chr); position += 1; let tail: String = iter::once(&chr).chain(chars[position..].iter()).collect(); term.write_str(&tail)?; term.move_cursor_left(tail.chars().count() - 1)?; term.flush()?; } Key::ArrowLeft if position > 0 => { if (position + prompt_len).is_multiple_of(term.size().1 as usize) { term.move_cursor_up(1)?; term.move_cursor_right(term.size().1 as usize)?; } else { term.move_cursor_left(1)?; } position -= 1; term.flush()?; } Key::ArrowRight if position < chars.len() => { if (position + prompt_len).is_multiple_of(term.size().1 as usize - 1) { term.move_cursor_down(1)?; term.move_cursor_left(term.size().1 as usize)?; } else { term.move_cursor_right(1)?; } position += 1; term.flush()?; } Key::UnknownEscSeq(seq) if seq == vec!['b'] => { let line_size = term.size().1 as usize; let nb_space = chars[..position] .iter() .rev() .take_while(|c| c.is_whitespace()) .count(); let find_last_space = chars[..position - nb_space] .iter() .rposition(|c| c.is_whitespace()); // If we find a space we set the cursor to the next char else we set it to the beginning of the input if let Some(mut last_space) = find_last_space { if last_space < position { last_space += 1; let new_line = (prompt_len + last_space) / line_size; let old_line = (prompt_len + position) / line_size; let diff_line = old_line - new_line; if diff_line != 0 { term.move_cursor_up(old_line - new_line)?; } let new_pos_x = (prompt_len + last_space) % line_size; let old_pos_x = (prompt_len + position) % line_size; let diff_pos_x = new_pos_x as i64 - old_pos_x as i64; if diff_pos_x < 0 { term.move_cursor_left(-diff_pos_x as usize)?; } else { term.move_cursor_right((diff_pos_x) as usize)?; } position = last_space; } } else { term.move_cursor_left(position)?; position = 0; } term.flush()?; } Key::UnknownEscSeq(seq) if seq == vec!['f'] => { let line_size = term.size().1 as usize; let find_next_space = chars[position..].iter().position(|c| c.is_whitespace()); // If we find a space we set the cursor to the next char else we set it to the beginning of the input if let Some(mut next_space) = find_next_space { let nb_space = chars[position + next_space..] .iter() .take_while(|c| c.is_whitespace()) .count(); next_space += nb_space; let new_line = (prompt_len + position + next_space) / line_size; let old_line = (prompt_len + position) / line_size; term.move_cursor_down(new_line - old_line)?; let new_pos_x = (prompt_len + position + next_space) % line_size; let old_pos_x = (prompt_len + position) % line_size; let diff_pos_x = new_pos_x as i64 - old_pos_x as i64; if diff_pos_x < 0 { term.move_cursor_left(-diff_pos_x as usize)?; } else { term.move_cursor_right((diff_pos_x) as usize)?; } position += next_space; } else { let new_line = (prompt_len + chars.len()) / line_size; let old_line = (prompt_len + position) / line_size; term.move_cursor_down(new_line - old_line)?; let new_pos_x = (prompt_len + chars.len()) % line_size; let old_pos_x = (prompt_len + position) % line_size; let diff_pos_x = new_pos_x as i64 - old_pos_x as i64; match diff_pos_x.cmp(&0) { Ordering::Less => { term.move_cursor_left((-diff_pos_x - 1) as usize)?; } Ordering::Equal => {} Ordering::Greater => { term.move_cursor_right((diff_pos_x) as usize)?; } } position = chars.len(); } term.flush()?; } Key::Enter => break, _ => (), } } let input = chars.iter().collect::(); term.write_line("")?; Ok(input) } /// Formats a number of bytes into a human readable SI-prefixed size (binary units). /// /// Returns a tuple of `(quantity, units)`. #[allow( clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_precision_loss, clippy::cast_sign_loss )] pub fn human_readable_bytes(bytes: u64) -> (f32, &'static str) { const UNITS: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]; let bytes_f32 = bytes as f32; let i = ((bytes_f32.log2() / 10.0) as usize).min(UNITS.len() - 1); (bytes_f32 / 1024_f32.powi(i as i32), UNITS[i]) } uv-0.9.17+ds1/crates/uv-dev/000077500000000000000000000000001520155276700154265ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-dev/.gitignore000066400000000000000000000000311520155276700174100ustar00rootroot00000000000000sdist_building_test_data uv-0.9.17+ds1/crates/uv-dev/Cargo.toml000066400000000000000000000054661520155276700173710ustar00rootroot00000000000000[package] name = "uv-dev" version = "0.0.7" description = "This is an internal component crate of uv" publish = false edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lints] workspace = true [dependencies] uv-cache = { workspace = true, features = ["clap"] } uv-cli = { workspace = true } uv-client = { workspace = true } uv-configuration = { workspace = true } uv-distribution-filename = { workspace = true } uv-distribution-types = { workspace = true } uv-extract = { workspace = true } uv-installer = { workspace = true } uv-macros = { workspace = true } uv-options-metadata = { workspace = true } uv-pep508 = { workspace = true } uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-settings = { workspace = true, features = ["schemars"] } uv-static = { workspace = true } uv-workspace = { workspace = true, features = ["schemars"] } # Any dependencies that are exclusively used in `uv-dev` should be listed as non-workspace # dependencies, to ensure that we're forced to think twice before including them in other crates. anstream = { workspace = true } anyhow = { workspace = true } clap = { workspace = true, features = ["derive", "wrap_help"] } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } itertools = { workspace = true } markdown = { version = "1.0.0" } owo-colors = { workspace = true } poloto = { version = "19.1.2", optional = true } pretty_assertions = { version = "1.4.1" } reqwest = { workspace = true, features = ["stream"] } resvg = { version = "0.29.0", optional = true } schemars = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { version = "0.9.34" } tagu = { version = "0.1.6", optional = true } tempfile = { workspace = true } textwrap = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } tracing = { workspace = true } tracing-durations-export = { workspace = true, features = ["plot"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } uv-performance-memory-allocator = { path = "../uv-performance-memory-allocator", optional = true } walkdir = { workspace = true } [lib] name = "uv_dev" [[bin]] name = "uv-dev" # We don't want to build the dev CLI by default, so we skip it by requiring an off-by-default feature required-features = ["dev"] [features] default = ["performance", "uv-extract/static"] # Actually build the dev CLI. dev = [] performance = ["performance-memory-allocator"] performance-memory-allocator = ["dep:uv-performance-memory-allocator"] render = ["poloto", "resvg", "tagu"] [package.metadata.cargo-shear] ignored = ["flate2", "uv-extract", "uv-performance-memory-allocator"] uv-0.9.17+ds1/crates/uv-dev/README.md000066400000000000000000000010171520155276700167040ustar00rootroot00000000000000 # uv-dev This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-dev). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-dev/builder.dockerfile000066400000000000000000000014461520155276700211120ustar00rootroot00000000000000# Provide isolation for source distribution builds # https://moyix.blogspot.com/2022/09/someones-been-messing-with-my-subnormals.html FROM ubuntu:22.04 # Feel free to add build dependencies you need RUN apt-get update \ && apt-get install -y --no-install-recommends \ autoconf \ build-essential \ cmake \ curl \ make \ pkg-config \ python3 \ python3-dev \ python3-pip \ python3-venv \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y ENV HOME="/root" WORKDIR /app RUN python3 -m venv $HOME/venv-docker ENV VIRTUAL_ENV="$HOME/venv-docker" ENV PATH="$HOME/.cargo/bin:$HOME/venv-docker/bin:$PATH" RUN rustup default 1.75.0 RUN rustup show uv-0.9.17+ds1/crates/uv-dev/src/000077500000000000000000000000001520155276700162155ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-dev/src/clear_compile.rs000066400000000000000000000020001520155276700213510ustar00rootroot00000000000000use std::path::PathBuf; use clap::Parser; use tracing::info; use walkdir::WalkDir; #[derive(Parser)] pub(crate) struct ClearCompileArgs { /// Compile all `.py` in this or any subdirectory to bytecode root: PathBuf, } pub(crate) fn clear_compile(args: &ClearCompileArgs) -> anyhow::Result<()> { let mut removed_files = 0; let mut removed_directories = 0; for entry in WalkDir::new(&args.root).contents_first(true) { let entry = entry?; let metadata = entry.metadata()?; if metadata.is_file() { if entry.path().extension().is_some_and(|ext| ext == "pyc") { fs_err::remove_file(entry.path())?; removed_files += 1; } } else if metadata.is_dir() { if entry.file_name() == "__pycache__" { fs_err::remove_dir(entry.path())?; removed_directories += 1; } } } info!("Removed {removed_files} files and {removed_directories} directories"); Ok(()) } uv-0.9.17+ds1/crates/uv-dev/src/compile.rs000066400000000000000000000023461520155276700202200ustar00rootroot00000000000000use std::path::PathBuf; use clap::Parser; use tracing::info; use uv_cache::{Cache, CacheArgs}; use uv_configuration::Concurrency; use uv_preview::Preview; use uv_python::{EnvironmentPreference, PythonEnvironment, PythonPreference, PythonRequest}; #[derive(Parser)] pub(crate) struct CompileArgs { /// Compile all `.py` in this or any subdirectory to bytecode root: PathBuf, python: Option, #[command(flatten)] cache_args: CacheArgs, } pub(crate) async fn compile(args: CompileArgs) -> anyhow::Result<()> { let cache = Cache::try_from(args.cache_args)?.init().await?; let interpreter = if let Some(python) = args.python { python } else { let interpreter = PythonEnvironment::find( &PythonRequest::default(), EnvironmentPreference::OnlyVirtual, PythonPreference::default(), &cache, Preview::default(), )? .into_interpreter(); interpreter.sys_executable().to_path_buf() }; let files = uv_installer::compile_tree( &fs_err::canonicalize(args.root)?, &interpreter, &Concurrency::default(), cache.root(), ) .await?; info!("Compiled {files} files"); Ok(()) } uv-0.9.17+ds1/crates/uv-dev/src/generate_all.rs000066400000000000000000000022621520155276700212070ustar00rootroot00000000000000//! Run all code and documentation generation steps. use anyhow::Result; use crate::{ generate_cli_reference, generate_env_vars_reference, generate_json_schema, generate_options_reference, generate_sysconfig_mappings, }; #[derive(clap::Args)] pub(crate) struct Args { #[arg(long, default_value_t, value_enum)] mode: Mode, } #[derive(Copy, Clone, PartialEq, Eq, clap::ValueEnum, Default)] pub(crate) enum Mode { /// Update the content in the `configuration.md`. #[default] Write, /// Don't write to the file, check if the file is up-to-date and error if not. Check, /// Write the generated help to stdout. DryRun, } pub(crate) async fn main(args: &Args) -> Result<()> { generate_json_schema::main(&generate_json_schema::Args { mode: args.mode })?; generate_options_reference::main(&generate_options_reference::Args { mode: args.mode })?; generate_cli_reference::main(&generate_cli_reference::Args { mode: args.mode })?; generate_env_vars_reference::main(&generate_env_vars_reference::Args { mode: args.mode })?; generate_sysconfig_mappings::main(&generate_sysconfig_mappings::Args { mode: args.mode }) .await?; Ok(()) } uv-0.9.17+ds1/crates/uv-dev/src/generate_cli_reference.rs000066400000000000000000000273331520155276700232320ustar00rootroot00000000000000//! Generate a Markdown-compatible reference for the uv command-line interface. use std::cmp::max; use std::path::PathBuf; use anstream::println; use anyhow::{Result, bail}; use clap::{Command, CommandFactory}; use itertools::Itertools; use pretty_assertions::StrComparison; use crate::ROOT_DIR; use crate::generate_all::Mode; use uv_cli::Cli; const REPLACEMENTS: &[(&str, &str)] = &[ // Replace suggestions to use `uv help python` with a link to the // `uv python` section ( "uv help python", "uv python", ), // Drop the manually included `env` section for `--no-python-downloads` // TODO(zanieb): In general, we should show all of the environment variables in the reference // but this one is non-standard so it's the only one included right now. When we tackle the rest // we can fix the formatting. (" [env: "UV_PYTHON_DOWNLOADS=never"]", ""), ]; const SHOW_HIDDEN_COMMANDS: &[&str] = &["generate-shell-completion"]; #[derive(clap::Args)] pub(crate) struct Args { #[arg(long, default_value_t, value_enum)] pub(crate) mode: Mode, } pub(crate) fn main(args: &Args) -> Result<()> { let reference_string = generate(); let filename = "cli.md"; let reference_path = PathBuf::from(ROOT_DIR) .join("docs") .join("reference") .join(filename); match args.mode { Mode::DryRun => { println!("{reference_string}"); } Mode::Check => match fs_err::read_to_string(reference_path) { Ok(current) => { if current == reference_string { println!("Up-to-date: {filename}"); } else { let comparison = StrComparison::new(¤t, &reference_string); bail!( "{filename} changed, please run `cargo dev generate-cli-reference`:\n{comparison}" ); } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { bail!("{filename} not found, please run `cargo dev generate-cli-reference`"); } Err(err) => { bail!("{filename} changed, please run `cargo dev generate-cli-reference`:\n{err}"); } }, Mode::Write => match fs_err::read_to_string(&reference_path) { Ok(current) => { if current == reference_string { println!("Up-to-date: {filename}"); } else { println!("Updating: {filename}"); fs_err::write(reference_path, reference_string.as_bytes())?; } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { println!("Updating: {filename}"); fs_err::write(reference_path, reference_string.as_bytes())?; } Err(err) => { bail!("{filename} changed, please run `cargo dev generate-cli-reference`:\n{err}"); } }, } Ok(()) } fn generate() -> String { let mut output = String::new(); let mut uv = Cli::command(); // It is very important to build the command before beginning inspection or subcommands // will be missing all of the propagated options. uv.build(); let mut parents = Vec::new(); output.push_str("# CLI Reference\n\n"); generate_command(&mut output, &uv, &mut parents); for (value, replacement) in REPLACEMENTS { assert_ne!( value, replacement, "`value` and `replacement` must be different, but both are `{value}`" ); let before = &output; let after = output.replace(value, replacement); assert_ne!(*before, after, "Could not find `{value}` in the output"); output = after; } output } #[allow(clippy::format_push_string)] fn generate_command<'a>(output: &mut String, command: &'a Command, parents: &mut Vec<&'a Command>) { if command.is_hide_set() && !SHOW_HIDDEN_COMMANDS.contains(&command.get_name()) { return; } // Generate the command header. let name = if parents.is_empty() { command.get_name().to_string() } else { format!( "{} {}", parents.iter().map(|cmd| cmd.get_name()).join(" "), command.get_name() ) }; // Display the top-level `uv` command at the same level as its children let level = max(2, parents.len() + 1); output.push_str(&format!("{} {name}\n\n", "#".repeat(level))); // Display the command description. if let Some(about) = command.get_long_about().or_else(|| command.get_about()) { output.push_str(&about.to_string()); output.push_str("\n\n"); } // Display the usage { // This appears to be the simplest way to get rendered usage from Clap, // it is complicated to render it manually. It's annoying that it // requires a mutable reference but it doesn't really matter. let mut command = command.clone(); output.push_str("

Usage

\n\n"); output.push_str(&format!( "```\n{}\n```", command .render_usage() .to_string() .trim_start_matches("Usage: "), )); output.push_str("\n\n"); } // Display a list of child commands let mut subcommands = command.get_subcommands().peekable(); let has_subcommands = subcommands.peek().is_some(); if has_subcommands { output.push_str("

Commands

\n\n"); output.push_str("
"); for subcommand in subcommands { if subcommand.is_hide_set() { continue; } let subcommand_name = format!("{name} {}", subcommand.get_name()); output.push_str(&format!( "
{subcommand_name}
", subcommand_name.replace(' ', "-") )); if let Some(about) = subcommand.get_about() { output.push_str(&format!( "
{}
\n", markdown::to_html(&about.to_string()) )); } } output.push_str("
\n\n"); } // Do not display options for commands with children if !has_subcommands { let name_key = name.replace(' ', "-"); // Display positional arguments let mut arguments = command .get_positionals() .filter(|arg| !arg.is_hide_set()) .peekable(); if arguments.peek().is_some() { output.push_str("

Arguments

\n\n"); output.push_str("
"); for arg in arguments { let id = format!("{name_key}--{}", arg.get_id()); output.push_str(&format!("
")); output.push_str(&format!( "{}", arg.get_id().to_string().to_uppercase(), )); output.push_str("
"); if let Some(help) = arg.get_long_help().or_else(|| arg.get_help()) { output.push_str("
"); output.push_str(&format!("{}\n", markdown::to_html(&help.to_string()))); output.push_str("
"); } } output.push_str("
\n\n"); } // Display options and flags let mut options = command .get_arguments() .filter(|arg| !arg.is_positional()) .filter(|arg| !arg.is_hide_set()) .sorted_by_key(|arg| arg.get_id()) .peekable(); if options.peek().is_some() { output.push_str("

Options

\n\n"); output.push_str("
"); for opt in options { let Some(long) = opt.get_long() else { continue }; let id = format!("{name_key}--{long}"); output.push_str(&format!("
")); output.push_str(&format!("--{long}")); for long_alias in opt.get_all_aliases().into_iter().flatten() { output.push_str(&format!(", --{long_alias}")); } if let Some(short) = opt.get_short() { output.push_str(&format!(", -{short}")); } for short_alias in opt.get_all_short_aliases().into_iter().flatten() { output.push_str(&format!(", -{short_alias}")); } // Re-implements private `Arg::is_takes_value_set` used in `Command::get_opts` if opt .get_num_args() .unwrap_or_else(|| 1.into()) .takes_values() { if let Some(values) = opt.get_value_names() { for value in values { output.push_str(&format!( " {}", value.to_lowercase().replace('_', "-") )); } } } output.push_str("
"); if let Some(help) = opt.get_long_help().or_else(|| opt.get_help()) { output.push_str("
"); output.push_str(&format!("{}\n", markdown::to_html(&help.to_string()))); emit_env_option(opt, output); emit_default_option(opt, output); emit_possible_options(opt, output); output.push_str("
"); } } output.push_str("
"); } output.push_str("\n\n"); } parents.push(command); // Recurse to all of the subcommands. for subcommand in command.get_subcommands() { generate_command(output, subcommand, parents); } parents.pop(); } fn emit_env_option(opt: &clap::Arg, output: &mut String) { if opt.is_hide_env_set() { return; } if let Some(env) = opt.get_env() { output.push_str(&markdown::to_html(&format!( "May also be set with the `{}` environment variable.", env.to_string_lossy() ))); } } fn emit_default_option(opt: &clap::Arg, output: &mut String) { if opt.is_hide_default_value_set() || !opt.get_num_args().expect("built").takes_values() { return; } let values = opt.get_default_values(); if !values.is_empty() { let value = format!( "\n[default: {}]", opt.get_default_values() .iter() .map(|s| s.to_string_lossy()) .join(",") ); output.push_str(&markdown::to_html(&value)); } } fn emit_possible_options(opt: &clap::Arg, output: &mut String) { if opt.is_hide_possible_values_set() { return; } let values = opt.get_possible_values(); if !values.is_empty() { let value = format!( "\nPossible values:\n{}", values .into_iter() .filter(|value| !value.is_hide_set()) .map(|value| { let name = value.get_name(); value.get_help().map_or_else( || format!(" - `{name}`"), |help| format!(" - `{name}`: {help}"), ) }) .collect_vec() .join("\n"), ); output.push_str(&markdown::to_html(&value)); } } uv-0.9.17+ds1/crates/uv-dev/src/generate_env_vars_reference.rs000066400000000000000000000071351520155276700243040ustar00rootroot00000000000000//! Generate the environment variables reference from `uv_static::EnvVars`. use anyhow::bail; use pretty_assertions::StrComparison; use std::collections::BTreeSet; use std::path::PathBuf; use uv_static::EnvVars; use crate::ROOT_DIR; use crate::generate_all::Mode; #[derive(clap::Args)] pub(crate) struct Args { #[arg(long, default_value_t, value_enum)] pub(crate) mode: Mode, } pub(crate) fn main(args: &Args) -> anyhow::Result<()> { let reference_string = generate(); let filename = "environment.md"; let reference_path = PathBuf::from(ROOT_DIR) .join("docs") .join("reference") .join(filename); match args.mode { Mode::DryRun => { anstream::println!("{reference_string}"); } Mode::Check => match fs_err::read_to_string(reference_path) { Ok(current) => { if current == reference_string { anstream::println!("Up-to-date: {filename}"); } else { let comparison = StrComparison::new(¤t, &reference_string); bail!( "{filename} changed, please run `cargo dev generate-env-vars-reference`:\n{comparison}" ); } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { bail!("{filename} not found, please run `cargo dev generate-env-vars-reference`"); } Err(err) => { bail!( "{filename} changed, please run `cargo dev generate-env-vars-reference`:\n{err}" ); } }, Mode::Write => match fs_err::read_to_string(&reference_path) { Ok(current) => { if current == reference_string { anstream::println!("Up-to-date: {filename}"); } else { anstream::println!("Updating: {filename}"); fs_err::write(reference_path, reference_string.as_bytes())?; } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { anstream::println!("Updating: {filename}"); fs_err::write(reference_path, reference_string.as_bytes())?; } Err(err) => { bail!( "{filename} changed, please run `cargo dev generate-env-vars-reference`:\n{err}" ); } }, } Ok(()) } fn generate() -> String { let mut output = String::new(); output.push_str("# Environment variables\n\n"); // Partition and sort environment variables into UV_ and external variables. let (uv_vars, external_vars): (BTreeSet<_>, BTreeSet<_>) = EnvVars::metadata() .iter() .partition(|(var, _, _)| var.starts_with("UV_")); output.push_str("uv defines and respects the following environment variables:\n\n"); for (var, doc, added_in) in uv_vars { output.push_str(&render(var, doc, added_in)); } output.push_str("\n\n## Externally defined variables\n\n"); output.push_str("uv also reads the following externally defined environment variables:\n\n"); for (var, doc, added_in) in external_vars { output.push_str(&render(var, doc, added_in)); } output } /// Render an environment variable and its documentation. fn render(var: &str, doc: &str, added_in: Option<&str>) -> String { if let Some(added_in) = added_in { format!("### `{var}`\nadded in `{added_in}`\n\n{doc}\n\n") } else { format!("### `{var}`\n\n{doc}\n\n") } } uv-0.9.17+ds1/crates/uv-dev/src/generate_json_schema.rs000066400000000000000000000105261520155276700227320ustar00rootroot00000000000000use std::path::PathBuf; use anstream::println; use anyhow::{Result, bail}; use pretty_assertions::StrComparison; use schemars::JsonSchema; use serde::Deserialize; use uv_settings::Options as SettingsOptions; use uv_workspace::pyproject::ToolUv as WorkspaceOptions; use crate::ROOT_DIR; use crate::generate_all::Mode; #[derive(Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] #[allow(dead_code)] // The names and docstrings of this struct and the types it contains are used as `title` and // `description` in uv.schema.json, see https://github.com/SchemaStore/schemastore/blob/master/editor-features.md#title-as-an-expected-object-type /// Metadata and configuration for uv. struct CombinedOptions { #[serde(flatten)] options: SettingsOptions, #[serde(flatten)] workspace: WorkspaceOptions, } #[derive(clap::Args)] pub(crate) struct Args { #[arg(long, default_value_t, value_enum)] pub(crate) mode: Mode, } pub(crate) fn main(args: &Args) -> Result<()> { // Generate the schema. let schema_string = generate(); let filename = "uv.schema.json"; let schema_path = PathBuf::from(ROOT_DIR).join(filename); match args.mode { Mode::DryRun => { println!("{schema_string}"); } Mode::Check => match fs_err::read_to_string(schema_path) { Ok(current) => { if current == schema_string { println!("Up-to-date: {filename}"); } else { let comparison = StrComparison::new(¤t, &schema_string); bail!( "{filename} changed, please run `cargo dev generate-json-schema`:\n{comparison}" ); } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { bail!("{filename} not found, please run `cargo dev generate-json-schema`"); } Err(err) => { bail!("{filename} changed, please run `cargo dev generate-json-schema`:\n{err}"); } }, Mode::Write => match fs_err::read_to_string(&schema_path) { Ok(current) => { if current == schema_string { println!("Up-to-date: {filename}"); } else { println!("Updating: {filename}"); fs_err::write(schema_path, schema_string.as_bytes())?; } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { println!("Updating: {filename}"); fs_err::write(schema_path, schema_string.as_bytes())?; } Err(err) => { bail!("{filename} changed, please run `cargo dev generate-json-schema`:\n{err}"); } }, } Ok(()) } const REPLACEMENTS: &[(&str, &str)] = &[ // Use the fully-resolved URL rather than the relative Markdown path. ( "(../concepts/projects/dependencies.md)", "(https://docs.astral.sh/uv/concepts/projects/dependencies/)", ), ]; /// Generate the JSON schema for the combined options as a string. fn generate() -> String { let settings = schemars::generate::SchemaSettings::draft07(); let generator = schemars::SchemaGenerator::new(settings); let schema = generator.into_root_schema_for::(); let mut output = serde_json::to_string_pretty(&schema).unwrap(); for (value, replacement) in REPLACEMENTS { assert_ne!( value, replacement, "`value` and `replacement` must be different, but both are `{value}`" ); let before = &output; let after = output.replace(value, replacement); assert_ne!(*before, after, "Could not find `{value}` in the output"); output = after; } output } #[cfg(test)] mod tests { use std::env; use anyhow::Result; use uv_static::EnvVars; use crate::generate_all::Mode; use super::{Args, main}; #[test] fn test_generate_json_schema() -> Result<()> { // Skip this test in CI to avoid redundancy with the dedicated CI job if env::var_os(EnvVars::CI).is_some() { return Ok(()); } let mode = if env::var(EnvVars::UV_UPDATE_SCHEMA).as_deref() == Ok("1") { Mode::Write } else { Mode::Check }; main(&Args { mode }) } } uv-0.9.17+ds1/crates/uv-dev/src/generate_options_reference.rs000066400000000000000000000260421520155276700241520ustar00rootroot00000000000000//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`. //! //! Based on: use std::fmt::Write; use std::path::PathBuf; use anstream::println; use anyhow::{Result, bail}; use itertools::Itertools; use pretty_assertions::StrComparison; use schemars::JsonSchema; use serde::Deserialize; use uv_macros::OptionsMetadata; use uv_options_metadata::{OptionField, OptionSet, OptionsMetadata, Visit}; use uv_settings::Options as SettingsOptions; use uv_workspace::pyproject::ToolUv as WorkspaceOptions; use crate::ROOT_DIR; use crate::generate_all::Mode; #[derive(Deserialize, JsonSchema, OptionsMetadata)] #[serde(deny_unknown_fields)] #[allow(dead_code)] // The names and docstrings of this struct and the types it contains are used as `title` and // `description` in uv.schema.json, see https://github.com/SchemaStore/schemastore/blob/master/editor-features.md#title-as-an-expected-object-type /// Metadata and configuration for uv. struct CombinedOptions { #[serde(flatten)] options: SettingsOptions, #[serde(flatten)] workspace: WorkspaceOptions, } #[derive(clap::Args)] pub(crate) struct Args { #[arg(long, default_value_t, value_enum)] pub(crate) mode: Mode, } pub(crate) fn main(args: &Args) -> Result<()> { let reference_string = generate(); let filename = "settings.md"; let reference_path = PathBuf::from(ROOT_DIR) .join("docs") .join("reference") .join(filename); match args.mode { Mode::DryRun => { println!("{reference_string}"); } Mode::Check => match fs_err::read_to_string(reference_path) { Ok(current) => { if current == reference_string { println!("Up-to-date: {filename}"); } else { let comparison = StrComparison::new(¤t, &reference_string); bail!( "{filename} changed, please run `cargo dev generate-options-reference`:\n{comparison}" ); } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { bail!("{filename} not found, please run `cargo dev generate-options-reference`"); } Err(err) => { bail!( "{filename} changed, please run `cargo dev generate-options-reference`:\n{err}" ); } }, Mode::Write => match fs_err::read_to_string(&reference_path) { Ok(current) => { if current == reference_string { println!("Up-to-date: {filename}"); } else { println!("Updating: {filename}"); fs_err::write(reference_path, reference_string.as_bytes())?; } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { println!("Updating: {filename}"); fs_err::write(reference_path, reference_string.as_bytes())?; } Err(err) => { bail!( "{filename} changed, please run `cargo dev generate-options-reference`:\n{err}" ); } }, } Ok(()) } enum OptionType { Configuration, ProjectMetadata, } fn generate() -> String { let mut output = String::new(); generate_set( &mut output, Set::Global { set: WorkspaceOptions::metadata(), option_type: OptionType::ProjectMetadata, }, &mut Vec::new(), ); generate_set( &mut output, Set::Global { set: SettingsOptions::metadata(), option_type: OptionType::Configuration, }, &mut Vec::new(), ); output } fn generate_set(output: &mut String, set: Set, parents: &mut Vec) { match &set { Set::Global { option_type, .. } => { let header = match option_type { OptionType::Configuration => "## Configuration\n", OptionType::ProjectMetadata => "## Project metadata\n", }; output.push_str(header); } Set::Named { name, .. } => { let title = parents .iter() .filter_map(|set| set.name()) .chain(std::iter::once(name.as_str())) .join("."); writeln!(output, "### `{title}`\n").unwrap(); if let Some(documentation) = set.metadata().documentation() { output.push_str(documentation); output.push('\n'); output.push('\n'); } } } let mut visitor = CollectOptionsVisitor::default(); set.metadata().record(&mut visitor); let (mut fields, mut sets) = (visitor.fields, visitor.groups); fields.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2)); sets.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2)); parents.push(set); // Generate the fields. for (name, field) in &fields { emit_field(output, name, field, parents.as_slice()); output.push_str("---\n\n"); } // Generate all the sub-sets. for (set_name, sub_set) in &sets { generate_set( output, Set::Named { name: set_name.to_owned(), set: *sub_set, }, parents, ); } parents.pop(); } enum Set { Global { option_type: OptionType, set: OptionSet, }, Named { name: String, set: OptionSet, }, } impl Set { fn name(&self) -> Option<&str> { match self { Self::Global { .. } => None, Self::Named { name, .. } => Some(name), } } fn metadata(&self) -> &OptionSet { match self { Self::Global { set, .. } => set, Self::Named { set, .. } => set, } } } #[allow(clippy::format_push_string)] fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[Set]) { let header_level = if parents.len() > 1 { "####" } else { "###" }; let parents_anchor = parents.iter().filter_map(|parent| parent.name()).join("_"); if parents_anchor.is_empty() { output.push_str(&format!( "{header_level} [`{name}`](#{name}) {{: #{name} }}\n" )); } else { output.push_str(&format!( "{header_level} [`{name}`](#{parents_anchor}_{name}) {{: #{parents_anchor}_{name} }}\n" )); // the anchor used to just be the name, but now it's the group name // for backwards compatibility, we need to keep the old anchor output.push_str(&format!("\n")); } output.push('\n'); if let Some(deprecated) = &field.deprecated { output.push_str("!!! warning \"Deprecated\"\n"); output.push_str(" This option has been deprecated"); if let Some(since) = deprecated.since { write!(output, " in {since}").unwrap(); } output.push('.'); if let Some(message) = deprecated.message { writeln!(output, " {message}").unwrap(); } output.push('\n'); } output.push_str(field.doc); output.push_str("\n\n"); output.push_str(&format!("**Default value**: `{}`\n", field.default)); output.push('\n'); if let Some(possible_values) = field .possible_values .as_ref() .filter(|values| !values.is_empty()) { output.push_str("**Possible values**:\n\n"); for value in possible_values { output.push_str(format!("- {value}\n").as_str()); } } else { output.push_str(&format!("**Type**: `{}`\n", field.value_type)); } output.push('\n'); output.push_str("**Example usage**:\n\n"); match parents[0] { Set::Global { option_type: OptionType::ProjectMetadata, .. } => { output.push_str(&format_code( "pyproject.toml", &format_header( field.scope, field.example, parents, ConfigurationFile::PyprojectToml, ), field.example, )); } Set::Global { option_type: OptionType::Configuration, .. } => { output.push_str(&format_tab( "pyproject.toml", &format_header( field.scope, field.example, parents, ConfigurationFile::PyprojectToml, ), field.example, )); output.push_str(&format_tab( "uv.toml", &format_header( field.scope, field.example, parents, ConfigurationFile::UvToml, ), field.example, )); } _ => {} } output.push('\n'); } fn format_tab(tab_name: &str, header: &str, content: &str) -> String { if header.is_empty() { format!( "=== \"{}\"\n\n ```toml\n{}\n ```\n", tab_name, textwrap::indent(content, " ") ) } else { format!( "=== \"{}\"\n\n ```toml\n {}\n{}\n ```\n", tab_name, header, textwrap::indent(content, " ") ) } } fn format_code(file_name: &str, header: &str, content: &str) -> String { format!("```toml title=\"{file_name}\"\n{header}\n{content}\n```\n") } /// Format the TOML header for the example usage for a given option. /// /// For example: `[tool.uv.pip]`. fn format_header( scope: Option<&str>, example: &str, parents: &[Set], configuration: ConfigurationFile, ) -> String { let tool_parent = match configuration { ConfigurationFile::PyprojectToml => Some("tool.uv"), ConfigurationFile::UvToml => None, }; let header = tool_parent .into_iter() .chain(parents.iter().filter_map(|parent| parent.name())) .chain(scope) .join("."); // Ex) `[[tool.uv.index]]` if example.starts_with(&format!("[[{header}")) { return String::new(); } // Ex) `[tool.uv.sources]` if example.starts_with(&format!("[{header}")) { return String::new(); } if header.is_empty() { String::new() } else { format!("[{header}]") } } #[derive(Debug, Copy, Clone)] enum ConfigurationFile { PyprojectToml, UvToml, } #[derive(Default)] struct CollectOptionsVisitor { groups: Vec<(String, OptionSet)>, fields: Vec<(String, OptionField)>, } impl Visit for CollectOptionsVisitor { fn record_set(&mut self, name: &str, group: OptionSet) { self.groups.push((name.to_owned(), group)); } fn record_field(&mut self, name: &str, field: OptionField) { self.fields.push((name.to_owned(), field)); } } uv-0.9.17+ds1/crates/uv-dev/src/generate_sysconfig_mappings.rs000066400000000000000000000157401520155276700243460ustar00rootroot00000000000000//! Generate sysconfig mappings for supported python-build-standalone *nix platforms. use anstream::println; use anyhow::{Result, bail}; use pretty_assertions::StrComparison; use serde::Deserialize; use std::collections::BTreeMap; use std::fmt::Write; use std::path::PathBuf; use crate::ROOT_DIR; use crate::generate_all::Mode; /// Contains current supported targets const TARGETS_YML_URL: &str = "https://raw.githubusercontent.com/astral-sh/python-build-standalone/refs/tags/20251209/cpython-unix/targets.yml"; #[derive(clap::Args)] pub(crate) struct Args { #[arg(long, default_value_t, value_enum)] pub(crate) mode: Mode, } #[derive(Debug, Deserialize)] struct TargetConfig { host_cc: Option, host_cxx: Option, target_cc: Option, target_cxx: Option, } pub(crate) async fn main(args: &Args) -> Result<()> { let reference_string = generate().await?; let filename = "generated_mappings.rs"; let reference_path = PathBuf::from(ROOT_DIR) .join("crates") .join("uv-python") .join("src") .join("sysconfig") .join(filename); match args.mode { Mode::DryRun => { println!("{reference_string}"); } Mode::Check => match fs_err::read_to_string(reference_path) { Ok(current) => { if current == reference_string { println!("Up-to-date: {filename}"); } else { let comparison = StrComparison::new(¤t, &reference_string); bail!( "{filename} changed, please run `cargo dev generate-sysconfig-metadata`:\n{comparison}" ); } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { bail!("{filename} not found, please run `cargo dev generate-sysconfig-metadata`"); } Err(err) => { bail!( "{filename} changed, please run `cargo dev generate-sysconfig-metadata`:\n{err}" ); } }, Mode::Write => match fs_err::read_to_string(&reference_path) { Ok(current) => { if current == reference_string { println!("Up-to-date: {filename}"); } else { println!("Updating: {filename}"); fs_err::write(reference_path, reference_string.as_bytes())?; } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { println!("Updating: {filename}"); fs_err::write(reference_path, reference_string.as_bytes())?; } Err(err) => { bail!( "{filename} changed, please run `cargo dev generate-sysconfig-metadata`:\n{err}" ); } }, } Ok(()) } async fn generate() -> Result { println!("Downloading python-build-standalone cpython-unix/targets.yml ..."); let body = reqwest::get(TARGETS_YML_URL).await?.text().await?; let parsed: BTreeMap = serde_yaml::from_str(&body)?; let mut replacements: BTreeMap<&str, BTreeMap> = BTreeMap::new(); for targets_config in parsed.values() { for sysconfig_cc_entry in ["CC", "LDSHARED", "BLDSHARED", "LINKCC"] { if let Some(ref from_cc) = targets_config.host_cc { replacements .entry(sysconfig_cc_entry) .or_default() .insert(from_cc.to_owned(), "cc".to_string()); } if let Some(ref from_cc) = targets_config.target_cc { replacements .entry(sysconfig_cc_entry) .or_default() .insert(from_cc.to_owned(), "cc".to_string()); } } for sysconfig_cxx_entry in ["CXX", "LDCXXSHARED"] { if let Some(ref from_cxx) = targets_config.host_cxx { replacements .entry(sysconfig_cxx_entry) .or_default() .insert(from_cxx.to_owned(), "c++".to_string()); } if let Some(ref from_cxx) = targets_config.target_cxx { replacements .entry(sysconfig_cxx_entry) .or_default() .insert(from_cxx.to_owned(), "c++".to_string()); } } } let mut output = String::new(); // Opening statements output.push_str("//! DO NOT EDIT\n"); output.push_str("//!\n"); output.push_str("//! Generated with `cargo run dev generate-sysconfig-metadata`\n"); output.push_str("//! Targets from \n"); output.push_str("//!\n"); // Disable clippy/fmt output.push_str("#![allow(clippy::all)]\n"); output.push_str("#![cfg_attr(any(), rustfmt::skip)]\n\n"); // Begin main code output.push_str("use std::collections::BTreeMap;\n"); output.push_str("use std::sync::LazyLock;\n\n"); output.push_str("use crate::sysconfig::replacements::{ReplacementEntry, ReplacementMode};\n\n"); output.push_str( "/// Mapping for sysconfig keys to lookup and replace with the appropriate entry.\n", ); output.push_str("pub(crate) static DEFAULT_VARIABLE_UPDATES: LazyLock>> = LazyLock::new(|| {\n"); output.push_str(" BTreeMap::from_iter([\n"); // Add Replacement Entries for CC, CXX, etc. for (key, entries) in &replacements { writeln!(output, " (\"{key}\".to_string(), vec![")?; for (from, to) in entries { writeln!( output, " ReplacementEntry {{ mode: ReplacementMode::Partial {{ from: \"{from}\".to_string() }}, to: \"{to}\".to_string() }}," )?; } writeln!(output, " ]),")?; } // Add AR case last output.push_str(" (\"AR\".to_string(), vec![\n"); output.push_str(" ReplacementEntry {\n"); output.push_str(" mode: ReplacementMode::Full,\n"); output.push_str(" to: \"ar\".to_string(),\n"); output.push_str(" },\n"); output.push_str(" ]),\n"); // Closing output.push_str(" ])\n});\n"); Ok(output) } #[cfg(test)] mod tests { use std::env; use anyhow::Result; use uv_static::EnvVars; use crate::generate_all::Mode; use super::{Args, main}; #[tokio::test] async fn test_generate_sysconfig_mappings() -> Result<()> { // Skip this test in CI to avoid redundancy with the dedicated CI job if env::var_os(EnvVars::CI).is_some() { return Ok(()); } let mode = if env::var(EnvVars::UV_UPDATE_SCHEMA).as_deref() == Ok("1") { Mode::Write } else { Mode::Check }; main(&Args { mode }).await } } uv-0.9.17+ds1/crates/uv-dev/src/lib.rs000066400000000000000000000071551520155276700173410ustar00rootroot00000000000000use std::env; use anyhow::Result; use clap::Parser; use tracing::instrument; use uv_settings::EnvironmentOptions; use crate::clear_compile::ClearCompileArgs; use crate::compile::CompileArgs; use crate::generate_all::Args as GenerateAllArgs; use crate::generate_cli_reference::Args as GenerateCliReferenceArgs; use crate::generate_env_vars_reference::Args as GenerateEnvVarsReferenceArgs; use crate::generate_json_schema::Args as GenerateJsonSchemaArgs; use crate::generate_options_reference::Args as GenerateOptionsReferenceArgs; use crate::generate_sysconfig_mappings::Args as GenerateSysconfigMetadataArgs; use crate::list_packages::ListPackagesArgs; #[cfg(feature = "render")] use crate::render_benchmarks::RenderBenchmarksArgs; use crate::validate_zip::ValidateZipArgs; use crate::wheel_metadata::WheelMetadataArgs; mod clear_compile; mod compile; mod generate_all; mod generate_cli_reference; mod generate_env_vars_reference; mod generate_json_schema; mod generate_options_reference; mod generate_sysconfig_mappings; mod list_packages; mod render_benchmarks; mod validate_zip; mod wheel_metadata; const ROOT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../"); #[derive(Parser)] enum Cli { /// Display the metadata for a `.whl` at a given URL. WheelMetadata(WheelMetadataArgs), /// Validate that a `.whl` or `.zip` file at a given URL is a valid ZIP file. ValidateZip(ValidateZipArgs), /// Compile all `.py` to `.pyc` files in the tree. Compile(CompileArgs), /// Remove all `.pyc` in the tree. ClearCompile(ClearCompileArgs), /// List all packages from a Simple API index. ListPackages(ListPackagesArgs), /// Run all code and documentation generation steps. GenerateAll(GenerateAllArgs), /// Generate JSON schema for the TOML configuration file. GenerateJSONSchema(GenerateJsonSchemaArgs), /// Generate the options reference for the documentation. GenerateOptionsReference(GenerateOptionsReferenceArgs), /// Generate the CLI reference for the documentation. GenerateCliReference(GenerateCliReferenceArgs), /// Generate the environment variables reference for the documentation. GenerateEnvVarsReference(GenerateEnvVarsReferenceArgs), /// Generate the sysconfig metadata from derived targets. GenerateSysconfigMetadata(GenerateSysconfigMetadataArgs), #[cfg(feature = "render")] /// Render the benchmarks. RenderBenchmarks(RenderBenchmarksArgs), } #[instrument] // Anchor span to check for overhead pub async fn run() -> Result<()> { let cli = Cli::parse(); let environment = EnvironmentOptions::new()?; match cli { Cli::WheelMetadata(args) => wheel_metadata::wheel_metadata(args, environment).await?, Cli::ValidateZip(args) => validate_zip::validate_zip(args, environment).await?, Cli::Compile(args) => compile::compile(args).await?, Cli::ClearCompile(args) => clear_compile::clear_compile(&args)?, Cli::ListPackages(args) => list_packages::list_packages(args, environment).await?, Cli::GenerateAll(args) => generate_all::main(&args).await?, Cli::GenerateJSONSchema(args) => generate_json_schema::main(&args)?, Cli::GenerateOptionsReference(args) => generate_options_reference::main(&args)?, Cli::GenerateCliReference(args) => generate_cli_reference::main(&args)?, Cli::GenerateEnvVarsReference(args) => generate_env_vars_reference::main(&args)?, Cli::GenerateSysconfigMetadata(args) => generate_sysconfig_mappings::main(&args).await?, #[cfg(feature = "render")] Cli::RenderBenchmarks(args) => render_benchmarks::render_benchmarks(&args)?, } Ok(()) } uv-0.9.17+ds1/crates/uv-dev/src/list_packages.rs000066400000000000000000000017171520155276700214020ustar00rootroot00000000000000use anstream::println; use anyhow::Result; use clap::Parser; use uv_cache::{Cache, CacheArgs}; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; use uv_distribution_types::IndexUrl; use uv_settings::EnvironmentOptions; #[derive(Parser)] pub(crate) struct ListPackagesArgs { /// The Simple API index URL (e.g., /) url: String, #[command(flatten)] cache_args: CacheArgs, } pub(crate) async fn list_packages( args: ListPackagesArgs, environment: EnvironmentOptions, ) -> Result<()> { let cache = Cache::try_from(args.cache_args)?.init().await?; let client = RegistryClientBuilder::new( BaseClientBuilder::default().timeout(environment.http_timeout), cache, ) .build(); let index_url = IndexUrl::parse(&args.url, None)?; let index = client.fetch_simple_index(&index_url).await?; for package_name in index.iter() { println!("{}", package_name); } Ok(()) } uv-0.9.17+ds1/crates/uv-dev/src/main.rs000066400000000000000000000050641520155276700175140ustar00rootroot00000000000000use std::env; use std::path::PathBuf; use std::process::ExitCode; use std::str::FromStr; use std::time::Instant; use anstream::eprintln; use owo_colors::OwoColorize; use tracing::{debug, trace}; use tracing_durations_export::DurationsLayerBuilder; use tracing_durations_export::plot::PlotConfig; use tracing_subscriber::filter::Directive; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{EnvFilter, Layer}; use uv_dev::run; use uv_static::EnvVars; #[tokio::main(flavor = "current_thread")] async fn main() -> ExitCode { let (duration_layer, _guard) = if let Ok(location) = env::var(EnvVars::TRACING_DURATIONS_FILE) { let location = PathBuf::from(location); if let Some(parent) = location.parent() { fs_err::tokio::create_dir_all(&parent) .await .expect("Failed to create parent of TRACING_DURATIONS_FILE"); } let plot_config = PlotConfig { multi_lane: true, min_length: None, remove: Some( ["get_cached_with_callback".to_string()] .into_iter() .collect(), ), ..PlotConfig::default() }; let (layer, guard) = DurationsLayerBuilder::default() .durations_file(&location) .plot_file(location.with_extension("svg")) .plot_config(plot_config) .build() .expect("Couldn't create TRACING_DURATIONS_FILE files"); (Some(layer), Some(guard)) } else { (None, None) }; // Show `INFO` messages from the uv crate, but allow `RUST_LOG` to override. let default_directive = Directive::from_str("uv=info").unwrap(); let filter = EnvFilter::builder() .with_default_directive(default_directive) .from_env() .expect("Valid RUST_LOG directives"); tracing_subscriber::registry() .with(duration_layer) .with( tracing_subscriber::fmt::layer() .with_writer(std::io::stderr) .with_filter(filter), ) .init(); let start = Instant::now(); let result = run().await; debug!("Took {}ms", start.elapsed().as_millis()); if let Err(err) = result { trace!("Error trace: {err:?}"); eprintln!("{}", "uv-dev failed".red().bold()); for err in err.chain() { eprintln!(" {}: {}", "Caused by".red().bold(), err.to_string().trim()); } ExitCode::FAILURE } else { ExitCode::SUCCESS } } uv-0.9.17+ds1/crates/uv-dev/src/render_benchmarks.rs000066400000000000000000000072431520155276700222450ustar00rootroot00000000000000#![cfg(feature = "render")] use std::path::{Path, PathBuf}; use anyhow::{Result, anyhow}; use clap::Parser; use poloto::build; use resvg::usvg_text_layout::{TreeTextToPath, fontdb}; use serde::Deserialize; use tagu::prelude::*; #[derive(Parser)] pub(crate) struct RenderBenchmarksArgs { /// Path to a JSON output from a `hyperfine` benchmark. path: PathBuf, /// Title of the plot. #[clap(long, short)] title: Option, } pub(crate) fn render_benchmarks(args: &RenderBenchmarksArgs) -> Result<()> { let mut results: BenchmarkResults = serde_json::from_slice(&fs_err::read(&args.path)?)?; // Replace the command with a shorter name. (The command typically includes the benchmark name, // but we assume we're running over a single benchmark here.) for result in &mut results.results { if result.command.starts_with("uv") { result.command = "uv".into(); } else if result.command.starts_with("pip-compile") { result.command = "pip-compile".into(); } else if result.command.starts_with("pip-sync") { result.command = "pip-sync".into(); } else if result.command.starts_with("poetry") { result.command = "Poetry".into(); } else if result.command.starts_with("pdm") { result.command = "PDM".into(); } else { return Err(anyhow!("unknown command: {}", result.command)); } } let fontdb = load_fonts(); render_to_png( &plot_benchmark(args.title.as_deref().unwrap_or("Benchmark"), &results)?, &args.path.with_extension("png"), &fontdb, )?; Ok(()) } /// Render a benchmark to an SVG (as a string). fn plot_benchmark(heading: &str, results: &BenchmarkResults) -> Result { let mut data = Vec::new(); for result in &results.results { data.push((result.mean, &result.command)); } let theme = poloto::render::Theme::light(); let theme = theme.append(tagu::build::raw( ".poloto0.poloto_fill{fill: #6340AC !important;}", )); let theme = theme.append(tagu::build::raw( ".poloto_background{fill: white !important;}", )); Ok(build::bar::gen_simple("", data, [0.0]) .label((heading, "Time (s)", "")) .append_to(poloto::header().append(theme)) .render_string()?) } /// Render an SVG to a PNG file. fn render_to_png(data: &str, path: &Path, fontdb: &fontdb::Database) -> Result<()> { let mut tree = resvg::usvg::Tree::from_str(data, &resvg::usvg::Options::default())?; tree.convert_text(fontdb); let fit_to = resvg::usvg::FitTo::Width(1600); let size = fit_to .fit_to(tree.size.to_screen_size()) .ok_or_else(|| anyhow!("failed to fit to screen size"))?; let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width(), size.height()).unwrap(); resvg::render( &tree, fit_to, resvg::tiny_skia::Transform::default(), pixmap.as_mut(), ) .ok_or_else(|| anyhow!("failed to render"))?; fs_err::create_dir_all(path.parent().unwrap())?; pixmap.save_png(path)?; Ok(()) } /// Load the system fonts and set the default font families. fn load_fonts() -> fontdb::Database { let mut fontdb = fontdb::Database::new(); fontdb.load_system_fonts(); fontdb.set_serif_family("Times New Roman"); fontdb.set_sans_serif_family("Arial"); fontdb.set_cursive_family("Comic Sans MS"); fontdb.set_fantasy_family("Impact"); fontdb.set_monospace_family("Courier New"); fontdb } #[derive(Debug, Deserialize)] struct BenchmarkResults { results: Vec, } #[derive(Debug, Deserialize)] struct BenchmarkResult { command: String, mean: f64, } uv-0.9.17+ds1/crates/uv-dev/src/validate_zip.rs000066400000000000000000000024631520155276700212430ustar00rootroot00000000000000use std::ops::Deref; use anyhow::{Result, bail}; use clap::Parser; use futures::TryStreamExt; use tokio_util::compat::FuturesAsyncReadCompatExt; use uv_cache::{Cache, CacheArgs}; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; use uv_pep508::VerbatimUrl; use uv_pypi_types::ParsedUrl; use uv_settings::EnvironmentOptions; #[derive(Parser)] pub(crate) struct ValidateZipArgs { url: VerbatimUrl, #[command(flatten)] cache_args: CacheArgs, } pub(crate) async fn validate_zip( args: ValidateZipArgs, environment: EnvironmentOptions, ) -> Result<()> { let cache = Cache::try_from(args.cache_args)?.init().await?; let client = RegistryClientBuilder::new( BaseClientBuilder::default().timeout(environment.http_timeout), cache, ) .build(); let ParsedUrl::Archive(archive) = ParsedUrl::try_from(args.url.to_url())? else { bail!("Only archive URLs are supported"); }; let response = client .uncached_client(&archive.url) .get(archive.url.deref().clone()) .send() .await?; let reader = response .bytes_stream() .map_err(std::io::Error::other) .into_async_read(); let target = tempfile::TempDir::new()?; uv_extract::stream::unzip(reader.compat(), target.path()).await?; Ok(()) } uv-0.9.17+ds1/crates/uv-dev/src/wheel_metadata.rs000066400000000000000000000027011520155276700215270ustar00rootroot00000000000000use std::str::FromStr; use anstream::println; use anyhow::{Result, bail}; use clap::Parser; use uv_cache::{Cache, CacheArgs}; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; use uv_distribution_filename::WheelFilename; use uv_distribution_types::{BuiltDist, DirectUrlBuiltDist, IndexCapabilities, RemoteSource}; use uv_pep508::VerbatimUrl; use uv_pypi_types::ParsedUrl; use uv_settings::EnvironmentOptions; #[derive(Parser)] pub(crate) struct WheelMetadataArgs { url: VerbatimUrl, #[command(flatten)] cache_args: CacheArgs, } pub(crate) async fn wheel_metadata( args: WheelMetadataArgs, environment: EnvironmentOptions, ) -> Result<()> { let cache = Cache::try_from(args.cache_args)?.init().await?; let client = RegistryClientBuilder::new( BaseClientBuilder::default().timeout(environment.http_timeout), cache, ) .build(); let capabilities = IndexCapabilities::default(); let filename = WheelFilename::from_str(&args.url.filename()?)?; let ParsedUrl::Archive(archive) = ParsedUrl::try_from(args.url.to_url())? else { bail!("Only HTTPS is supported"); }; let metadata = client .wheel_metadata( &BuiltDist::DirectUrl(DirectUrlBuiltDist { filename, location: Box::new(archive.url), url: args.url, }), &capabilities, ) .await?; println!("{metadata:?}"); Ok(()) } uv-0.9.17+ds1/crates/uv-dev/sync_sysconfig_targets.sh000077500000000000000000000015211520155276700225550ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail # Fetch latest python-build-standalone tag latest_tag=$(curl -fsSL -H "Accept: application/json" https://github.com/astral-sh/python-build-standalone/releases/latest | jq -r .tag_name) # Validate we got a tag name back if [[ -z "${latest_tag}" ]]; then echo "Error: Failed to fetch the latest tag from astral-sh/python-build-standalone." >&2 exit 1 fi # Edit the sysconfig mapping endpoints sed -i.bak "s|refs/tags/[^/]\+/cpython-unix|refs/tags/${latest_tag}/cpython-unix|g" src/generate_sysconfig_mappings.rs && rm -f src/generate_sysconfig_mappings.rs.bak sed -i.bak "s|blob/[^/]\+/cpython-unix|blob/${latest_tag}/cpython-unix|g" src/generate_sysconfig_mappings.rs && rm -f src/generate_sysconfig_mappings.rs.bak # Regenerate mappings in case there's any changes cargo dev generate-sysconfig-metadata uv-0.9.17+ds1/crates/uv-dev/test_sdist_building.sh000066400000000000000000000026371520155276700220340ustar00rootroot00000000000000#!/usr/bin/env bash # Simple source distribution building integration test using the tqdm (PEP 517) and geoextract (setup.py) sdists. set -e mkdir -p sdist_building_test_data/sdist if [ ! -f sdist_building_test_data/sdist/tqdm-4.66.1.tar.gz ]; then wget https://files.pythonhosted.org/packages/62/06/d5604a70d160f6a6ca5fd2ba25597c24abd5c5ca5f437263d177ac242308/tqdm-4.66.1.tar.gz -O sdist_building_test_data/sdist/tqdm-4.66.1.tar.gz fi if [ ! -f sdist_building_test_data/sdist/geoextract-0.3.1.tar.gz ]; then wget https://files.pythonhosted.org/packages/c4/00/9d9826a6e1c9139cc7183647f47f6b7acb290fa4c572140aa84a12728e60/geoextract-0.3.1.tar.gz -O sdist_building_test_data/sdist/geoextract-0.3.1.tar.gz fi rm -rf sdist_building_test_data/wheels RUST_LOG=uv_build=debug cargo run --bin uv-dev -- build --wheels sdist_building_test_data/wheels sdist_building_test_data/sdist/tqdm-4.66.1.tar.gz RUST_LOG=uv_build=debug cargo run --bin uv-dev -- build --wheels sdist_building_test_data/wheels sdist_building_test_data/sdist/geoextract-0.3.1.tar.gz # Check that pip accepts the wheels. It would be better to do functional checks virtualenv -p 3.8 -q --clear sdist_building_test_data/.venv sdist_building_test_data/.venv/bin/pip install -q --no-deps sdist_building_test_data/wheels/geoextract-0.3.1-py3-none-any.whl sdist_building_test_data/.venv/bin/pip install -q --no-deps sdist_building_test_data/wheels/tqdm-4.66.1-py3-none-any.whl uv-0.9.17+ds1/crates/uv-dirs/000077500000000000000000000000001520155276700156115ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-dirs/Cargo.toml000066400000000000000000000010741520155276700175430ustar00rootroot00000000000000[package] name = "uv-dirs" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-static = { workspace = true } etcetera = { workspace = true } fs-err = { workspace = true } tracing = { workspace = true } [dev-dependencies] assert_fs = { workspace = true } indoc = { workspace = true } uv-0.9.17+ds1/crates/uv-dirs/README.md000066400000000000000000000010211520155276700170620ustar00rootroot00000000000000 # uv-dirs This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-dirs). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-dirs/src/000077500000000000000000000000001520155276700164005ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-dirs/src/lib.rs000066400000000000000000000207121520155276700175160ustar00rootroot00000000000000use std::{ env, ffi::OsString, path::{Path, PathBuf}, }; use etcetera::BaseStrategy; use uv_static::EnvVars; /// Returns an appropriate user-level directory for storing executables. /// /// This follows, in order: /// /// - `$OVERRIDE_VARIABLE` (if provided) /// - `$XDG_BIN_HOME` /// - `$XDG_DATA_HOME/../bin` /// - `$HOME/.local/bin` /// /// On all platforms. /// /// Returns `None` if a directory cannot be found, i.e., if `$HOME` cannot be resolved. Does not /// check if the directory exists. pub fn user_executable_directory(override_variable: Option<&'static str>) -> Option { override_variable .and_then(std::env::var_os) .and_then(parse_path) .or_else(|| std::env::var_os(EnvVars::XDG_BIN_HOME).and_then(parse_path)) .or_else(|| { std::env::var_os(EnvVars::XDG_DATA_HOME) .and_then(parse_path) .map(|path| path.join("../bin")) }) .or_else(|| { let home_dir = etcetera::home_dir().ok(); home_dir.map(|path| path.join(".local").join("bin")) }) } /// Returns an appropriate user-level directory for storing the cache. /// /// Corresponds to `$XDG_CACHE_HOME/uv` on Unix. pub fn user_cache_dir() -> Option { etcetera::base_strategy::choose_base_strategy() .ok() .map(|dirs| dirs.cache_dir().join("uv")) } /// Returns the legacy cache directory path. /// /// Uses `/Users/user/Library/Application Support/uv` on macOS, in contrast to the new preference /// for using the XDG directories on all Unix platforms. pub fn legacy_user_cache_dir() -> Option { etcetera::base_strategy::choose_native_strategy() .ok() .map(|dirs| dirs.cache_dir().join("uv")) .map(|dir| { if cfg!(windows) { dir.join("cache") } else { dir } }) } /// Returns an appropriate user-level directory for storing application state. /// /// Corresponds to `$XDG_DATA_HOME/uv` on Unix. pub fn user_state_dir() -> Option { etcetera::base_strategy::choose_base_strategy() .ok() .map(|dirs| dirs.data_dir().join("uv")) } /// Returns the legacy state directory path. /// /// Uses `/Users/user/Library/Application Support/uv` on macOS, in contrast to the new preference /// for using the XDG directories on all Unix platforms. pub fn legacy_user_state_dir() -> Option { etcetera::base_strategy::choose_native_strategy() .ok() .map(|dirs| dirs.data_dir().join("uv")) .map(|dir| if cfg!(windows) { dir.join("data") } else { dir }) } /// Return a [`PathBuf`] if the given [`OsString`] is an absolute path. fn parse_path(path: OsString) -> Option { let path = PathBuf::from(path); if path.is_absolute() { Some(path) } else { None } } /// Returns the path to the user configuration directory. /// /// On Windows, use, e.g., C:\Users\Alice\AppData\Roaming /// On Linux and macOS, use `XDG_CONFIG_HOME` or $HOME/.config, e.g., /home/alice/.config. pub fn user_config_dir() -> Option { etcetera::choose_base_strategy() .map(|dirs| dirs.config_dir()) .ok() } pub fn user_uv_config_dir() -> Option { user_config_dir().map(|mut path| { path.push("uv"); path }) } #[cfg(not(windows))] fn locate_system_config_xdg(value: Option<&str>) -> Option { // On Linux and macOS, read the `XDG_CONFIG_DIRS` environment variable. use std::path::Path; let default = "/etc/xdg"; let config_dirs = value.filter(|s| !s.is_empty()).unwrap_or(default); for dir in config_dirs.split(':').take_while(|s| !s.is_empty()) { let uv_toml_path = Path::new(dir).join("uv").join("uv.toml"); if uv_toml_path.is_file() { return Some(uv_toml_path); } } None } #[cfg(windows)] fn locate_system_config_windows(system_drive: impl AsRef) -> Option { // On Windows, use `%SYSTEMDRIVE%\ProgramData\uv\uv.toml` (e.g., `C:\ProgramData`). let candidate = system_drive .as_ref() .join("ProgramData") .join("uv") .join("uv.toml"); candidate.as_path().is_file().then_some(candidate) } /// Returns the path to the system configuration file. /// /// On Unix-like systems, uses the `XDG_CONFIG_DIRS` environment variable (falling back to /// `/etc/xdg/uv/uv.toml` if unset or empty) and then `/etc/uv/uv.toml` /// /// On Windows, uses `%SYSTEMDRIVE%\ProgramData\uv\uv.toml`. pub fn system_config_file() -> Option { #[cfg(windows)] { env::var(EnvVars::SYSTEMDRIVE) .ok() .and_then(|system_drive| locate_system_config_windows(format!("{system_drive}\\"))) } #[cfg(not(windows))] { if let Some(path) = locate_system_config_xdg(env::var(EnvVars::XDG_CONFIG_DIRS).ok().as_deref()) { return Some(path); } // Fallback to `/etc/uv/uv.toml` if `XDG_CONFIG_DIRS` is not set or no valid // path is found. let candidate = Path::new("/etc/uv/uv.toml"); match candidate.try_exists() { Ok(true) => Some(candidate.to_path_buf()), Ok(false) => None, Err(err) => { tracing::warn!("Failed to query system configuration file: {err}"); None } } } } #[cfg(test)] mod test { #[cfg(windows)] use crate::locate_system_config_windows; #[cfg(not(windows))] use crate::locate_system_config_xdg; use assert_fs::fixture::FixtureError; use assert_fs::prelude::*; use indoc::indoc; #[test] #[cfg(not(windows))] fn test_locate_system_config_xdg() -> Result<(), FixtureError> { // Write a `uv.toml` to a temporary directory. let context = assert_fs::TempDir::new()?; context.child("uv").child("uv.toml").write_str(indoc! { r#" [pip] index-url = "https://test.pypi.org/simple" "#, })?; // None assert_eq!(locate_system_config_xdg(None), None); // Empty string assert_eq!(locate_system_config_xdg(Some("")), None); // Single colon assert_eq!(locate_system_config_xdg(Some(":")), None); // Assert that the `system_config_file` function returns the correct path. assert_eq!( locate_system_config_xdg(Some(context.to_str().unwrap())).unwrap(), context.child("uv").child("uv.toml").path() ); // Write a separate `uv.toml` to a different directory. let first = context.child("first"); let first_config = first.child("uv").child("uv.toml"); first_config.write_str("")?; assert_eq!( locate_system_config_xdg(Some( format!("{}:{}", first.to_string_lossy(), context.to_string_lossy()).as_str() )) .unwrap(), first_config.path() ); Ok(()) } #[test] #[cfg(unix)] fn test_locate_system_config_xdg_unix_permissions() -> Result<(), FixtureError> { let context = assert_fs::TempDir::new()?; let config = context.child("uv").child("uv.toml"); config.write_str("")?; fs_err::set_permissions( &context, std::os::unix::fs::PermissionsExt::from_mode(0o000), ) .unwrap(); assert_eq!( locate_system_config_xdg(Some(context.to_str().unwrap())), None ); Ok(()) } #[test] #[cfg(windows)] fn test_windows_config() -> Result<(), FixtureError> { // Write a `uv.toml` to a temporary directory. let context = assert_fs::TempDir::new()?; context .child("ProgramData") .child("uv") .child("uv.toml") .write_str(indoc! { r#" [pip] index-url = "https://test.pypi.org/simple" "#})?; // This is typically only a drive (that is, letter and colon) but we // allow anything, including a path to the test fixtures... assert_eq!( locate_system_config_windows(context.path()).unwrap(), context .child("ProgramData") .child("uv") .child("uv.toml") .path() ); // This does not have a `ProgramData` child, so contains no config. let context = assert_fs::TempDir::new()?; assert_eq!(locate_system_config_windows(context.path()), None); Ok(()) } } uv-0.9.17+ds1/crates/uv-dispatch/000077500000000000000000000000001520155276700164475ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-dispatch/Cargo.toml000066400000000000000000000024161520155276700204020ustar00rootroot00000000000000[package] name = "uv-dispatch" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-build-backend = { workspace = true } uv-build-frontend = { workspace = true } uv-cache = { workspace = true } uv-client = { workspace = true } uv-configuration = { workspace = true } uv-distribution = { workspace = true } uv-distribution-filename = { workspace = true } uv-distribution-types = { workspace = true } uv-git = { workspace = true } uv-install-wheel = { workspace = true } uv-installer = { workspace = true } uv-platform-tags = { workspace = true } uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-resolver = { workspace = true } uv-types = { workspace = true } uv-version = { workspace = true } uv-workspace = { workspace = true } anyhow = { workspace = true } futures = { workspace = true } itertools = { workspace = true } rustc-hash = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } uv-0.9.17+ds1/crates/uv-dispatch/README.md000066400000000000000000000010311520155276700177210ustar00rootroot00000000000000 # uv-dispatch This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-dispatch). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-dispatch/src/000077500000000000000000000000001520155276700172365ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-dispatch/src/lib.rs000066400000000000000000000506031520155276700203560ustar00rootroot00000000000000//! Avoid cyclic crate dependencies between [resolver][`uv_resolver`], //! [installer][`uv_installer`] and [build][`uv_build`] through [`BuildDispatch`] //! implementing [`BuildContext`]. use std::ffi::{OsStr, OsString}; use std::path::Path; use anyhow::{Context, Result}; use futures::FutureExt; use itertools::Itertools; use rustc_hash::FxHashMap; use thiserror::Error; use tracing::{debug, instrument, trace}; use uv_build_backend::check_direct_build; use uv_build_frontend::{SourceBuild, SourceBuildContext}; use uv_cache::Cache; use uv_client::RegistryClient; use uv_configuration::{ BuildKind, BuildOptions, Constraints, IndexStrategy, Reinstall, SourceStrategy, }; use uv_configuration::{BuildOutput, Concurrency}; use uv_distribution::DistributionDatabase; use uv_distribution_filename::DistFilename; use uv_distribution_types::{ CachedDist, ConfigSettings, DependencyMetadata, ExtraBuildRequires, ExtraBuildVariables, Identifier, IndexCapabilities, IndexLocations, IsBuildBackendError, Name, PackageConfigSettings, Requirement, Resolution, SourceDist, VersionOrUrlRef, }; use uv_git::GitResolver; use uv_installer::{InstallationStrategy, Installer, Plan, Planner, Preparer, SitePackages}; use uv_preview::Preview; use uv_pypi_types::Conflicts; use uv_python::{Interpreter, PythonEnvironment}; use uv_resolver::{ ExcludeNewer, FlatIndex, Flexibility, InMemoryIndex, Manifest, OptionsBuilder, PythonRequirement, Resolver, ResolverEnvironment, }; use uv_types::{ AnyErrorBuild, BuildArena, BuildContext, BuildIsolation, BuildStack, EmptyInstalledPackages, HashStrategy, InFlight, }; use uv_workspace::WorkspaceCache; #[derive(Debug, Error)] pub enum BuildDispatchError { #[error(transparent)] BuildFrontend(#[from] AnyErrorBuild), #[error(transparent)] Tags(#[from] uv_platform_tags::TagsError), #[error(transparent)] Resolve(#[from] uv_resolver::ResolveError), #[error(transparent)] Join(#[from] tokio::task::JoinError), #[error(transparent)] Anyhow(#[from] anyhow::Error), #[error(transparent)] Prepare(#[from] uv_installer::PrepareError), } impl IsBuildBackendError for BuildDispatchError { fn is_build_backend_error(&self) -> bool { match self { Self::Tags(_) | Self::Resolve(_) | Self::Join(_) | Self::Anyhow(_) | Self::Prepare(_) => false, Self::BuildFrontend(err) => err.is_build_backend_error(), } } } /// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`] /// documentation. pub struct BuildDispatch<'a> { client: &'a RegistryClient, cache: &'a Cache, constraints: &'a Constraints, interpreter: &'a Interpreter, index_locations: &'a IndexLocations, index_strategy: IndexStrategy, flat_index: &'a FlatIndex, shared_state: SharedState, dependency_metadata: &'a DependencyMetadata, build_isolation: BuildIsolation<'a>, extra_build_requires: &'a ExtraBuildRequires, extra_build_variables: &'a ExtraBuildVariables, link_mode: uv_install_wheel::LinkMode, build_options: &'a BuildOptions, config_settings: &'a ConfigSettings, config_settings_package: &'a PackageConfigSettings, hasher: &'a HashStrategy, exclude_newer: ExcludeNewer, source_build_context: SourceBuildContext, build_extra_env_vars: FxHashMap, sources: SourceStrategy, workspace_cache: WorkspaceCache, concurrency: Concurrency, preview: Preview, } impl<'a> BuildDispatch<'a> { pub fn new( client: &'a RegistryClient, cache: &'a Cache, constraints: &'a Constraints, interpreter: &'a Interpreter, index_locations: &'a IndexLocations, flat_index: &'a FlatIndex, dependency_metadata: &'a DependencyMetadata, shared_state: SharedState, index_strategy: IndexStrategy, config_settings: &'a ConfigSettings, config_settings_package: &'a PackageConfigSettings, build_isolation: BuildIsolation<'a>, extra_build_requires: &'a ExtraBuildRequires, extra_build_variables: &'a ExtraBuildVariables, link_mode: uv_install_wheel::LinkMode, build_options: &'a BuildOptions, hasher: &'a HashStrategy, exclude_newer: ExcludeNewer, sources: SourceStrategy, workspace_cache: WorkspaceCache, concurrency: Concurrency, preview: Preview, ) -> Self { Self { client, cache, constraints, interpreter, index_locations, flat_index, shared_state, dependency_metadata, index_strategy, config_settings, config_settings_package, build_isolation, extra_build_requires, extra_build_variables, link_mode, build_options, hasher, exclude_newer, source_build_context: SourceBuildContext::default(), build_extra_env_vars: FxHashMap::default(), sources, workspace_cache, concurrency, preview, } } /// Set the environment variables to be used when building a source distribution. #[must_use] pub fn with_build_extra_env_vars(mut self, sdist_build_env_variables: I) -> Self where I: IntoIterator, K: AsRef, V: AsRef, { self.build_extra_env_vars = sdist_build_env_variables .into_iter() .map(|(key, value)| (key.as_ref().to_owned(), value.as_ref().to_owned())) .collect(); self } } #[allow(refining_impl_trait)] impl BuildContext for BuildDispatch<'_> { type SourceDistBuilder = SourceBuild; async fn interpreter(&self) -> &Interpreter { self.interpreter } fn cache(&self) -> &Cache { self.cache } fn git(&self) -> &GitResolver { &self.shared_state.git } fn build_arena(&self) -> &BuildArena { &self.shared_state.build_arena } fn capabilities(&self) -> &IndexCapabilities { &self.shared_state.capabilities } fn dependency_metadata(&self) -> &DependencyMetadata { self.dependency_metadata } fn build_options(&self) -> &BuildOptions { self.build_options } fn build_isolation(&self) -> BuildIsolation<'_> { self.build_isolation } fn config_settings(&self) -> &ConfigSettings { self.config_settings } fn config_settings_package(&self) -> &PackageConfigSettings { self.config_settings_package } fn sources(&self) -> SourceStrategy { self.sources } fn locations(&self) -> &IndexLocations { self.index_locations } fn workspace_cache(&self) -> &WorkspaceCache { &self.workspace_cache } fn extra_build_requires(&self) -> &ExtraBuildRequires { self.extra_build_requires } fn extra_build_variables(&self) -> &ExtraBuildVariables { self.extra_build_variables } async fn resolve<'data>( &'data self, requirements: &'data [Requirement], build_stack: &'data BuildStack, ) -> Result { let python_requirement = PythonRequirement::from_interpreter(self.interpreter); let marker_env = self.interpreter.resolver_marker_environment(); let tags = self.interpreter.tags()?; let resolver = Resolver::new( Manifest::simple(requirements.to_vec()).with_constraints(self.constraints.clone()), OptionsBuilder::new() .exclude_newer(self.exclude_newer.clone()) .index_strategy(self.index_strategy) .build_options(self.build_options.clone()) .flexibility(Flexibility::Fixed) .build(), &python_requirement, ResolverEnvironment::specific(marker_env), self.interpreter.markers(), // Conflicting groups only make sense when doing universal resolution. Conflicts::empty(), Some(tags), self.flat_index, &self.shared_state.index, self.hasher, self, EmptyInstalledPackages, DistributionDatabase::new(self.client, self, self.concurrency.downloads) .with_build_stack(build_stack), )?; let resolution = Resolution::from(resolver.resolve().await.with_context(|| { format!( "No solution found when resolving: {}", requirements .iter() .map(|requirement| format!("`{requirement}`")) .join(", ") ) })?); Ok(resolution) } #[instrument( skip(self, resolution, venv), fields( resolution = resolution.distributions().map(ToString::to_string).join(", "), venv = ?venv.root() ) )] async fn install<'data>( &'data self, resolution: &'data Resolution, venv: &'data PythonEnvironment, build_stack: &'data BuildStack, ) -> Result, BuildDispatchError> { debug!( "Installing in {} in {}", resolution .distributions() .map(ToString::to_string) .join(", "), venv.root().display(), ); // Determine the current environment markers. let tags = self.interpreter.tags()?; // Determine the set of installed packages. let site_packages = SitePackages::from_environment(venv)?; let Plan { cached, remote, reinstalls, extraneous: _, } = Planner::new(resolution).build( site_packages, InstallationStrategy::Permissive, &Reinstall::default(), self.build_options, self.hasher, self.index_locations, self.config_settings, self.config_settings_package, self.extra_build_requires(), self.extra_build_variables, self.cache(), venv, tags, )?; // Nothing to do. if remote.is_empty() && cached.is_empty() && reinstalls.is_empty() { debug!("No build requirements to install for build"); return Ok(vec![]); } // Verify that none of the missing distributions are already in the build stack. for dist in &remote { let id = dist.distribution_id(); if build_stack.contains(&id) { return Err(BuildDispatchError::BuildFrontend( uv_build_frontend::Error::CyclicBuildDependency(dist.name().clone()).into(), )); } } // Download any missing distributions. let wheels = if remote.is_empty() { vec![] } else { let preparer = Preparer::new( self.cache, tags, self.hasher, self.build_options, DistributionDatabase::new(self.client, self, self.concurrency.downloads) .with_build_stack(build_stack), ); debug!( "Downloading and building requirement{} for build: {}", if remote.len() == 1 { "" } else { "s" }, remote.iter().map(ToString::to_string).join(", ") ); preparer .prepare(remote, &self.shared_state.in_flight, resolution) .await? }; // Remove any unnecessary packages. if !reinstalls.is_empty() { for dist_info in &reinstalls { let summary = uv_installer::uninstall(dist_info) .await .context("Failed to uninstall build dependencies")?; debug!( "Uninstalled {} ({} file{}, {} director{})", dist_info.name(), summary.file_count, if summary.file_count == 1 { "" } else { "s" }, summary.dir_count, if summary.dir_count == 1 { "y" } else { "ies" }, ); } } // Install the resolved distributions. let mut wheels = wheels.into_iter().chain(cached).collect::>(); if !wheels.is_empty() { debug!( "Installing build requirement{}: {}", if wheels.len() == 1 { "" } else { "s" }, wheels.iter().map(ToString::to_string).join(", ") ); wheels = Installer::new(venv, self.preview) .with_link_mode(self.link_mode) .with_cache(self.cache) .install(wheels) .await .context("Failed to install build dependencies")?; } Ok(wheels) } #[instrument(skip_all, fields(version_id = version_id, subdirectory = ?subdirectory))] async fn setup_build<'data>( &'data self, source: &'data Path, subdirectory: Option<&'data Path>, install_path: &'data Path, version_id: Option<&'data str>, dist: Option<&'data SourceDist>, sources: SourceStrategy, build_kind: BuildKind, build_output: BuildOutput, mut build_stack: BuildStack, ) -> Result { let dist_name = dist.map(uv_distribution_types::Name::name); let dist_version = dist .map(uv_distribution_types::DistributionMetadata::version_or_url) .and_then(|version| match version { VersionOrUrlRef::Version(version) => Some(version), VersionOrUrlRef::Url(_) => None, }); // Note we can only prevent builds by name for packages with names // unless all builds are disabled. if self .build_options .no_build_requirement(dist_name) // We always allow editable builds && !matches!(build_kind, BuildKind::Editable) { let err = if let Some(dist) = dist { uv_build_frontend::Error::NoSourceDistBuild(dist.name().clone()) } else { uv_build_frontend::Error::NoSourceDistBuilds }; return Err(err); } // Push the current distribution onto the build stack, to prevent cyclic dependencies. if let Some(dist) = dist { build_stack.insert(dist.distribution_id()); } // Get package-specific config settings if available; otherwise, use global settings. let config_settings = if let Some(name) = dist_name { if let Some(package_settings) = self.config_settings_package.get(name) { package_settings.clone().merge(self.config_settings.clone()) } else { self.config_settings.clone() } } else { self.config_settings.clone() }; // Get package-specific environment variables if available. let mut environment_variables = self.build_extra_env_vars.clone(); if let Some(name) = dist_name { if let Some(package_vars) = self.extra_build_variables.get(name) { environment_variables.extend( package_vars .iter() .map(|(key, value)| (OsString::from(key), OsString::from(value))), ); } } let builder = SourceBuild::setup( source, subdirectory, install_path, dist_name, dist_version, self.interpreter, self, self.source_build_context.clone(), version_id, self.index_locations, sources, self.workspace_cache(), config_settings, self.build_isolation, self.extra_build_requires, &build_stack, build_kind, environment_variables, build_output, self.concurrency.builds, self.client.credentials_cache(), self.preview, ) .boxed_local() .await?; Ok(builder) } async fn direct_build<'data>( &'data self, source: &'data Path, subdirectory: Option<&'data Path>, output_dir: &'data Path, sources: SourceStrategy, build_kind: BuildKind, version_id: Option<&'data str>, ) -> Result, BuildDispatchError> { let source_tree = if let Some(subdir) = subdirectory { source.join(subdir) } else { source.to_path_buf() }; // Only perform the direct build if the backend is uv in a compatible version. let source_tree_str = source_tree.display().to_string(); let identifier = version_id.unwrap_or_else(|| &source_tree_str); if !check_direct_build(&source_tree, identifier) { trace!("Requirements for direct build not matched: {identifier}"); return Ok(None); } debug!("Performing direct build for {identifier}"); let output_dir = output_dir.to_path_buf(); let filename = tokio::task::spawn_blocking(move || -> Result<_> { let filename = match build_kind { BuildKind::Wheel => { let wheel = uv_build_backend::build_wheel( &source_tree, &output_dir, None, uv_version::version(), sources == SourceStrategy::Enabled, )?; DistFilename::WheelFilename(wheel) } BuildKind::Sdist => { let source_dist = uv_build_backend::build_source_dist( &source_tree, &output_dir, uv_version::version(), sources == SourceStrategy::Enabled, )?; DistFilename::SourceDistFilename(source_dist) } BuildKind::Editable => { let wheel = uv_build_backend::build_editable( &source_tree, &output_dir, None, uv_version::version(), sources == SourceStrategy::Enabled, )?; DistFilename::WheelFilename(wheel) } }; Ok(filename) }) .await??; Ok(Some(filename)) } } /// Shared state used during resolution and installation. /// /// All elements are `Arc`s, so we can clone freely. #[derive(Default, Clone)] pub struct SharedState { /// The resolved Git references. git: GitResolver, /// The discovered capabilities for each registry index. capabilities: IndexCapabilities, /// The fetched package versions and metadata. index: InMemoryIndex, /// The downloaded distributions. in_flight: InFlight, /// Build directories for any PEP 517 builds executed during resolution or installation. build_arena: BuildArena, } impl SharedState { /// Fork the [`SharedState`], creating a new in-memory index and in-flight cache. /// /// State that is universally applicable (like the Git resolver and index capabilities) /// are retained. #[must_use] pub fn fork(&self) -> Self { Self { git: self.git.clone(), capabilities: self.capabilities.clone(), build_arena: self.build_arena.clone(), ..Default::default() } } /// Return the [`GitResolver`] used by the [`SharedState`]. pub fn git(&self) -> &GitResolver { &self.git } /// Return the [`InMemoryIndex`] used by the [`SharedState`]. pub fn index(&self) -> &InMemoryIndex { &self.index } /// Return the [`InFlight`] used by the [`SharedState`]. pub fn in_flight(&self) -> &InFlight { &self.in_flight } /// Return the [`IndexCapabilities`] used by the [`SharedState`]. pub fn capabilities(&self) -> &IndexCapabilities { &self.capabilities } /// Return the [`BuildArena`] used by the [`SharedState`]. pub fn build_arena(&self) -> &BuildArena { &self.build_arena } } uv-0.9.17+ds1/crates/uv-distribution-filename/000077500000000000000000000000001520155276700211455ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-distribution-filename/Cargo.toml000066400000000000000000000014261520155276700231000ustar00rootroot00000000000000[package] name = "uv-distribution-filename" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-cache-key = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-platform-tags = { workspace = true } uv-small-str = { workspace = true } memchr = { workspace = true } rkyv = { workspace = true, features = ["smallvec-1"] } serde = { workspace = true } smallvec = { workspace = true } thiserror = { workspace = true } [dev-dependencies] insta = { workspace = true } uv-0.9.17+ds1/crates/uv-distribution-filename/README.md000066400000000000000000000010631520155276700224240ustar00rootroot00000000000000 # uv-distribution-filename This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-distribution-filename). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-distribution-filename/src/000077500000000000000000000000001520155276700217345ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-distribution-filename/src/build_tag.rs000066400000000000000000000040531520155276700242360ustar00rootroot00000000000000use std::num::ParseIntError; use std::str::FromStr; use uv_small_str::SmallString; #[derive(thiserror::Error, Debug)] pub enum BuildTagError { #[error("must not be empty")] Empty, #[error("must start with a digit")] NoLeadingDigit, #[error(transparent)] ParseInt(#[from] ParseIntError), } /// The optional build tag for a wheel: /// /// > Must start with a digit. Acts as a tie-breaker if two wheel file names are the same in all /// > other respects (i.e. name, version, and other tags). Sort as an empty tuple if unspecified, /// > else sort as a two-item tuple with the first item being the initial digits as an int, and the /// > second item being the remainder of the tag as a str. /// /// See: #[derive( Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, )] #[rkyv(derive(Debug))] pub struct BuildTag(u64, Option); impl FromStr for BuildTag { type Err = BuildTagError; fn from_str(s: &str) -> Result { // A build tag must not be empty. if s.is_empty() { return Err(BuildTagError::Empty); } // A build tag must start with a digit. let (prefix, suffix) = match s.find(|c: char| !c.is_ascii_digit()) { // Ex) `abc` Some(0) => return Err(BuildTagError::NoLeadingDigit), // Ex) `123abc` Some(split) => { let (prefix, suffix) = s.split_at(split); (prefix, Some(suffix)) } // Ex) `123` None => (s, None), }; Ok(Self(prefix.parse::()?, suffix.map(SmallString::from))) } } impl std::fmt::Display for BuildTag { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.1 { Some(suffix) => write!(f, "{}{}", self.0, suffix), None => write!(f, "{}", self.0), } } } uv-0.9.17+ds1/crates/uv-distribution-filename/src/egg.rs000066400000000000000000000075321520155276700230530ustar00rootroot00000000000000use std::str::FromStr; use thiserror::Error; use uv_normalize::{InvalidNameError, PackageName}; use uv_pep440::{Version, VersionParseError}; #[derive(Error, Debug)] pub enum EggInfoFilenameError { #[error("The filename \"{0}\" does not end in `.egg-info`")] InvalidExtension(String), #[error("The `.egg-info` filename \"{0}\" is missing a package name")] MissingPackageName(String), #[error("The `.egg-info` filename \"{0}\" has an invalid package name")] InvalidPackageName(String, InvalidNameError), #[error("The `.egg-info` filename \"{0}\" has an invalid version: {1}")] InvalidVersion(String, VersionParseError), } /// A filename parsed from an `.egg-info` file or directory (e.g., `zstandard-0.22.0-py3.12.egg-info`). /// /// An `.egg-info` filename can contain up to four components, as in: /// /// ```text /// name ["-" version ["-py" pyver ["-" required_platform]]] "." ext /// ``` /// /// See: #[derive(Debug, Clone)] pub struct EggInfoFilename { pub name: PackageName, pub version: Option, } impl EggInfoFilename { /// Parse an `.egg-info` filename, requiring at least a name. pub fn parse(stem: &str) -> Result { // pip uses the following regex: // ```python // EGG_NAME = re.compile( // r""" // (?P[^-]+) ( // -(?P[^-]+) ( // -py(?P[^-]+) ( // -(?P.+) // )? // )? // )? // """, // re.VERBOSE | re.IGNORECASE, // ).match // ``` let mut parts = stem.split('-'); let name = parts .next() .ok_or_else(|| EggInfoFilenameError::MissingPackageName(format!("{stem}.egg-info")))?; let name = PackageName::from_str(name) .map_err(|e| EggInfoFilenameError::InvalidPackageName(format!("{stem}.egg-info"), e))?; let version = parts .next() .map(|s| { Version::from_str(s).map_err(|e| { EggInfoFilenameError::InvalidVersion(format!("{stem}.egg-info"), e) }) }) .transpose()?; Ok(Self { name, version }) } } impl FromStr for EggInfoFilename { type Err = EggInfoFilenameError; fn from_str(filename: &str) -> Result { let stem = filename .strip_suffix(".egg-info") .ok_or_else(|| EggInfoFilenameError::InvalidExtension(filename.to_string()))?; Self::parse(stem) } } #[cfg(test)] mod tests { use super::*; #[test] fn egg_info_filename() { let filename = "zstandard-0.22.0-py3.12-darwin.egg-info"; let parsed = EggInfoFilename::from_str(filename).unwrap(); assert_eq!(parsed.name.as_ref(), "zstandard"); assert_eq!( parsed.version.map(|v| v.to_string()), Some("0.22.0".to_string()) ); let filename = "zstandard-0.22.0-py3.12.egg-info"; let parsed = EggInfoFilename::from_str(filename).unwrap(); assert_eq!(parsed.name.as_ref(), "zstandard"); assert_eq!( parsed.version.map(|v| v.to_string()), Some("0.22.0".to_string()) ); let filename = "zstandard-0.22.0.egg-info"; let parsed = EggInfoFilename::from_str(filename).unwrap(); assert_eq!(parsed.name.as_ref(), "zstandard"); assert_eq!( parsed.version.map(|v| v.to_string()), Some("0.22.0".to_string()) ); let filename = "zstandard.egg-info"; let parsed = EggInfoFilename::from_str(filename).unwrap(); assert_eq!(parsed.name.as_ref(), "zstandard"); assert!(parsed.version.is_none()); } } uv-0.9.17+ds1/crates/uv-distribution-filename/src/expanded_tags.rs000066400000000000000000000371731520155276700251230ustar00rootroot00000000000000use std::str::FromStr; use memchr::memchr; use thiserror::Error; use uv_platform_tags::{ AbiTag, LanguageTag, ParseAbiTagError, ParseLanguageTagError, ParsePlatformTagError, PlatformTag, TagCompatibility, Tags, }; use crate::splitter::MemchrSplitter; use crate::wheel_tag::{WheelTag, WheelTagLarge, WheelTagSmall}; /// The expanded wheel tags as stored in a `WHEEL` file. /// /// For example, if a wheel filename included `py2.py3-none-any`, the `WHEEL` file would include: /// ``` /// Tag: py2-none-any /// Tag: py3-none-any /// ``` /// /// This type stores those expanded tags. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct ExpandedTags(smallvec::SmallVec<[WheelTag; 1]>); impl ExpandedTags { /// Parse a list of expanded wheel tags (e.g., `py3-none-any`). pub fn parse<'a>(tags: impl IntoIterator) -> Result { let tags = tags .into_iter() .map(parse_expanded_tag) .collect::>()?; Ok(Self(tags)) } /// Returns `true` if the wheel is compatible with the given tags. pub fn is_compatible(&self, compatible_tags: &Tags) -> bool { self.0.iter().any(|tag| { compatible_tags.is_compatible(tag.python_tags(), tag.abi_tags(), tag.platform_tags()) }) } /// Return the Python tags in this expanded tag set. pub fn python_tags(&self) -> impl Iterator { self.0.iter().flat_map(WheelTag::python_tags) } /// Return the ABI tags in this expanded tag set. pub fn abi_tags(&self) -> impl Iterator { self.0.iter().flat_map(WheelTag::abi_tags) } /// Return the platform tags in this expanded tag set. pub fn platform_tags(&self) -> impl Iterator { self.0.iter().flat_map(WheelTag::platform_tags) } /// Return the [`TagCompatibility`] of the wheel with the given tags pub fn compatibility(&self, compatible_tags: &Tags) -> TagCompatibility { compatible_tags.compatibility( self.python_tags().copied().collect::>().as_slice(), self.abi_tags().copied().collect::>().as_slice(), self.platform_tags().cloned().collect::>().as_slice(), ) } } #[derive(Error, Debug)] pub enum ExpandedTagError { #[error("The wheel tag \"{0}\" is missing a language tag")] MissingLanguageTag(String), #[error("The wheel tag \"{0}\" is missing an ABI tag")] MissingAbiTag(String), #[error("The wheel tag \"{0}\" is missing a platform tag")] MissingPlatformTag(String), #[error("The wheel tag \"{0}\" contains too many segments")] ExtraSegment(String), #[error("The wheel tag \"{0}\" contains an invalid language tag")] InvalidLanguageTag(String, #[source] ParseLanguageTagError), #[error("The wheel tag \"{0}\" contains an invalid ABI tag")] InvalidAbiTag(String, #[source] ParseAbiTagError), #[error("The wheel tag \"{0}\" contains an invalid platform tag")] InvalidPlatformTag(String, #[source] ParsePlatformTagError), } /// Parse an expanded (i.e., simplified) wheel tag, e.g. `py3-none-any`. /// /// Unlike parsing tags in a wheel filename, each tag in this case is expected to contain exactly /// three segments separated by `-`: a language tag, an ABI tag, and a platform tag; however, /// empirically, some build backends do emit multipart tags (like `cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64`), /// so we allow those too. fn parse_expanded_tag(tag: &str) -> Result { let mut splitter = memchr::Memchr::new(b'-', tag.as_bytes()); if tag.is_empty() { return Err(ExpandedTagError::MissingLanguageTag(tag.to_string())); } let Some(python_tag_index) = splitter.next() else { return Err(ExpandedTagError::MissingAbiTag(tag.to_string())); }; let Some(abi_tag_index) = splitter.next() else { return Err(ExpandedTagError::MissingPlatformTag(tag.to_string())); }; if splitter.next().is_some() { return Err(ExpandedTagError::ExtraSegment(tag.to_string())); } let python_tag = &tag[..python_tag_index]; let abi_tag = &tag[python_tag_index + 1..abi_tag_index]; let platform_tag = &tag[abi_tag_index + 1..]; let is_small = memchr(b'.', tag.as_bytes()).is_none(); if let Some(small) = is_small .then(|| { Some(WheelTagSmall { python_tag: LanguageTag::from_str(python_tag).ok()?, abi_tag: AbiTag::from_str(abi_tag).ok()?, platform_tag: PlatformTag::from_str(platform_tag).ok()?, }) }) .flatten() { Ok(WheelTag::Small { small }) } else { Ok(WheelTag::Large { large: Box::new(WheelTagLarge { build_tag: None, python_tag: MemchrSplitter::split(python_tag, b'.') .map(LanguageTag::from_str) .filter_map(Result::ok) .collect(), abi_tag: MemchrSplitter::split(abi_tag, b'.') .map(AbiTag::from_str) .filter_map(Result::ok) .collect(), platform_tag: MemchrSplitter::split(platform_tag, b'.') .map(PlatformTag::from_str) .filter_map(Result::ok) .collect(), repr: tag.into(), }), }) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_simple_expanded_tag() { let tags = ExpandedTags::parse(vec!["py3-none-any"]).unwrap(); insta::assert_debug_snapshot!(tags, @r" ExpandedTags( [ Small { small: WheelTagSmall { python_tag: Python { major: 3, minor: None, }, abi_tag: None, platform_tag: Any, }, }, ], ) "); } #[test] fn test_parse_multiple_expanded_tags() { let tags = ExpandedTags::parse(vec![ "py2-none-any", "py3-none-any", "cp39-cp39-linux_x86_64", ]) .unwrap(); insta::assert_debug_snapshot!(tags, @r" ExpandedTags( [ Small { small: WheelTagSmall { python_tag: Python { major: 2, minor: None, }, abi_tag: None, platform_tag: Any, }, }, Small { small: WheelTagSmall { python_tag: Python { major: 3, minor: None, }, abi_tag: None, platform_tag: Any, }, }, Small { small: WheelTagSmall { python_tag: CPython { python_version: ( 3, 9, ), }, abi_tag: CPython { gil_disabled: false, python_version: ( 3, 9, ), }, platform_tag: Linux { arch: X86_64, }, }, }, ], ) "); } #[test] fn test_parse_complex_platform_tag() { let tags = ExpandedTags::parse(vec![ "cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64", ]) .unwrap(); insta::assert_debug_snapshot!(tags, @r#" ExpandedTags( [ Large { large: WheelTagLarge { build_tag: None, python_tag: [ CPython { python_version: ( 3, 12, ), }, ], abi_tag: [ CPython { gil_disabled: false, python_version: ( 3, 12, ), }, ], platform_tag: [ Manylinux { major: 2, minor: 17, arch: X86_64, }, Manylinux2014 { arch: X86_64, }, ], repr: "cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64", }, }, ], ) "#); } #[test] fn test_parse_unknown_expanded_tag() { let tags = ExpandedTags::parse(vec!["py3-foo-any"]).unwrap(); insta::assert_debug_snapshot!(tags, @r#" ExpandedTags( [ Large { large: WheelTagLarge { build_tag: None, python_tag: [ Python { major: 3, minor: None, }, ], abi_tag: [], platform_tag: [ Any, ], repr: "py3-foo-any", }, }, ], ) "#); } #[test] fn test_parse_expanded_tag_with_dots() { let tags = ExpandedTags::parse(vec!["py2.py3-none-any"]).unwrap(); insta::assert_debug_snapshot!(tags, @r#" ExpandedTags( [ Large { large: WheelTagLarge { build_tag: None, python_tag: [ Python { major: 2, minor: None, }, Python { major: 3, minor: None, }, ], abi_tag: [ None, ], platform_tag: [ Any, ], repr: "py2.py3-none-any", }, }, ], ) "#); } #[test] fn test_error_missing_language_tag() { let err = ExpandedTags::parse(vec![""]).unwrap_err(); insta::assert_debug_snapshot!(err, @r#" MissingLanguageTag( "", ) "#); } #[test] fn test_error_missing_abi_tag() { let err = ExpandedTags::parse(vec!["py3"]).unwrap_err(); insta::assert_debug_snapshot!(err, @r#" MissingAbiTag( "py3", ) "#); } #[test] fn test_error_missing_platform_tag() { let err = ExpandedTags::parse(vec!["py3-none"]).unwrap_err(); insta::assert_debug_snapshot!(err, @r#" MissingPlatformTag( "py3-none", ) "#); } #[test] fn test_error_extra_segment() { let err = ExpandedTags::parse(vec!["py3-none-any-extra"]).unwrap_err(); insta::assert_debug_snapshot!(err, @r#" ExtraSegment( "py3-none-any-extra", ) "#); } #[test] fn test_parse_expanded_tag_single_segment() { let result = parse_expanded_tag("py3-none-any"); assert!(result.is_ok()); let tag = result.unwrap(); insta::assert_debug_snapshot!(tag, @r" Small { small: WheelTagSmall { python_tag: Python { major: 3, minor: None, }, abi_tag: None, platform_tag: Any, }, } "); } #[test] fn test_parse_expanded_tag_multi_segment() { let result = parse_expanded_tag("cp39.cp310-cp39.cp310-linux_x86_64.linux_i686"); assert!(result.is_ok()); let tag = result.unwrap(); insta::assert_debug_snapshot!(tag, @r#" Large { large: WheelTagLarge { build_tag: None, python_tag: [ CPython { python_version: ( 3, 9, ), }, CPython { python_version: ( 3, 10, ), }, ], abi_tag: [ CPython { gil_disabled: false, python_version: ( 3, 9, ), }, CPython { gil_disabled: false, python_version: ( 3, 10, ), }, ], platform_tag: [ Linux { arch: X86_64, }, Linux { arch: X86, }, ], repr: "cp39.cp310-cp39.cp310-linux_x86_64.linux_i686", }, } "#); } #[test] fn test_parse_expanded_tag_empty() { let result = parse_expanded_tag(""); assert!(result.is_err()); insta::assert_debug_snapshot!(result.unwrap_err(), @r#" MissingLanguageTag( "", ) "#); } #[test] fn test_parse_expanded_tag_one_segment() { let result = parse_expanded_tag("python"); assert!(result.is_err()); insta::assert_debug_snapshot!(result.unwrap_err(), @r#" MissingAbiTag( "python", ) "#); } #[test] fn test_parse_expanded_tag_two_segments() { let result = parse_expanded_tag("py3-none"); assert!(result.is_err()); insta::assert_debug_snapshot!(result.unwrap_err(), @r#" MissingPlatformTag( "py3-none", ) "#); } #[test] fn test_parse_expanded_tag_four_segments() { let result = parse_expanded_tag("py3-none-any-extra"); assert!(result.is_err()); insta::assert_debug_snapshot!(result.unwrap_err(), @r#" ExtraSegment( "py3-none-any-extra", ) "#); } #[test] fn test_expanded_tags_ordering() { let tags1 = ExpandedTags::parse(vec!["py3-none-any"]).unwrap(); let tags2 = ExpandedTags::parse(vec!["py3-none-any"]).unwrap(); let tags3 = ExpandedTags::parse(vec!["py2-none-any"]).unwrap(); assert_eq!(tags1, tags2); assert_ne!(tags1, tags3); } } uv-0.9.17+ds1/crates/uv-distribution-filename/src/extension.rs000066400000000000000000000072731520155276700243270ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use std::path::Path; use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum DistExtension { Wheel, Source(SourceDistExtension), } #[derive( Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, )] #[rkyv(derive(Debug))] pub enum SourceDistExtension { Tar, TarBz2, TarGz, TarLz, TarLzma, TarXz, TarZst, Tbz, Tgz, Tlz, Txz, Zip, } impl DistExtension { /// Extract the [`DistExtension`] from a path. pub fn from_path(path: impl AsRef) -> Result { let Some(extension) = path.as_ref().extension().and_then(|ext| ext.to_str()) else { return Err(ExtensionError::Dist); }; match extension { "whl" => Ok(Self::Wheel), _ => SourceDistExtension::from_path(path) .map(Self::Source) .map_err(|_| ExtensionError::Dist), } } /// Return the name for the extension. pub fn name(&self) -> &'static str { match self { Self::Wheel => "whl", Self::Source(ext) => ext.name(), } } } impl SourceDistExtension { /// Extract the [`SourceDistExtension`] from a path. pub fn from_path(path: impl AsRef) -> Result { /// Returns true if the path is a tar file (e.g., `.tar.gz`). fn is_tar(path: &Path) -> bool { path.file_stem().is_some_and(|stem| { Path::new(stem) .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("tar")) }) } let Some(extension) = path.as_ref().extension().and_then(|ext| ext.to_str()) else { return Err(ExtensionError::SourceDist); }; match extension { "zip" => Ok(Self::Zip), "tar" => Ok(Self::Tar), "tgz" => Ok(Self::Tgz), "tbz" => Ok(Self::Tbz), "txz" => Ok(Self::Txz), "tlz" => Ok(Self::Tlz), "gz" if is_tar(path.as_ref()) => Ok(Self::TarGz), "bz2" if is_tar(path.as_ref()) => Ok(Self::TarBz2), "xz" if is_tar(path.as_ref()) => Ok(Self::TarXz), "lz" if is_tar(path.as_ref()) => Ok(Self::TarLz), "lzma" if is_tar(path.as_ref()) => Ok(Self::TarLzma), "zst" if is_tar(path.as_ref()) => Ok(Self::TarZst), _ => Err(ExtensionError::SourceDist), } } /// Return the name for the extension. pub fn name(&self) -> &'static str { match self { Self::Tar => "tar", Self::TarBz2 => "tar.bz2", Self::TarGz => "tar.gz", Self::TarLz => "tar.lz", Self::TarLzma => "tar.lzma", Self::TarXz => "tar.xz", Self::TarZst => "tar.zst", Self::Tbz => "tbz", Self::Tgz => "tgz", Self::Tlz => "tlz", Self::Txz => "txz", Self::Zip => "zip", } } } impl Display for SourceDistExtension { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(self.name()) } } #[derive(Error, Debug)] pub enum ExtensionError { #[error( "`.whl`, `.tar.gz`, `.zip`, `.tar.bz2`, `.tar.lz`, `.tar.lzma`, `.tar.xz`, `.tar.zst`, `.tar`, `.tbz`, `.tgz`, `.tlz`, or `.txz`" )] Dist, #[error( "`.tar.gz`, `.zip`, `.tar.bz2`, `.tar.lz`, `.tar.lzma`, `.tar.xz`, `.tar.zst`, `.tar`, `.tbz`, `.tgz`, `.tlz`, or `.txz`" )] SourceDist, } uv-0.9.17+ds1/crates/uv-distribution-filename/src/lib.rs000066400000000000000000000066621520155276700230620ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use std::str::FromStr; use uv_normalize::PackageName; use uv_pep440::Version; pub use build_tag::{BuildTag, BuildTagError}; pub use egg::{EggInfoFilename, EggInfoFilenameError}; pub use expanded_tags::{ExpandedTagError, ExpandedTags}; pub use extension::{DistExtension, ExtensionError, SourceDistExtension}; pub use source_dist::{SourceDistFilename, SourceDistFilenameError}; pub use wheel::{WheelFilename, WheelFilenameError}; mod build_tag; mod egg; mod expanded_tags; mod extension; mod source_dist; mod splitter; mod wheel; mod wheel_tag; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum DistFilename { SourceDistFilename(SourceDistFilename), WheelFilename(WheelFilename), } impl DistFilename { /// Parse a filename as wheel or source dist name. pub fn try_from_filename(filename: &str, package_name: &PackageName) -> Option { match DistExtension::from_path(filename) { Ok(DistExtension::Wheel) => { if let Ok(filename) = WheelFilename::from_str(filename) { return Some(Self::WheelFilename(filename)); } } Ok(DistExtension::Source(extension)) => { if let Ok(filename) = SourceDistFilename::parse(filename, extension, package_name) { return Some(Self::SourceDistFilename(filename)); } } Err(_) => {} } None } /// Like [`DistFilename::try_from_normalized_filename`], but without knowing the package name. /// /// Source dist filenames can be ambiguous, e.g. `a-1-1.tar.gz`. Without knowing the package name, we assume that /// source dist filename version doesn't contain minus (the version is normalized). pub fn try_from_normalized_filename(filename: &str) -> Option { if let Ok(filename) = WheelFilename::from_str(filename) { Some(Self::WheelFilename(filename)) } else if let Ok(filename) = SourceDistFilename::parsed_normalized_filename(filename) { Some(Self::SourceDistFilename(filename)) } else { None } } pub fn name(&self) -> &PackageName { match self { Self::SourceDistFilename(filename) => &filename.name, Self::WheelFilename(filename) => &filename.name, } } pub fn version(&self) -> &Version { match self { Self::SourceDistFilename(filename) => &filename.version, Self::WheelFilename(filename) => &filename.version, } } pub fn into_version(self) -> Version { match self { Self::SourceDistFilename(filename) => filename.version, Self::WheelFilename(filename) => filename.version, } } /// Whether the file is a `bdist_wheel` or an `sdist`. pub fn filetype(&self) -> &'static str { match self { Self::SourceDistFilename(_) => "sdist", Self::WheelFilename(_) => "bdist_wheel", } } } impl Display for DistFilename { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::SourceDistFilename(filename) => Display::fmt(filename, f), Self::WheelFilename(filename) => Display::fmt(filename, f), } } } #[cfg(test)] mod tests { use crate::WheelFilename; #[test] fn wheel_filename_size() { assert_eq!(size_of::(), 48); } } uv-0.9.17+ds1/crates/uv-distribution-filename/src/snapshots/000077500000000000000000000000001520155276700237565ustar00rootroot00000000000000uv_distribution_filename__wheel__tests__ok_build_tag.snap000066400000000000000000000015671520155276700374320ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-distribution-filename/src/snapshots--- source: crates/uv-distribution-filename/src/wheel.rs expression: "WheelFilename::from_str(\"foo-1.2.3-202206090410-py3-none-any.whl\")" --- Ok( WheelFilename { name: PackageName( "foo", ), version: "1.2.3", tags: Large { large: WheelTagLarge { build_tag: Some( BuildTag( 202206090410, None, ), ), python_tag: [ Python { major: 3, minor: None, }, ], abi_tag: [ None, ], platform_tag: [ Any, ], repr: "202206090410-py3-none-any", }, }, }, ) uv_distribution_filename__wheel__tests__ok_multiple_tags.snap000066400000000000000000000024311520155276700403400ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-distribution-filename/src/snapshots--- source: crates/uv-distribution-filename/src/wheel.rs expression: "WheelFilename::from_str(\"foo-1.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\")" --- Ok( WheelFilename { name: PackageName( "foo", ), version: "1.2.3", tags: Large { large: WheelTagLarge { build_tag: None, python_tag: [ CPython { python_version: ( 3, 11, ), }, ], abi_tag: [ CPython { gil_disabled: false, python_version: ( 3, 11, ), }, ], platform_tag: [ Manylinux { major: 2, minor: 17, arch: X86_64, }, Manylinux2014 { arch: X86_64, }, ], repr: "cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64", }, }, }, ) uv_distribution_filename__wheel__tests__ok_single_tags.snap000066400000000000000000000010031520155276700377600ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-distribution-filename/src/snapshots--- source: crates/uv-distribution-filename/src/wheel.rs expression: "WheelFilename::from_str(\"foo-1.2.3-py3-none-any.whl\")" --- Ok( WheelFilename { name: PackageName( "foo", ), version: "1.2.3", tags: Small { small: WheelTagSmall { python_tag: Python { major: 3, minor: None, }, abi_tag: None, platform_tag: Any, }, }, }, ) uv-0.9.17+ds1/crates/uv-distribution-filename/src/source_dist.rs000066400000000000000000000170011520155276700246240ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use std::str::FromStr; use crate::SourceDistExtension; use serde::{Deserialize, Serialize}; use thiserror::Error; use uv_normalize::{InvalidNameError, PackageName}; use uv_pep440::{Version, VersionParseError}; /// Note that this is a normalized and not an exact representation, keep the original string if you /// need the latter. #[derive( Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, )] #[rkyv(derive(Debug))] pub struct SourceDistFilename { pub name: PackageName, pub version: Version, pub extension: SourceDistExtension, } impl SourceDistFilename { /// No `FromStr` impl since we need to know the package name to be able to reasonable parse /// these (consider e.g. `a-1-1.zip`) pub fn parse( filename: &str, extension: SourceDistExtension, package_name: &PackageName, ) -> Result { // Drop the extension (e.g., given `tar.gz`, drop `.tar.gz`). if filename.len() <= extension.name().len() + 1 { return Err(SourceDistFilenameError { filename: filename.to_string(), kind: SourceDistFilenameErrorKind::Extension, }); } let stem = &filename[..(filename.len() - (extension.name().len() + 1))]; if stem.len() <= package_name.as_ref().len() + "-".len() { return Err(SourceDistFilenameError { filename: filename.to_string(), kind: SourceDistFilenameErrorKind::Filename(package_name.clone()), }); } let actual_package_name = PackageName::from_str(&stem[..package_name.as_ref().len()]) .map_err(|err| SourceDistFilenameError { filename: filename.to_string(), kind: SourceDistFilenameErrorKind::PackageName(err), })?; if actual_package_name != *package_name { return Err(SourceDistFilenameError { filename: filename.to_string(), kind: SourceDistFilenameErrorKind::Filename(package_name.clone()), }); } // We checked the length above let version = Version::from_str(&stem[package_name.as_ref().len() + "-".len()..]).map_err(|err| { SourceDistFilenameError { filename: filename.to_string(), kind: SourceDistFilenameErrorKind::Version(err), } })?; Ok(Self { name: package_name.clone(), version, extension, }) } /// Like [`SourceDistFilename::parse`], but without knowing the package name. /// /// Source dist filenames can be ambiguous, e.g. `a-1-1.tar.gz`. Without knowing the package name, we assume that /// source dist filename version doesn't contain minus (the version is normalized). pub fn parsed_normalized_filename(filename: &str) -> Result { let Ok(extension) = SourceDistExtension::from_path(filename) else { return Err(SourceDistFilenameError { filename: filename.to_string(), kind: SourceDistFilenameErrorKind::Extension, }); }; // Drop the extension (e.g., given `tar.gz`, drop `.tar.gz`). if filename.len() <= extension.name().len() + 1 { return Err(SourceDistFilenameError { filename: filename.to_string(), kind: SourceDistFilenameErrorKind::Extension, }); } let stem = &filename[..(filename.len() - (extension.name().len() + 1))]; let Some((package_name, version)) = stem.rsplit_once('-') else { return Err(SourceDistFilenameError { filename: filename.to_string(), kind: SourceDistFilenameErrorKind::Minus, }); }; let package_name = PackageName::from_str(package_name).map_err(|err| SourceDistFilenameError { filename: filename.to_string(), kind: SourceDistFilenameErrorKind::PackageName(err), })?; // We checked the length above let version = Version::from_str(version).map_err(|err| SourceDistFilenameError { filename: filename.to_string(), kind: SourceDistFilenameErrorKind::Version(err), })?; Ok(Self { name: package_name, version, extension, }) } } impl Display for SourceDistFilename { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{}-{}.{}", self.name.as_dist_info_name(), self.version, self.extension ) } } #[derive(Error, Debug, Clone)] pub struct SourceDistFilenameError { filename: String, kind: SourceDistFilenameErrorKind, } impl Display for SourceDistFilenameError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "Failed to parse source distribution filename {}: {}", self.filename, self.kind ) } } #[derive(Error, Debug, Clone)] enum SourceDistFilenameErrorKind { #[error("Name doesn't start with package name {0}")] Filename(PackageName), #[error("File extension is invalid")] Extension, #[error("Version section is invalid")] Version(#[from] VersionParseError), #[error(transparent)] PackageName(#[from] InvalidNameError), #[error("Missing name-version separator")] Minus, } #[cfg(test)] mod tests { use std::str::FromStr; use uv_normalize::PackageName; use crate::{SourceDistExtension, SourceDistFilename}; /// Only test already normalized names since the parsing is lossy /// /// /// #[test] fn roundtrip() { for normalized in [ "foo_lib-1.2.3.zip", "foo_lib-1.2.3a3.zip", "foo_lib-1.2.3.tar.gz", "foo_lib-1.2.3.tar.bz2", "foo_lib-1.2.3.tar.zst", "foo_lib-1.2.3.tar.xz", "foo_lib-1.2.3.tar.lz", "foo_lib-1.2.3.tar.lzma", "foo_lib-1.2.3.tgz", "foo_lib-1.2.3.tbz", "foo_lib-1.2.3.tlz", "foo_lib-1.2.3.txz", ] { let ext = SourceDistExtension::from_path(normalized).unwrap(); assert_eq!( SourceDistFilename::parse( normalized, ext, &PackageName::from_str("foo_lib").unwrap() ) .unwrap() .to_string(), normalized ); } } #[test] fn errors() { for invalid in ["b-1.2.3.zip", "a-1.2.3-gamma.3.zip"] { let ext = SourceDistExtension::from_path(invalid).unwrap(); assert!( SourceDistFilename::parse(invalid, ext, &PackageName::from_str("a").unwrap()) .is_err() ); } } #[test] fn name_too_long() { assert!( SourceDistFilename::parse( "foo.zip", SourceDistExtension::Zip, &PackageName::from_str("foo-lib").unwrap() ) .is_err() ); } } uv-0.9.17+ds1/crates/uv-distribution-filename/src/splitter.rs000066400000000000000000000027501520155276700241540ustar00rootroot00000000000000/// A simple splitter that uses `memchr` to find the next delimiter. pub(crate) struct MemchrSplitter<'a> { memchr: memchr::Memchr<'a>, haystack: &'a str, offset: usize, } impl<'a> MemchrSplitter<'a> { #[inline] pub(crate) fn split(haystack: &'a str, delimiter: u8) -> Self { Self { memchr: memchr::Memchr::new(delimiter, haystack.as_bytes()), haystack, offset: 0, } } } impl<'a> Iterator for MemchrSplitter<'a> { type Item = &'a str; #[inline(always)] #[allow(clippy::inline_always)] fn next(&mut self) -> Option { match self.memchr.next() { Some(index) => { let start = self.offset; self.offset = index + 1; Some(&self.haystack[start..index]) } None if self.offset < self.haystack.len() => { let start = self.offset; self.offset = self.haystack.len(); Some(&self.haystack[start..]) } None => None, } } #[inline] fn size_hint(&self) -> (usize, Option) { // We know we'll return at least one item if there's remaining text. let min = usize::from(self.offset < self.haystack.len()); // Maximum possible splits is remaining length divided by 2 (minimum one char between delimiters). let max = (self.haystack.len() - self.offset).div_ceil(2) + min; (min, Some(max)) } } uv-0.9.17+ds1/crates/uv-distribution-filename/src/wheel.rs000066400000000000000000000421251520155276700234120ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use std::hash::Hash; use std::str::FromStr; use memchr::memchr; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use thiserror::Error; use uv_cache_key::cache_digest; use uv_normalize::{InvalidNameError, PackageName}; use uv_pep440::{Version, VersionParseError}; use uv_platform_tags::{ AbiTag, LanguageTag, ParseAbiTagError, ParseLanguageTagError, ParsePlatformTagError, PlatformTag, TagCompatibility, Tags, }; use crate::splitter::MemchrSplitter; use crate::wheel_tag::{WheelTag, WheelTagLarge, WheelTagSmall}; use crate::{BuildTag, BuildTagError}; #[derive( Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, )] #[rkyv(derive(Debug))] pub struct WheelFilename { pub name: PackageName, pub version: Version, tags: WheelTag, } impl FromStr for WheelFilename { type Err = WheelFilenameError; fn from_str(filename: &str) -> Result { let stem = filename.strip_suffix(".whl").ok_or_else(|| { WheelFilenameError::InvalidWheelFileName( filename.to_string(), "Must end with .whl".to_string(), ) })?; Self::parse(stem, filename) } } impl Display for WheelFilename { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{}-{}-{}.whl", self.name.as_dist_info_name(), self.version, self.tags, ) } } impl WheelFilename { /// Create a [`WheelFilename`] from its components. pub fn new( name: PackageName, version: Version, python_tag: LanguageTag, abi_tag: AbiTag, platform_tag: PlatformTag, ) -> Self { Self { name, version, tags: WheelTag::Small { small: WheelTagSmall { python_tag, abi_tag, platform_tag, }, }, } } /// Returns `true` if the wheel is compatible with the given tags. pub fn is_compatible(&self, compatible_tags: &Tags) -> bool { compatible_tags.is_compatible(self.python_tags(), self.abi_tags(), self.platform_tags()) } /// Return the [`TagCompatibility`] of the wheel with the given tags pub fn compatibility(&self, compatible_tags: &Tags) -> TagCompatibility { compatible_tags.compatibility(self.python_tags(), self.abi_tags(), self.platform_tags()) } /// The wheel filename without the extension. pub fn stem(&self) -> String { format!( "{}-{}-{}", self.name.as_dist_info_name(), self.version, self.tags ) } /// Returns a consistent cache key with a maximum length of 64 characters. /// /// Prefers `{version}-{tags}` if such an identifier fits within the maximum allowed length; /// otherwise, uses a truncated version of the version and a digest of the tags. pub fn cache_key(&self) -> String { const CACHE_KEY_MAX_LEN: usize = 64; let full = format!("{}-{}", self.version, self.tags); if full.len() <= CACHE_KEY_MAX_LEN { return full; } // Create a digest of the tag string (instead of its individual fields) to retain // compatibility across platforms, Rust versions, etc. let digest = cache_digest(&format!("{}", self.tags)); // Truncate the version, but avoid trailing dots, plus signs, etc. to avoid ambiguity. let version_width = CACHE_KEY_MAX_LEN - 1 /* dash */ - 16 /* digest */; let mut version = self.version.to_string(); // PANIC SAFETY: version strings can only contain ASCII characters. version.truncate(version_width); let version = version.trim_end_matches(['.', '+']); format!("{version}-{digest}") } /// Return the wheel's Python tags. pub fn python_tags(&self) -> &[LanguageTag] { self.tags.python_tags() } /// Return the wheel's ABI tags. pub fn abi_tags(&self) -> &[AbiTag] { self.tags.abi_tags() } /// Return the wheel's platform tags. pub fn platform_tags(&self) -> &[PlatformTag] { self.tags.platform_tags() } /// Return the wheel's build tag, if present. pub fn build_tag(&self) -> Option<&BuildTag> { self.tags.build_tag() } /// Parse a wheel filename from the stem (e.g., `foo-1.2.3-py3-none-any`). pub fn from_stem(stem: &str) -> Result { // The wheel stem should not contain the `.whl` extension. if std::path::Path::new(stem) .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("whl")) { return Err(WheelFilenameError::UnexpectedExtension(stem.to_string())); } Self::parse(stem, stem) } /// Parse a wheel filename from the stem (e.g., `foo-1.2.3-py3-none-any`). /// /// The originating `filename` is used for high-fidelity error messages. fn parse(stem: &str, filename: &str) -> Result { // The wheel filename should contain either five or six entries. If six, then the third // entry is the build tag. If five, then the third entry is the Python tag. // https://www.python.org/dev/peps/pep-0427/#file-name-convention let mut splitter = memchr::Memchr::new(b'-', stem.as_bytes()); let Some(version) = splitter.next() else { return Err(WheelFilenameError::InvalidWheelFileName( filename.to_string(), "Must have a version".to_string(), )); }; let Some(build_tag_or_python_tag) = splitter.next() else { return Err(WheelFilenameError::InvalidWheelFileName( filename.to_string(), "Must have a Python tag".to_string(), )); }; let Some(python_tag_or_abi_tag) = splitter.next() else { return Err(WheelFilenameError::InvalidWheelFileName( filename.to_string(), "Must have an ABI tag".to_string(), )); }; let Some(abi_tag_or_platform_tag) = splitter.next() else { return Err(WheelFilenameError::InvalidWheelFileName( filename.to_string(), "Must have a platform tag".to_string(), )); }; let (name, version, build_tag, python_tag, abi_tag, platform_tag, is_small) = if let Some(platform_tag) = splitter.next() { if splitter.next().is_some() { return Err(WheelFilenameError::InvalidWheelFileName( filename.to_string(), "Must have 5 or 6 components, but has more".to_string(), )); } ( &stem[..version], &stem[version + 1..build_tag_or_python_tag], Some(&stem[build_tag_or_python_tag + 1..python_tag_or_abi_tag]), &stem[python_tag_or_abi_tag + 1..abi_tag_or_platform_tag], &stem[abi_tag_or_platform_tag + 1..platform_tag], &stem[platform_tag + 1..], // Always take the slow path if a build tag is present. false, ) } else { ( &stem[..version], &stem[version + 1..build_tag_or_python_tag], None, &stem[build_tag_or_python_tag + 1..python_tag_or_abi_tag], &stem[python_tag_or_abi_tag + 1..abi_tag_or_platform_tag], &stem[abi_tag_or_platform_tag + 1..], // Determine whether any of the tag types contain a period, which would indicate // that at least one of the tag types includes multiple tags (which in turn // necessitates taking the slow path). memchr(b'.', &stem.as_bytes()[build_tag_or_python_tag..]).is_none(), ) }; let name = PackageName::from_str(name) .map_err(|err| WheelFilenameError::InvalidPackageName(filename.to_string(), err))?; let version = Version::from_str(version) .map_err(|err| WheelFilenameError::InvalidVersion(filename.to_string(), err))?; let build_tag = build_tag .map(|build_tag| { BuildTag::from_str(build_tag) .map_err(|err| WheelFilenameError::InvalidBuildTag(filename.to_string(), err)) }) .transpose()?; let tags = if let Some(small) = is_small .then(|| { Some(WheelTagSmall { python_tag: LanguageTag::from_str(python_tag).ok()?, abi_tag: AbiTag::from_str(abi_tag).ok()?, platform_tag: PlatformTag::from_str(platform_tag).ok()?, }) }) .flatten() { WheelTag::Small { small } } else { // Store the plaintext representation of the tags. let repr = &stem[build_tag_or_python_tag + 1..]; WheelTag::Large { large: Box::new(WheelTagLarge { build_tag, python_tag: MemchrSplitter::split(python_tag, b'.') .map(LanguageTag::from_str) .filter_map(Result::ok) .collect(), abi_tag: MemchrSplitter::split(abi_tag, b'.') .map(AbiTag::from_str) .filter_map(Result::ok) .collect(), platform_tag: MemchrSplitter::split(platform_tag, b'.') .map(PlatformTag::from_str) .filter_map(Result::ok) .collect(), repr: repr.into(), }), } }; Ok(Self { name, version, tags, }) } } impl<'de> Deserialize<'de> for WheelFilename { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct Visitor; impl de::Visitor<'_> for Visitor { type Value = WheelFilename; fn expecting(&self, f: &mut Formatter) -> std::fmt::Result { f.write_str("a string") } fn visit_str(self, v: &str) -> Result { WheelFilename::from_str(v).map_err(de::Error::custom) } } deserializer.deserialize_str(Visitor) } } impl Serialize for WheelFilename { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.to_string()) } } #[derive(Error, Debug)] pub enum WheelFilenameError { #[error("The wheel filename \"{0}\" is invalid: {1}")] InvalidWheelFileName(String, String), #[error("The wheel filename \"{0}\" has an invalid version: {1}")] InvalidVersion(String, VersionParseError), #[error("The wheel filename \"{0}\" has an invalid package name")] InvalidPackageName(String, InvalidNameError), #[error("The wheel filename \"{0}\" has an invalid build tag: {1}")] InvalidBuildTag(String, BuildTagError), #[error("The wheel filename \"{0}\" has an invalid language tag: {1}")] InvalidLanguageTag(String, ParseLanguageTagError), #[error("The wheel filename \"{0}\" has an invalid ABI tag: {1}")] InvalidAbiTag(String, ParseAbiTagError), #[error("The wheel filename \"{0}\" has an invalid platform tag: {1}")] InvalidPlatformTag(String, ParsePlatformTagError), #[error("The wheel filename \"{0}\" is missing a language tag")] MissingLanguageTag(String), #[error("The wheel filename \"{0}\" is missing an ABI tag")] MissingAbiTag(String), #[error("The wheel filename \"{0}\" is missing a platform tag")] MissingPlatformTag(String), #[error("The wheel stem \"{0}\" has an unexpected extension")] UnexpectedExtension(String), } #[cfg(test)] mod tests { use super::*; #[test] fn err_not_whl_extension() { let err = WheelFilename::from_str("foo.rs").unwrap_err(); insta::assert_snapshot!(err, @r###"The wheel filename "foo.rs" is invalid: Must end with .whl"###); } #[test] fn err_1_part_empty() { let err = WheelFilename::from_str(".whl").unwrap_err(); insta::assert_snapshot!(err, @r###"The wheel filename ".whl" is invalid: Must have a version"###); } #[test] fn err_1_part_no_version() { let err = WheelFilename::from_str("foo.whl").unwrap_err(); insta::assert_snapshot!(err, @r###"The wheel filename "foo.whl" is invalid: Must have a version"###); } #[test] fn err_2_part_no_pythontag() { let err = WheelFilename::from_str("foo-1.2.3.whl").unwrap_err(); insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3.whl" is invalid: Must have a Python tag"###); } #[test] fn err_3_part_no_abitag() { let err = WheelFilename::from_str("foo-1.2.3-py3.whl").unwrap_err(); insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-py3.whl" is invalid: Must have an ABI tag"###); } #[test] fn err_4_part_no_platformtag() { let err = WheelFilename::from_str("foo-1.2.3-py3-none.whl").unwrap_err(); insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-py3-none.whl" is invalid: Must have a platform tag"###); } #[test] fn err_too_many_parts() { let err = WheelFilename::from_str("foo-1.2.3-202206090410-py3-none-any-whoops.whl").unwrap_err(); insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-202206090410-py3-none-any-whoops.whl" is invalid: Must have 5 or 6 components, but has more"###); } #[test] fn err_invalid_package_name() { let err = WheelFilename::from_str("f!oo-1.2.3-py3-none-any.whl").unwrap_err(); insta::assert_snapshot!(err, @r###"The wheel filename "f!oo-1.2.3-py3-none-any.whl" has an invalid package name"###); } #[test] fn err_invalid_version() { let err = WheelFilename::from_str("foo-x.y.z-py3-none-any.whl").unwrap_err(); insta::assert_snapshot!(err, @r###"The wheel filename "foo-x.y.z-py3-none-any.whl" has an invalid version: expected version to start with a number, but no leading ASCII digits were found"###); } #[test] fn err_invalid_build_tag() { let err = WheelFilename::from_str("foo-1.2.3-tag-py3-none-any.whl").unwrap_err(); insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-tag-py3-none-any.whl" has an invalid build tag: must start with a digit"###); } #[test] fn ok_single_tags() { insta::assert_debug_snapshot!(WheelFilename::from_str("foo-1.2.3-py3-none-any.whl")); } #[test] fn ok_multiple_tags() { insta::assert_debug_snapshot!(WheelFilename::from_str( "foo-1.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" )); } #[test] fn ok_build_tag() { insta::assert_debug_snapshot!(WheelFilename::from_str( "foo-1.2.3-202206090410-py3-none-any.whl" )); } #[test] fn from_and_to_string() { let wheel_names = &[ "django_allauth-0.51.0-py3-none-any.whl", "osm2geojson-0.2.4-py3-none-any.whl", "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", ]; for wheel_name in wheel_names { assert_eq!( WheelFilename::from_str(wheel_name).unwrap().to_string(), *wheel_name ); } } #[test] fn cache_key() { // Short names should use `version-tags` format. let filename = WheelFilename::from_str("django_allauth-0.51.0-py3-none-any.whl").unwrap(); insta::assert_snapshot!(filename.cache_key(), @"0.51.0-py3-none-any"); // Common `manylinux` names should use still use the `version-tags` format. let filename = WheelFilename::from_str( "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", ) .unwrap(); insta::assert_snapshot!(filename.cache_key(), @"1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64"); // But larger names should use the `truncated(version)-digest(tags)` format. let filename = WheelFilename::from_str( "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_2.whl", ) .unwrap(); insta::assert_snapshot!(filename.cache_key(), @"1.26.2-5a2adc379b2dc214"); // Larger versions should get truncated. let filename = WheelFilename::from_str( "example-1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.1.2.3.4.5.6.7.8.9.0.1.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" ).unwrap(); insta::assert_snapshot!(filename.cache_key(), @"1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.1.2-80bf8598e9647cf7"); } } uv-0.9.17+ds1/crates/uv-distribution-filename/src/wheel_tag.rs000066400000000000000000000106341520155276700242450ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use crate::BuildTag; use uv_platform_tags::{AbiTag, LanguageTag, PlatformTag}; use uv_small_str::SmallString; /// A [`SmallVec`] type for storing tags. /// /// Wheels tend to include a single language, ABI, and platform tag, so we use a [`SmallVec`] with a /// capacity of 1 to optimize for this common case. pub(crate) type TagSet = smallvec::SmallVec<[T; 3]>; /// The portion of the wheel filename following the name and version: the optional build tag, along /// with the Python tag(s), ABI tag(s), and platform tag(s). /// /// Most wheels consist of a single Python, ABI, and platform tag (and no build tag). We represent /// such wheels with [`WheelTagSmall`], a variant with a smaller memory footprint and (generally) /// zero allocations. The [`WheelTagLarge`] variant is used for wheels with multiple tags, a build /// tag, or an unsupported tag (i.e., a tag that can't be represented by [`LanguageTag`], /// [`AbiTag`], or [`PlatformTag`]). (Unsupported tags are filtered out, but retained in the display /// representation of [`WheelTagLarge`].) #[derive( Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, )] #[rkyv(derive(Debug))] pub(crate) enum WheelTag { Small { small: WheelTagSmall }, Large { large: Box }, } impl WheelTag { /// Return the Python tags. pub(crate) fn python_tags(&self) -> &[LanguageTag] { match self { Self::Small { small } => std::slice::from_ref(&small.python_tag), Self::Large { large } => large.python_tag.as_slice(), } } /// Return the ABI tags. pub(crate) fn abi_tags(&self) -> &[AbiTag] { match self { Self::Small { small } => std::slice::from_ref(&small.abi_tag), Self::Large { large } => large.abi_tag.as_slice(), } } /// Return the platform tags. pub(crate) fn platform_tags(&self) -> &[PlatformTag] { match self { Self::Small { small } => std::slice::from_ref(&small.platform_tag), Self::Large { large } => large.platform_tag.as_slice(), } } /// Return the build tag, if present. pub(crate) fn build_tag(&self) -> Option<&BuildTag> { match self { Self::Small { .. } => None, Self::Large { large } => large.build_tag.as_ref(), } } } impl Display for WheelTag { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Small { small } => write!(f, "{small}"), Self::Large { large } => write!(f, "{large}"), } } } #[derive( Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, )] #[rkyv(derive(Debug))] #[allow(clippy::struct_field_names)] pub(crate) struct WheelTagSmall { /// The Python tag, e.g., `py3` in `1.2.3-py3-none-any`. pub(crate) python_tag: LanguageTag, /// The ABI tag, e.g., `none` in `1.2.3-py3-none-any`. pub(crate) abi_tag: AbiTag, /// The platform tag, e.g., `none` in `1.2.3-py3-none-any`. pub(crate) platform_tag: PlatformTag, } impl Display for WheelTagSmall { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{}-{}-{}", self.python_tag, self.abi_tag, self.platform_tag ) } } #[derive( Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, )] #[rkyv(derive(Debug))] #[allow(clippy::struct_field_names)] pub(crate) struct WheelTagLarge { /// The optional build tag, e.g., `73` in `1.2.3-73-py3-none-any`. pub(crate) build_tag: Option, /// The Python tag(s), e.g., `py3` in `1.2.3-73-py3-none-any`. pub(crate) python_tag: TagSet, /// The ABI tag(s), e.g., `none` in `1.2.3-73-py3-none-any`. pub(crate) abi_tag: TagSet, /// The platform tag(s), e.g., `none` in `1.2.3-73-py3-none-any`. pub(crate) platform_tag: TagSet, /// The string representation of the tag. /// /// Preserves any unsupported tags that were filtered out when parsing the wheel filename. pub(crate) repr: SmallString, } impl Display for WheelTagLarge { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.repr) } } uv-0.9.17+ds1/crates/uv-distribution-types/000077500000000000000000000000001520155276700205315ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-distribution-types/Cargo.toml000066400000000000000000000032061520155276700224620ustar00rootroot00000000000000[package] name = "uv-distribution-types" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-auth = { workspace = true, features = ["schemars"] } uv-cache-info = { workspace = true } uv-cache-key = { workspace = true } uv-distribution-filename = { workspace = true } uv-fs = { workspace = true } uv-git-types = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-install-wheel = { workspace = true } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } uv-redacted = { workspace = true } uv-small-str = { workspace = true } uv-warnings = { workspace = true } arcstr = { workspace = true } bitflags = { workspace = true } fs-err = { workspace = true } http = { workspace = true } itertools = { workspace = true } jiff = { workspace = true } owo-colors = { workspace = true } percent-encoding = { workspace = true } petgraph = { workspace = true } rkyv = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } url = { workspace = true } version-ranges = { workspace = true } [dev-dependencies] toml = { workspace = true } [features] schemars = ["dep:schemars", "uv-redacted/schemars"] uv-0.9.17+ds1/crates/uv-distribution-types/README.md000066400000000000000000000010551520155276700220110ustar00rootroot00000000000000 # uv-distribution-types This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-distribution-types). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-distribution-types/src/000077500000000000000000000000001520155276700213205ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-distribution-types/src/annotation.rs000066400000000000000000000065111520155276700240430ustar00rootroot00000000000000use std::collections::{BTreeMap, BTreeSet}; use uv_fs::Simplified; use uv_normalize::PackageName; use uv_pep508::RequirementOrigin; /// Source of a dependency, e.g., a `-r requirements.txt` file. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum SourceAnnotation { /// A `-c constraints.txt` file. Constraint(RequirementOrigin), /// An `--override overrides.txt` file. Override(RequirementOrigin), /// A `-r requirements.txt` file. Requirement(RequirementOrigin), } impl std::fmt::Display for SourceAnnotation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Requirement(origin) => match origin { RequirementOrigin::File(path) => { write!(f, "-r {}", path.portable_display()) } RequirementOrigin::Project(path, project_name) => { write!(f, "{project_name} ({})", path.portable_display()) } RequirementOrigin::Group(path, project_name, group) => { if let Some(project_name) = project_name { write!(f, "{project_name} ({}:{group})", path.portable_display()) } else { write!(f, "({}:{group})", path.portable_display()) } } RequirementOrigin::Workspace => { write!(f, "(workspace)") } }, Self::Constraint(origin) => { write!(f, "-c {}", origin.path().portable_display()) } Self::Override(origin) => match origin { RequirementOrigin::File(path) => { write!(f, "--override {}", path.portable_display()) } RequirementOrigin::Project(path, project_name) => { // Project is not used for override write!(f, "--override {project_name} ({})", path.portable_display()) } RequirementOrigin::Group(path, project_name, group) => { // Group is not used for override if let Some(project_name) = project_name { write!( f, "--override {project_name} ({}:{group})", path.portable_display() ) } else { write!(f, "--override ({}:{group})", path.portable_display()) } } RequirementOrigin::Workspace => { write!(f, "--override (workspace)") } }, } } } /// A collection of source annotations. #[derive(Default, Debug, Clone)] pub struct SourceAnnotations(BTreeMap>); impl SourceAnnotations { /// Add a source annotation to the collection for the given package. pub fn add(&mut self, package: &PackageName, annotation: SourceAnnotation) { self.0 .entry(package.clone()) .or_default() .insert(annotation); } /// Return the source annotations for a given package. pub fn get(&self, package: &PackageName) -> Option<&BTreeSet> { self.0.get(package) } } uv-0.9.17+ds1/crates/uv-distribution-types/src/any.rs000066400000000000000000000051631520155276700224620ustar00rootroot00000000000000use std::hash::Hash; use uv_cache_key::CanonicalUrl; use uv_normalize::PackageName; use uv_pep440::Version; use crate::cached::CachedDist; use crate::installed::InstalledDist; use crate::{InstalledMetadata, InstalledVersion, Name}; /// A distribution which is either installable, is a wheel in our cache or is already installed. /// /// Note equality and hash operations are only based on the name and canonical version, not the /// kind. #[derive(Debug, Clone, Eq)] #[allow(clippy::large_enum_variant)] pub enum LocalDist { Cached(CachedDist, CanonicalVersion), Installed(InstalledDist, CanonicalVersion), } impl LocalDist { fn canonical_version(&self) -> &CanonicalVersion { match self { Self::Cached(_, version) => version, Self::Installed(_, version) => version, } } } impl Name for LocalDist { fn name(&self) -> &PackageName { match self { Self::Cached(dist, _) => dist.name(), Self::Installed(dist, _) => dist.name(), } } } impl InstalledMetadata for LocalDist { fn installed_version(&self) -> InstalledVersion<'_> { match self { Self::Cached(dist, _) => dist.installed_version(), Self::Installed(dist, _) => dist.installed_version(), } } } impl From for LocalDist { fn from(dist: CachedDist) -> Self { let version = CanonicalVersion::from(dist.installed_version()); Self::Cached(dist, version) } } impl From for LocalDist { fn from(dist: InstalledDist) -> Self { let version = CanonicalVersion::from(dist.installed_version()); Self::Installed(dist, version) } } impl Hash for LocalDist { fn hash(&self, state: &mut H) { self.name().hash(state); self.canonical_version().hash(state); } } impl PartialEq for LocalDist { fn eq(&self, other: &Self) -> bool { self.name() == other.name() && self.canonical_version() == other.canonical_version() } } /// Like [`InstalledVersion`], but with [`CanonicalUrl`] to ensure robust URL comparisons. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum CanonicalVersion { Version(Version), Url(CanonicalUrl, Version), } impl From> for CanonicalVersion { fn from(installed_version: InstalledVersion<'_>) -> Self { match installed_version { InstalledVersion::Version(version) => Self::Version(version.clone()), InstalledVersion::Url(url, version) => { Self::Url(CanonicalUrl::new(url), version.clone()) } } } } uv-0.9.17+ds1/crates/uv-distribution-types/src/build_info.rs000066400000000000000000000040041520155276700237760ustar00rootroot00000000000000use uv_cache_key::{CacheKey, CacheKeyHasher, cache_digest}; use crate::{BuildVariables, ConfigSettings, ExtraBuildRequirement}; /// A digest representing the build settings, such as build dependencies or other build-time /// configuration. #[derive(Default, Debug, Clone, Hash, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub struct BuildInfo { #[serde(default, skip_serializing_if = "ConfigSettings::is_empty")] config_settings: ConfigSettings, #[serde(default, skip_serializing_if = "Vec::is_empty")] extra_build_requires: Vec, #[serde(default, skip_serializing_if = "BuildVariables::is_empty")] extra_build_variables: BuildVariables, } impl CacheKey for BuildInfo { fn cache_key(&self, state: &mut CacheKeyHasher) { self.config_settings.cache_key(state); self.extra_build_requires.cache_key(state); self.extra_build_variables.cache_key(state); } } impl BuildInfo { /// Creates a [`BuildInfo`] instance with the given configuration settings, extra build /// dependencies, and extra build variables. pub fn from_settings( config_settings: &ConfigSettings, extra_build_dependencies: &[ExtraBuildRequirement], extra_build_variables: Option<&BuildVariables>, ) -> Self { Self { config_settings: config_settings.clone(), extra_build_requires: extra_build_dependencies.to_vec(), extra_build_variables: extra_build_variables.cloned().unwrap_or_default(), } } /// Returns `true` if the [`BuildInfo`] is empty, meaning it has no configuration settings, pub fn is_empty(&self) -> bool { self.config_settings.is_empty() && self.extra_build_requires.is_empty() && self.extra_build_variables.is_empty() } /// Return the cache shard for this [`BuildInfo`]. pub fn cache_shard(&self) -> Option { if self.is_empty() { None } else { Some(cache_digest(self)) } } } uv-0.9.17+ds1/crates/uv-distribution-types/src/build_requires.rs000066400000000000000000000154701520155276700247130ustar00rootroot00000000000000use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use uv_cache_key::{CacheKey, CacheKeyHasher}; use uv_normalize::PackageName; use crate::{Name, Requirement, RequirementSource, Resolution}; #[derive(Debug, thiserror::Error)] pub enum ExtraBuildRequiresError { #[error( "`{0}` was declared as an extra build dependency with `match-runtime = true`, but was not found in the resolution" )] NotFound(PackageName), #[error( "Dependencies marked with `match-runtime = true` cannot include version specifiers, but found: `{0}{1}`" )] VersionSpecifiersNotAllowed(PackageName, Box), #[error( "Dependencies marked with `match-runtime = true` cannot include URL constraints, but found: `{0}{1}`" )] UrlNotAllowed(PackageName, Box), } /// Lowered extra build dependencies with source resolution applied. #[derive(Debug, Clone, Default)] pub struct ExtraBuildRequires(BTreeMap>); impl std::ops::Deref for ExtraBuildRequires { type Target = BTreeMap>; fn deref(&self) -> &Self::Target { &self.0 } } impl std::ops::DerefMut for ExtraBuildRequires { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl IntoIterator for ExtraBuildRequires { type Item = (PackageName, Vec); type IntoIter = std::collections::btree_map::IntoIter>; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl FromIterator<(PackageName, Vec)> for ExtraBuildRequires { fn from_iter)>>( iter: T, ) -> Self { Self(iter.into_iter().collect()) } } #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct ExtraBuildRequirement { /// The underlying [`Requirement`] for the build requirement. pub requirement: Requirement, /// Whether this build requirement should match the runtime environment. pub match_runtime: bool, } impl From for Requirement { fn from(value: ExtraBuildRequirement) -> Self { value.requirement } } impl CacheKey for ExtraBuildRequirement { fn cache_key(&self, state: &mut CacheKeyHasher) { self.requirement.cache_key(state); self.match_runtime.cache_key(state); } } impl ExtraBuildRequires { /// Apply runtime constraints from a resolution to the extra build requirements. pub fn match_runtime(self, resolution: &Resolution) -> Result { self.into_iter() .filter(|(_, requirements)| !requirements.is_empty()) .filter(|(name, _)| resolution.distributions().any(|dist| dist.name() == name)) .map(|(name, requirements)| { let requirements = requirements .into_iter() .map(|requirement| match requirement { ExtraBuildRequirement { requirement, match_runtime: true, } => { // Reject requirements with `match-runtime = true` that include any form // of constraint. if let RequirementSource::Registry { specifier, .. } = &requirement.source { if !specifier.is_empty() { return Err( ExtraBuildRequiresError::VersionSpecifiersNotAllowed( requirement.name.clone(), Box::new(requirement.source.clone()), ), ); } } else { return Err(ExtraBuildRequiresError::VersionSpecifiersNotAllowed( requirement.name.clone(), Box::new(requirement.source.clone()), )); } let dist = resolution .distributions() .find(|dist| dist.name() == &requirement.name) .ok_or_else(|| { ExtraBuildRequiresError::NotFound(requirement.name.clone()) })?; let requirement = Requirement { source: RequirementSource::from(dist), ..requirement }; Ok::<_, ExtraBuildRequiresError>(ExtraBuildRequirement { requirement, match_runtime: true, }) } requirement => Ok(requirement), }) .collect::, _>>()?; Ok::<_, ExtraBuildRequiresError>((name, requirements)) }) .collect::>() } } /// A map of extra build variables, from variable name to value. pub type BuildVariables = BTreeMap; /// Extra environment variables to set during builds, on a per-package basis. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ExtraBuildVariables(BTreeMap); impl std::ops::Deref for ExtraBuildVariables { type Target = BTreeMap; fn deref(&self) -> &Self::Target { &self.0 } } impl std::ops::DerefMut for ExtraBuildVariables { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl IntoIterator for ExtraBuildVariables { type Item = (PackageName, BuildVariables); type IntoIter = std::collections::btree_map::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl FromIterator<(PackageName, BuildVariables)> for ExtraBuildVariables { fn from_iter>(iter: T) -> Self { Self(iter.into_iter().collect()) } } impl CacheKey for ExtraBuildVariables { fn cache_key(&self, state: &mut CacheKeyHasher) { for (package, vars) in &self.0 { package.as_str().cache_key(state); for (key, value) in vars { key.cache_key(state); value.cache_key(state); } } } } uv-0.9.17+ds1/crates/uv-distribution-types/src/buildable.rs000066400000000000000000000153411520155276700236150ustar00rootroot00000000000000use std::borrow::Cow; use std::path::Path; use uv_distribution_filename::SourceDistExtension; use uv_git_types::GitUrl; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::VerbatimUrl; use uv_normalize::PackageName; use uv_redacted::DisplaySafeUrl; use crate::{DirectorySourceDist, GitSourceDist, Name, PathSourceDist, SourceDist}; /// A reference to a source that can be built into a built distribution. /// /// This can either be a distribution (e.g., a package on a registry) or a direct URL. /// /// Distributions can _also_ point to URLs in lieu of a registry; however, the primary distinction /// here is that a distribution will always include a package name, while a URL will not. #[derive(Debug, Clone)] pub enum BuildableSource<'a> { Dist(&'a SourceDist), Url(SourceUrl<'a>), } impl BuildableSource<'_> { /// Return the [`PackageName`] of the source, if available. pub fn name(&self) -> Option<&PackageName> { match self { Self::Dist(dist) => Some(dist.name()), Self::Url(_) => None, } } /// Return the source tree of the source, if available. pub fn source_tree(&self) -> Option<&Path> { match self { Self::Dist(dist) => dist.source_tree(), Self::Url(url) => url.source_tree(), } } /// Return the [`Version`] of the source, if available. pub fn version(&self) -> Option<&Version> { match self { Self::Dist(SourceDist::Registry(dist)) => Some(&dist.version), Self::Dist(SourceDist::Path(dist)) => dist.version.as_ref(), Self::Dist(_) => None, Self::Url(_) => None, } } /// Return the [`BuildableSource`] as a [`SourceDist`], if it is a distribution. pub fn as_dist(&self) -> Option<&SourceDist> { match self { Self::Dist(dist) => Some(dist), Self::Url(_) => None, } } /// Returns `true` if the source is editable. pub fn is_editable(&self) -> bool { match self { Self::Dist(dist) => dist.is_editable(), Self::Url(url) => url.is_editable(), } } /// Return true if the source refers to a local source tree (i.e., a directory). pub fn is_source_tree(&self) -> bool { match self { Self::Dist(dist) => matches!(dist, SourceDist::Directory(_)), Self::Url(url) => matches!(url, SourceUrl::Directory(_)), } } /// Return the Python version specifier required by the source, if available. pub fn requires_python(&self) -> Option<&VersionSpecifiers> { let Self::Dist(SourceDist::Registry(dist)) = self else { return None; }; dist.file.requires_python.as_ref() } } impl std::fmt::Display for BuildableSource<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Dist(dist) => write!(f, "{dist}"), Self::Url(url) => write!(f, "{url}"), } } } /// A reference to a source distribution defined by a URL. #[derive(Debug, Clone)] pub enum SourceUrl<'a> { Direct(DirectSourceUrl<'a>), Git(GitSourceUrl<'a>), Path(PathSourceUrl<'a>), Directory(DirectorySourceUrl<'a>), } impl SourceUrl<'_> { /// Return the [`DisplaySafeUrl`] of the source. pub fn url(&self) -> &DisplaySafeUrl { match self { Self::Direct(dist) => dist.url, Self::Git(dist) => dist.url, Self::Path(dist) => dist.url, Self::Directory(dist) => dist.url, } } /// Return the source tree of the source, if available. pub fn source_tree(&self) -> Option<&Path> { match self { Self::Directory(dist) => Some(&dist.install_path), _ => None, } } /// Returns `true` if the source is editable. pub fn is_editable(&self) -> bool { matches!( self, Self::Directory(DirectorySourceUrl { editable: Some(true), .. }) ) } /// Return true if the source refers to a local file or directory. pub fn is_local(&self) -> bool { matches!(self, Self::Path(_) | Self::Directory(_)) } } impl std::fmt::Display for SourceUrl<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Direct(url) => write!(f, "{url}"), Self::Git(url) => write!(f, "{url}"), Self::Path(url) => write!(f, "{url}"), Self::Directory(url) => write!(f, "{url}"), } } } #[derive(Debug, Clone)] pub struct DirectSourceUrl<'a> { pub url: &'a DisplaySafeUrl, pub subdirectory: Option<&'a Path>, pub ext: SourceDistExtension, } impl std::fmt::Display for DirectSourceUrl<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{url}", url = self.url) } } #[derive(Debug, Clone)] pub struct GitSourceUrl<'a> { /// The URL with the revision and subdirectory fragment. pub url: &'a VerbatimUrl, pub git: &'a GitUrl, /// The URL without the revision and subdirectory fragment. pub subdirectory: Option<&'a Path>, } impl std::fmt::Display for GitSourceUrl<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{url}", url = self.url) } } impl<'a> From<&'a GitSourceDist> for GitSourceUrl<'a> { fn from(dist: &'a GitSourceDist) -> Self { Self { url: &dist.url, git: &dist.git, subdirectory: dist.subdirectory.as_deref(), } } } #[derive(Debug, Clone)] pub struct PathSourceUrl<'a> { pub url: &'a DisplaySafeUrl, pub path: Cow<'a, Path>, pub ext: SourceDistExtension, } impl std::fmt::Display for PathSourceUrl<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{url}", url = self.url) } } impl<'a> From<&'a PathSourceDist> for PathSourceUrl<'a> { fn from(dist: &'a PathSourceDist) -> Self { Self { url: &dist.url, path: Cow::Borrowed(&dist.install_path), ext: dist.ext, } } } #[derive(Debug, Clone)] pub struct DirectorySourceUrl<'a> { pub url: &'a DisplaySafeUrl, pub install_path: Cow<'a, Path>, pub editable: Option, } impl std::fmt::Display for DirectorySourceUrl<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{url}", url = self.url) } } impl<'a> From<&'a DirectorySourceDist> for DirectorySourceUrl<'a> { fn from(dist: &'a DirectorySourceDist) -> Self { Self { url: &dist.url, install_path: Cow::Borrowed(&dist.install_path), editable: dist.editable, } } } uv-0.9.17+ds1/crates/uv-distribution-types/src/cached.rs000066400000000000000000000161611520155276700231020ustar00rootroot00000000000000use std::path::Path; use uv_cache_info::CacheInfo; use uv_distribution_filename::WheelFilename; use uv_normalize::PackageName; use uv_pypi_types::{HashDigest, HashDigests, VerbatimParsedUrl}; use crate::{ BuildInfo, BuiltDist, Dist, DistributionMetadata, Hashed, InstalledMetadata, InstalledVersion, Name, ParsedUrl, SourceDist, VersionOrUrlRef, }; /// A built distribution (wheel) that exists in the local cache. #[derive(Debug, Clone, Hash, PartialEq, Eq)] #[allow(clippy::large_enum_variant)] pub enum CachedDist { /// The distribution exists in a registry, like `PyPI`. Registry(CachedRegistryDist), /// The distribution exists at an arbitrary URL. Url(CachedDirectUrlDist), } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct CachedRegistryDist { pub filename: WheelFilename, pub path: Box, pub hashes: HashDigests, pub cache_info: CacheInfo, pub build_info: Option, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct CachedDirectUrlDist { pub filename: WheelFilename, pub url: VerbatimParsedUrl, pub path: Box, pub hashes: HashDigests, pub cache_info: CacheInfo, pub build_info: Option, } impl CachedDist { /// Initialize a [`CachedDist`] from a [`Dist`]. pub fn from_remote( remote: Dist, filename: WheelFilename, hashes: HashDigests, cache_info: CacheInfo, build_info: Option, path: Box, ) -> Self { match remote { Dist::Built(BuiltDist::Registry(_dist)) => Self::Registry(CachedRegistryDist { filename, path, hashes, cache_info, build_info, }), Dist::Built(BuiltDist::DirectUrl(dist)) => Self::Url(CachedDirectUrlDist { filename, url: VerbatimParsedUrl { parsed_url: dist.parsed_url(), verbatim: dist.url, }, hashes, cache_info, build_info, path, }), Dist::Built(BuiltDist::Path(dist)) => Self::Url(CachedDirectUrlDist { filename, url: VerbatimParsedUrl { parsed_url: dist.parsed_url(), verbatim: dist.url, }, hashes, cache_info, build_info, path, }), Dist::Source(SourceDist::Registry(_dist)) => Self::Registry(CachedRegistryDist { filename, path, hashes, cache_info, build_info, }), Dist::Source(SourceDist::DirectUrl(dist)) => Self::Url(CachedDirectUrlDist { filename, url: VerbatimParsedUrl { parsed_url: dist.parsed_url(), verbatim: dist.url, }, hashes, cache_info, build_info, path, }), Dist::Source(SourceDist::Git(dist)) => Self::Url(CachedDirectUrlDist { filename, url: VerbatimParsedUrl { parsed_url: dist.parsed_url(), verbatim: dist.url, }, hashes, cache_info, build_info, path, }), Dist::Source(SourceDist::Path(dist)) => Self::Url(CachedDirectUrlDist { filename, url: VerbatimParsedUrl { parsed_url: dist.parsed_url(), verbatim: dist.url, }, hashes, cache_info, build_info, path, }), Dist::Source(SourceDist::Directory(dist)) => Self::Url(CachedDirectUrlDist { filename, url: VerbatimParsedUrl { parsed_url: dist.parsed_url(), verbatim: dist.url, }, hashes, cache_info, build_info, path, }), } } /// Return the [`Path`] at which the distribution is stored on-disk. pub fn path(&self) -> &Path { match self { Self::Registry(dist) => &dist.path, Self::Url(dist) => &dist.path, } } /// Return the [`CacheInfo`] of the distribution. pub fn cache_info(&self) -> &CacheInfo { match self { Self::Registry(dist) => &dist.cache_info, Self::Url(dist) => &dist.cache_info, } } /// Return the [`BuildInfo`] of the distribution. pub fn build_info(&self) -> Option<&BuildInfo> { match self { Self::Registry(dist) => dist.build_info.as_ref(), Self::Url(dist) => dist.build_info.as_ref(), } } /// Return the [`ParsedUrl`] of the distribution, if it exists. pub fn parsed_url(&self) -> Option<&ParsedUrl> { match self { Self::Registry(_) => None, Self::Url(dist) => Some(&dist.url.parsed_url), } } /// Returns the [`WheelFilename`] of the distribution. pub fn filename(&self) -> &WheelFilename { match self { Self::Registry(dist) => &dist.filename, Self::Url(dist) => &dist.filename, } } } impl Hashed for CachedRegistryDist { fn hashes(&self) -> &[HashDigest] { self.hashes.as_slice() } } impl Name for CachedRegistryDist { fn name(&self) -> &PackageName { &self.filename.name } } impl Name for CachedDirectUrlDist { fn name(&self) -> &PackageName { &self.filename.name } } impl Name for CachedDist { fn name(&self) -> &PackageName { match self { Self::Registry(dist) => dist.name(), Self::Url(dist) => dist.name(), } } } impl DistributionMetadata for CachedRegistryDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { VersionOrUrlRef::Version(&self.filename.version) } } impl DistributionMetadata for CachedDirectUrlDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { VersionOrUrlRef::Url(&self.url.verbatim) } } impl DistributionMetadata for CachedDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { match self { Self::Registry(dist) => dist.version_or_url(), Self::Url(dist) => dist.version_or_url(), } } } impl InstalledMetadata for CachedRegistryDist { fn installed_version(&self) -> InstalledVersion<'_> { InstalledVersion::Version(&self.filename.version) } } impl InstalledMetadata for CachedDirectUrlDist { fn installed_version(&self) -> InstalledVersion<'_> { InstalledVersion::Url(&self.url.verbatim, &self.filename.version) } } impl InstalledMetadata for CachedDist { fn installed_version(&self) -> InstalledVersion<'_> { match self { Self::Registry(dist) => dist.installed_version(), Self::Url(dist) => dist.installed_version(), } } } uv-0.9.17+ds1/crates/uv-distribution-types/src/config_settings.rs000066400000000000000000000400321520155276700250520ustar00rootroot00000000000000use std::{ collections::{BTreeMap, btree_map::Entry}, str::FromStr, }; use uv_cache_key::CacheKeyHasher; use uv_normalize::PackageName; #[derive(Debug, Clone)] pub struct ConfigSettingEntry { /// The key of the setting. For example, given `key=value`, this would be `key`. key: String, /// The value of the setting. For example, given `key=value`, this would be `value`. value: String, } impl FromStr for ConfigSettingEntry { type Err = String; fn from_str(s: &str) -> Result { let Some((key, value)) = s.split_once('=') else { return Err(format!( "Invalid config setting: {s} (expected `KEY=VALUE`)" )); }; Ok(Self { key: key.trim().to_string(), value: value.trim().to_string(), }) } } #[derive(Debug, Clone)] pub struct ConfigSettingPackageEntry { /// The package name to apply the setting to. package: PackageName, /// The config setting entry. setting: ConfigSettingEntry, } impl FromStr for ConfigSettingPackageEntry { type Err = String; fn from_str(s: &str) -> Result { let Some((package_str, config_str)) = s.split_once(':') else { return Err(format!( "Invalid config setting: {s} (expected `PACKAGE:KEY=VALUE`)" )); }; let package = PackageName::from_str(package_str.trim()) .map_err(|e| format!("Invalid package name: {e}"))?; let setting = ConfigSettingEntry::from_str(config_str)?; Ok(Self { package, setting }) } } #[derive(Debug, Clone, Hash, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))] enum ConfigSettingValue { /// The value consists of a single string. String(String), /// The value consists of a list of strings. List(Vec), } impl serde::Serialize for ConfigSettingValue { fn serialize(&self, serializer: S) -> Result { match self { Self::String(value) => serializer.serialize_str(value), Self::List(values) => serializer.collect_seq(values.iter()), } } } impl<'de> serde::Deserialize<'de> for ConfigSettingValue { fn deserialize>(deserializer: D) -> Result { struct Visitor; impl<'de> serde::de::Visitor<'de> for Visitor { type Value = ConfigSettingValue; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a string or list of strings") } fn visit_str(self, value: &str) -> Result { Ok(ConfigSettingValue::String(value.to_string())) } fn visit_seq>( self, mut seq: A, ) -> Result { let mut values = Vec::new(); while let Some(value) = seq.next_element()? { values.push(value); } Ok(ConfigSettingValue::List(values)) } } deserializer.deserialize_any(Visitor) } } /// Settings to pass to a PEP 517 build backend, structured as a map from (string) key to string or /// list of strings. /// /// See: #[derive(Debug, Default, Hash, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ConfigSettings(BTreeMap); impl FromIterator for ConfigSettings { fn from_iter>(iter: T) -> Self { let mut config = BTreeMap::default(); for entry in iter { match config.entry(entry.key) { Entry::Vacant(vacant) => { vacant.insert(ConfigSettingValue::String(entry.value)); } Entry::Occupied(mut occupied) => match occupied.get_mut() { ConfigSettingValue::String(existing) => { let existing = existing.clone(); occupied.insert(ConfigSettingValue::List(vec![existing, entry.value])); } ConfigSettingValue::List(existing) => { existing.push(entry.value); } }, } } Self(config) } } impl ConfigSettings { /// Returns the number of settings in the configuration. pub fn len(&self) -> usize { self.0.len() } /// Returns `true` if the configuration contains no settings. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Convert the settings to a string that can be passed directly to a PEP 517 build backend. pub fn escape_for_python(&self) -> String { serde_json::to_string(self).expect("Failed to serialize config settings") } /// Merge two sets of config settings, with the values in `self` taking precedence. #[must_use] pub fn merge(self, other: Self) -> Self { let mut config = self.0; for (key, value) in other.0 { match config.entry(key) { Entry::Vacant(vacant) => { vacant.insert(value); } Entry::Occupied(mut occupied) => match occupied.get_mut() { ConfigSettingValue::String(existing) => { let existing = existing.clone(); match value { ConfigSettingValue::String(value) => { occupied.insert(ConfigSettingValue::List(vec![existing, value])); } ConfigSettingValue::List(mut values) => { values.insert(0, existing); occupied.insert(ConfigSettingValue::List(values)); } } } ConfigSettingValue::List(existing) => match value { ConfigSettingValue::String(value) => { existing.push(value); } ConfigSettingValue::List(values) => { existing.extend(values); } }, }, } } Self(config) } } impl uv_cache_key::CacheKey for ConfigSettings { fn cache_key(&self, state: &mut CacheKeyHasher) { for (key, value) in &self.0 { key.cache_key(state); match value { ConfigSettingValue::String(value) => value.cache_key(state), ConfigSettingValue::List(values) => values.cache_key(state), } } } } impl serde::Serialize for ConfigSettings { fn serialize(&self, serializer: S) -> Result { use serde::ser::SerializeMap; let mut map = serializer.serialize_map(Some(self.0.len()))?; for (key, value) in &self.0 { map.serialize_entry(key, value)?; } map.end() } } impl<'de> serde::Deserialize<'de> for ConfigSettings { fn deserialize>(deserializer: D) -> Result { struct Visitor; impl<'de> serde::de::Visitor<'de> for Visitor { type Value = ConfigSettings; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a map from string to string or list of strings") } fn visit_map>( self, mut map: A, ) -> Result { let mut config = BTreeMap::default(); while let Some((key, value)) = map.next_entry()? { config.insert(key, value); } Ok(ConfigSettings(config)) } } deserializer.deserialize_map(Visitor) } } /// Settings to pass to PEP 517 build backends on a per-package basis. #[derive(Debug, Default, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct PackageConfigSettings(BTreeMap); impl FromIterator for PackageConfigSettings { fn from_iter>(iter: T) -> Self { let mut package_configs: BTreeMap> = BTreeMap::new(); for entry in iter { package_configs .entry(entry.package) .or_default() .push(entry.setting); } let configs = package_configs .into_iter() .map(|(package, entries)| (package, entries.into_iter().collect())) .collect(); Self(configs) } } impl PackageConfigSettings { /// Returns the config settings for a specific package, if any. pub fn get(&self, package: &PackageName) -> Option<&ConfigSettings> { self.0.get(package) } /// Returns `true` if there are no package-specific settings. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Merge two sets of package config settings, with the values in `self` taking precedence. #[must_use] pub fn merge(mut self, other: Self) -> Self { for (package, settings) in other.0 { match self.0.entry(package) { Entry::Vacant(vacant) => { vacant.insert(settings); } Entry::Occupied(mut occupied) => { let merged = occupied.get().clone().merge(settings); occupied.insert(merged); } } } self } } impl uv_cache_key::CacheKey for PackageConfigSettings { fn cache_key(&self, state: &mut CacheKeyHasher) { for (package, settings) in &self.0 { package.to_string().cache_key(state); settings.cache_key(state); } } } impl serde::Serialize for PackageConfigSettings { fn serialize(&self, serializer: S) -> Result { use serde::ser::SerializeMap; let mut map = serializer.serialize_map(Some(self.0.len()))?; for (key, value) in &self.0 { map.serialize_entry(&key.to_string(), value)?; } map.end() } } impl<'de> serde::Deserialize<'de> for PackageConfigSettings { fn deserialize>(deserializer: D) -> Result { struct Visitor; impl<'de> serde::de::Visitor<'de> for Visitor { type Value = PackageConfigSettings; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a map from package name to config settings") } fn visit_map>( self, mut map: A, ) -> Result { let mut config = BTreeMap::default(); while let Some((key, value)) = map.next_entry::()? { let package = PackageName::from_str(&key).map_err(|e| { serde::de::Error::custom(format!("Invalid package name: {e}")) })?; config.insert(package, value); } Ok(PackageConfigSettings(config)) } } deserializer.deserialize_map(Visitor) } } #[cfg(test)] mod tests { use super::*; #[test] fn collect_config_settings() { let settings: ConfigSettings = vec![ ConfigSettingEntry { key: "key".to_string(), value: "value".to_string(), }, ConfigSettingEntry { key: "key".to_string(), value: "value2".to_string(), }, ConfigSettingEntry { key: "list".to_string(), value: "value3".to_string(), }, ConfigSettingEntry { key: "list".to_string(), value: "value4".to_string(), }, ] .into_iter() .collect(); assert_eq!( settings.0.get("key"), Some(&ConfigSettingValue::List(vec![ "value".to_string(), "value2".to_string() ])) ); assert_eq!( settings.0.get("list"), Some(&ConfigSettingValue::List(vec![ "value3".to_string(), "value4".to_string() ])) ); } #[test] fn escape_for_python() { let mut settings = ConfigSettings::default(); settings.0.insert( "key".to_string(), ConfigSettingValue::String("value".to_string()), ); settings.0.insert( "list".to_string(), ConfigSettingValue::List(vec!["value1".to_string(), "value2".to_string()]), ); assert_eq!( settings.escape_for_python(), r#"{"key":"value","list":["value1","value2"]}"# ); let mut settings = ConfigSettings::default(); settings.0.insert( "key".to_string(), ConfigSettingValue::String("Hello, \"world!\"".to_string()), ); settings.0.insert( "list".to_string(), ConfigSettingValue::List(vec!["'value1'".to_string()]), ); assert_eq!( settings.escape_for_python(), r#"{"key":"Hello, \"world!\"","list":["'value1'"]}"# ); let mut settings = ConfigSettings::default(); settings.0.insert( "key".to_string(), ConfigSettingValue::String("val\\1 {}value".to_string()), ); assert_eq!(settings.escape_for_python(), r#"{"key":"val\\1 {}value"}"#); } #[test] fn parse_config_setting_package_entry() { // Test valid parsing let entry = ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap(); assert_eq!(entry.package.as_ref(), "numpy"); assert_eq!(entry.setting.key, "editable_mode"); assert_eq!(entry.setting.value, "compat"); // Test with package name containing hyphens let entry = ConfigSettingPackageEntry::from_str("my-package:some_key=value").unwrap(); assert_eq!(entry.package.as_ref(), "my-package"); assert_eq!(entry.setting.key, "some_key"); assert_eq!(entry.setting.value, "value"); // Test with spaces around values let entry = ConfigSettingPackageEntry::from_str(" numpy : key = value ").unwrap(); assert_eq!(entry.package.as_ref(), "numpy"); assert_eq!(entry.setting.key, "key"); assert_eq!(entry.setting.value, "value"); } #[test] fn collect_config_settings_package() { let settings: PackageConfigSettings = vec![ ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap(), ConfigSettingPackageEntry::from_str("numpy:another_key=value").unwrap(), ConfigSettingPackageEntry::from_str("scipy:build_option=fast").unwrap(), ] .into_iter() .collect(); let numpy_settings = settings .get(&PackageName::from_str("numpy").unwrap()) .unwrap(); assert_eq!( numpy_settings.0.get("editable_mode"), Some(&ConfigSettingValue::String("compat".to_string())) ); assert_eq!( numpy_settings.0.get("another_key"), Some(&ConfigSettingValue::String("value".to_string())) ); let scipy_settings = settings .get(&PackageName::from_str("scipy").unwrap()) .unwrap(); assert_eq!( scipy_settings.0.get("build_option"), Some(&ConfigSettingValue::String("fast".to_string())) ); } } uv-0.9.17+ds1/crates/uv-distribution-types/src/dependency_metadata.rs000066400000000000000000000105701520155276700256470ustar00rootroot00000000000000use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use tracing::{debug, warn}; use uv_normalize::{ExtraName, PackageName}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::Requirement; use uv_pypi_types::{ResolutionMetadata, VerbatimParsedUrl}; /// Pre-defined [`StaticMetadata`] entries, indexed by [`PackageName`] and [`Version`]. #[derive(Debug, Clone, Default)] pub struct DependencyMetadata(FxHashMap>); impl DependencyMetadata { /// Index a set of [`StaticMetadata`] entries by [`PackageName`] and [`Version`]. pub fn from_entries(entries: impl IntoIterator) -> Self { let mut map = Self::default(); for entry in entries { map.0.entry(entry.name.clone()).or_default().push(entry); } map } /// Retrieve a [`StaticMetadata`] entry by [`PackageName`] and [`Version`]. pub fn get( &self, package: &PackageName, version: Option<&Version>, ) -> Option { let versions = self.0.get(package)?; if let Some(version) = version { // If a specific version was requested, search for an exact match, then a global match. let metadata = if let Some(metadata) = versions .iter() .find(|entry| entry.version.as_ref() == Some(version)) { debug!("Found dependency metadata entry for `{package}=={version}`"); metadata } else if let Some(metadata) = versions.iter().find(|entry| entry.version.is_none()) { debug!("Found global metadata entry for `{package}`"); metadata } else { warn!("No dependency metadata entry found for `{package}=={version}`"); return None; }; Some(ResolutionMetadata { name: metadata.name.clone(), version: version.clone(), requires_dist: metadata.requires_dist.clone(), requires_python: metadata.requires_python.clone(), provides_extra: metadata.provides_extra.clone(), dynamic: false, }) } else { // If no version was requested (i.e., it's a direct URL dependency), allow a single // versioned match. let [metadata] = versions.as_slice() else { warn!("Multiple dependency metadata entries found for `{package}`"); return None; }; let Some(version) = metadata.version.clone() else { warn!("No version found in dependency metadata entry for `{package}`"); return None; }; debug!("Found dependency metadata entry for `{package}` (assuming: `{version}`)"); Some(ResolutionMetadata { name: metadata.name.clone(), version, requires_dist: metadata.requires_dist.clone(), requires_python: metadata.requires_python.clone(), provides_extra: metadata.provides_extra.clone(), dynamic: false, }) } } /// Retrieve all [`StaticMetadata`] entries. pub fn values(&self) -> impl Iterator { self.0.values().flatten() } } /// A subset of the Python Package Metadata 2.3 standard as specified in /// . #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct StaticMetadata { // Mandatory fields pub name: PackageName, #[cfg_attr( feature = "schemars", schemars( with = "Option", description = "PEP 440-style package version, e.g., `1.2.3`" ) )] pub version: Option, // Optional fields #[serde(default)] pub requires_dist: Box<[Requirement]>, #[cfg_attr( feature = "schemars", schemars( with = "Option", description = "PEP 508-style Python requirement, e.g., `>=3.10`" ) )] pub requires_python: Option, #[serde(default, alias = "provides-extras")] pub provides_extra: Box<[ExtraName]>, } uv-0.9.17+ds1/crates/uv-distribution-types/src/diagnostic.rs000066400000000000000000000004301520155276700240070ustar00rootroot00000000000000use uv_normalize::PackageName; pub trait Diagnostic { /// Convert the diagnostic into a user-facing message. fn message(&self) -> String; /// Returns `true` if the [`PackageName`] is involved in this diagnostic. fn includes(&self, name: &PackageName) -> bool; } uv-0.9.17+ds1/crates/uv-distribution-types/src/dist_error.rs000066400000000000000000000155261520155276700240530ustar00rootroot00000000000000use std::collections::VecDeque; use std::fmt::{Debug, Display, Formatter}; use petgraph::Direction; use petgraph::prelude::EdgeRef; use rustc_hash::FxHashSet; use version_ranges::Ranges; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep440::Version; use crate::{ BuiltDist, Dist, DistRef, Edge, Name, Node, RequestedDist, Resolution, ResolvedDist, SourceDist, }; /// Inspect whether an error type is a build error. pub trait IsBuildBackendError: std::error::Error + Send + Sync + 'static { /// Returns whether the build backend failed to build the package, so it's not a uv error. fn is_build_backend_error(&self) -> bool; } /// The operation(s) that failed when reporting an error with a distribution. #[derive(Debug)] pub enum DistErrorKind { Download, DownloadAndBuild, Build, BuildBackend, Read, } impl DistErrorKind { pub fn from_requested_dist(dist: &RequestedDist, err: &impl IsBuildBackendError) -> Self { match dist { RequestedDist::Installed(_) => Self::Read, RequestedDist::Installable(dist) => Self::from_dist(dist, err), } } pub fn from_dist(dist: &Dist, err: &impl IsBuildBackendError) -> Self { if err.is_build_backend_error() { Self::BuildBackend } else { match dist { Dist::Built(BuiltDist::Path(_)) => Self::Read, Dist::Source(SourceDist::Path(_) | SourceDist::Directory(_)) => Self::Build, Dist::Built(_) => Self::Download, Dist::Source(source_dist) => { if source_dist.is_local() { Self::Build } else { Self::DownloadAndBuild } } } } } } impl Display for DistErrorKind { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Download => f.write_str("Failed to download"), Self::DownloadAndBuild => f.write_str("Failed to download and build"), Self::Build => f.write_str("Failed to build"), Self::BuildBackend => f.write_str("Failed to build"), Self::Read => f.write_str("Failed to read"), } } } /// A chain of derivation steps from the root package to the current package, to explain why a /// package is included in the resolution. #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] pub struct DerivationChain(Vec); impl FromIterator for DerivationChain { fn from_iter>(iter: T) -> Self { Self(iter.into_iter().collect()) } } impl DerivationChain { /// Compute a [`DerivationChain`] from a resolution graph. /// /// This is used to construct a derivation chain upon install failure in the `uv pip` context, /// where we don't have a lockfile describing the resolution. pub fn from_resolution(resolution: &Resolution, target: DistRef<'_>) -> Option { // Find the target distribution in the resolution graph. let target = resolution.graph().node_indices().find(|node| { let Node::Dist { dist: ResolvedDist::Installable { dist, .. }, .. } = &resolution.graph()[*node] else { return false; }; target == dist.as_ref().into() })?; // Perform a BFS to find the shortest path to the root. let mut queue = VecDeque::new(); queue.push_back((target, None, None, Vec::new())); // TODO(charlie): Consider respecting markers here. let mut seen = FxHashSet::default(); while let Some((node, extra, group, mut path)) = queue.pop_front() { if !seen.insert(node) { continue; } match &resolution.graph()[node] { Node::Root => { path.reverse(); path.pop(); return Some(Self::from_iter(path)); } Node::Dist { dist, .. } => { for edge in resolution.graph().edges_directed(node, Direction::Incoming) { let mut path = path.clone(); path.push(DerivationStep::new( dist.name().clone(), extra.clone(), group.clone(), dist.version().cloned(), Ranges::empty(), )); let target = edge.source(); let extra = match edge.weight() { Edge::Optional(extra) => Some(extra.clone()), _ => None, }; let group = match edge.weight() { Edge::Dev(group) => Some(group.clone()), _ => None, }; queue.push_back((target, extra, group, path)); } } } } None } /// Returns the length of the derivation chain. pub fn len(&self) -> usize { self.0.len() } /// Returns `true` if the derivation chain is empty. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Returns an iterator over the steps in the derivation chain. pub fn iter(&self) -> std::slice::Iter<'_, DerivationStep> { self.0.iter() } } impl<'chain> IntoIterator for &'chain DerivationChain { type Item = &'chain DerivationStep; type IntoIter = std::slice::Iter<'chain, DerivationStep>; fn into_iter(self) -> Self::IntoIter { self.0.iter() } } impl IntoIterator for DerivationChain { type Item = DerivationStep; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } /// A step in a derivation chain. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct DerivationStep { /// The name of the package. pub name: PackageName, /// The enabled extra of the package, if any. pub extra: Option, /// The enabled dependency group of the package, if any. pub group: Option, /// The version of the package. pub version: Option, /// The constraints applied to the subsequent package in the chain. pub range: Ranges, } impl DerivationStep { /// Create a [`DerivationStep`] from a package name and version. pub fn new( name: PackageName, extra: Option, group: Option, version: Option, range: Ranges, ) -> Self { Self { name, extra, group, version, range, } } } uv-0.9.17+ds1/crates/uv-distribution-types/src/error.rs000066400000000000000000000012451520155276700230210ustar00rootroot00000000000000use uv_normalize::PackageName; use uv_redacted::DisplaySafeUrl; #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] Utf8(#[from] std::str::Utf8Error), #[error(transparent)] WheelFilename(#[from] uv_distribution_filename::WheelFilenameError), #[error("Could not extract path segments from URL: {0}")] MissingPathSegments(String), #[error("Distribution not found at: {0}")] NotFound(DisplaySafeUrl), #[error("Requested package name `{0}` does not match `{1}` in the distribution filename: {2}")] PackageNameMismatch(PackageName, PackageName, String), } uv-0.9.17+ds1/crates/uv-distribution-types/src/file.rs000066400000000000000000000262131520155276700226110ustar00rootroot00000000000000use std::borrow::Cow; use std::fmt::{self, Display, Formatter}; use std::str::FromStr; use jiff::Timestamp; use serde::{Deserialize, Serialize}; use uv_pep440::{VersionSpecifiers, VersionSpecifiersParseError}; use uv_pep508::split_scheme; use uv_pypi_types::{CoreMetadata, HashDigests, Yanked}; use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError}; use uv_small_str::SmallString; /// Error converting [`uv_pypi_types::PypiFile`] to [`distribution_type::File`]. #[derive(Debug, thiserror::Error)] pub enum FileConversionError { #[error("Failed to parse `requires-python`: `{0}`")] RequiresPython(String, #[source] VersionSpecifiersParseError), #[error("Failed to parse URL: {0}")] Url(String, #[source] url::ParseError), #[error("Failed to parse filename from URL: {0}")] MissingPathSegments(String), #[error(transparent)] Utf8(#[from] std::str::Utf8Error), } /// Internal analog to [`uv_pypi_types::PypiFile`]. #[derive(Debug, Clone, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] pub struct File { pub dist_info_metadata: bool, pub filename: SmallString, pub hashes: HashDigests, pub requires_python: Option, pub size: Option, // N.B. We don't use a Jiff timestamp here because it's a little // annoying to do so with rkyv. Since we only use this field for doing // comparisons in testing, we just store it as a UTC timestamp in // milliseconds. pub upload_time_utc_ms: Option, pub url: FileLocation, pub yanked: Option>, pub zstd: Option>, } impl File { /// `TryFrom` instead of `From` to filter out files with invalid requires python version specifiers pub fn try_from_pypi( file: uv_pypi_types::PypiFile, base: &SmallString, ) -> Result { Ok(Self { dist_info_metadata: file .core_metadata .as_ref() .is_some_and(CoreMetadata::is_available), filename: file.filename, hashes: HashDigests::from(file.hashes), requires_python: file .requires_python .transpose() .map_err(|err| FileConversionError::RequiresPython(err.line().clone(), err))?, size: file.size, upload_time_utc_ms: file.upload_time.map(Timestamp::as_millisecond), url: FileLocation::new(file.url, base), yanked: file.yanked, zstd: None, }) } pub fn try_from_pyx( file: uv_pypi_types::PyxFile, base: &SmallString, ) -> Result { let filename = if let Some(filename) = file.filename { filename } else { // Remove any query parameters or fragments from the URL to get the filename. let base_url = file .url .as_ref() .split_once('?') .or_else(|| file.url.as_ref().split_once('#')) .map(|(path, _)| path) .unwrap_or(file.url.as_ref()); // Take the last segment, stripping any query or fragment. let last = base_url .split('/') .next_back() .ok_or_else(|| FileConversionError::MissingPathSegments(file.url.to_string()))?; // Decode the filename, which may be percent-encoded. let filename = percent_encoding::percent_decode_str(last).decode_utf8()?; SmallString::from(filename) }; Ok(Self { filename, dist_info_metadata: file .core_metadata .as_ref() .is_some_and(CoreMetadata::is_available), hashes: HashDigests::from(file.hashes), requires_python: file .requires_python .transpose() .map_err(|err| FileConversionError::RequiresPython(err.line().clone(), err))?, size: file.size, upload_time_utc_ms: file.upload_time.map(Timestamp::as_millisecond), url: FileLocation::new(file.url, base), yanked: file.yanked, zstd: file .zstd .map(|zstd| Zstd { hashes: HashDigests::from(zstd.hashes), size: zstd.size, }) .map(Box::new), }) } } /// While a registry file is generally a remote URL, it can also be a file if it comes from a directory flat indexes. #[derive(Debug, Clone, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] pub enum FileLocation { /// URL relative to the base URL. RelativeUrl(SmallString, SmallString), /// Absolute URL. AbsoluteUrl(UrlString), } impl FileLocation { /// Parse a relative or absolute URL on a page with a base URL. /// /// This follows the HTML semantics where a link on a page is resolved relative to the URL of /// that page. pub fn new(url: SmallString, base: &SmallString) -> Self { match split_scheme(&url) { Some(..) => Self::AbsoluteUrl(UrlString::new(url)), None => Self::RelativeUrl(base.clone(), url), } } /// Convert this location to a URL. /// /// A relative URL has its base joined to the path. An absolute URL is /// parsed as-is. And a path location is turned into a URL via the `file` /// protocol. /// /// # Errors /// /// This returns an error if any of the URL parsing fails, or if, for /// example, the location is a path and the path isn't valid UTF-8. /// (Because URLs must be valid UTF-8.) pub fn to_url(&self) -> Result { match self { Self::RelativeUrl(base, path) => { let base_url = DisplaySafeUrl::parse(base).map_err(|err| ToUrlError::InvalidBase { base: base.to_string(), err, })?; let joined = base_url.join(path).map_err(|err| ToUrlError::InvalidJoin { base: base.to_string(), path: path.to_string(), err, })?; Ok(joined) } Self::AbsoluteUrl(absolute) => absolute.to_url(), } } } impl Display for FileLocation { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Self::RelativeUrl(_base, url) => Display::fmt(&url, f), Self::AbsoluteUrl(url) => Display::fmt(&url.0, f), } } } /// A [`Url`] represented as a `String`. /// /// This type is not guaranteed to be a valid URL, and may error on conversion. #[derive( Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, )] #[serde(transparent)] #[rkyv(derive(Debug))] pub struct UrlString(SmallString); impl UrlString { /// Create a new [`UrlString`] from a [`String`]. pub fn new(url: SmallString) -> Self { Self(url) } /// Converts a [`UrlString`] to a [`DisplaySafeUrl`]. pub fn to_url(&self) -> Result { DisplaySafeUrl::from_str(&self.0).map_err(|err| ToUrlError::InvalidAbsolute { absolute: self.0.to_string(), err, }) } /// Return the [`UrlString`] with any query parameters and fragments removed. pub fn base_str(&self) -> &str { self.as_ref() .split_once('?') .or_else(|| self.as_ref().split_once('#')) .map(|(path, _)| path) .unwrap_or(self.as_ref()) } /// Return the [`UrlString`] (as a [`Cow`]) with any fragments removed. #[must_use] pub fn without_fragment(&self) -> Cow<'_, Self> { self.as_ref() .split_once('#') .map(|(path, _)| Cow::Owned(Self(SmallString::from(path)))) .unwrap_or(Cow::Borrowed(self)) } } impl AsRef for UrlString { fn as_ref(&self) -> &str { &self.0 } } impl From for UrlString { fn from(value: DisplaySafeUrl) -> Self { Self(value.as_str().into()) } } impl From<&DisplaySafeUrl> for UrlString { fn from(value: &DisplaySafeUrl) -> Self { Self(value.as_str().into()) } } impl Display for UrlString { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) } } /// An error that occurs when a [`FileLocation`] is not a valid URL. #[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)] pub enum ToUrlError { /// An error that occurs when the base URL in [`FileLocation::Relative`] /// could not be parsed as a valid URL. #[error("Could not parse base URL `{base}` as a valid URL")] InvalidBase { /// The base URL that could not be parsed as a valid URL. base: String, /// The underlying URL parse error. #[source] err: DisplaySafeUrlError, }, /// An error that occurs when the base URL could not be joined with /// the relative path in a [`FileLocation::Relative`]. #[error("Could not join base URL `{base}` to relative path `{path}`")] InvalidJoin { /// The base URL that could not be parsed as a valid URL. base: String, /// The relative path segment. path: String, /// The underlying URL parse error. #[source] err: DisplaySafeUrlError, }, /// An error that occurs when the absolute URL in [`FileLocation::Absolute`] /// could not be parsed as a valid URL. #[error("Could not parse absolute URL `{absolute}` as a valid URL")] InvalidAbsolute { /// The absolute URL that could not be parsed as a valid URL. absolute: String, /// The underlying URL parse error. #[source] err: DisplaySafeUrlError, }, } #[derive(Debug, Clone, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] pub struct Zstd { pub hashes: HashDigests, pub size: Option, } #[cfg(test)] mod tests { use super::*; #[test] fn base_str() { let url = UrlString("https://example.com/path?query#fragment".into()); assert_eq!(url.base_str(), "https://example.com/path"); let url = UrlString("https://example.com/path#fragment".into()); assert_eq!(url.base_str(), "https://example.com/path"); let url = UrlString("https://example.com/path".into()); assert_eq!(url.base_str(), "https://example.com/path"); } #[test] fn without_fragment() { // Borrows a URL without a fragment let url = UrlString("https://example.com/path".into()); assert_eq!(&*url.without_fragment(), &url); assert!(matches!(url.without_fragment(), Cow::Borrowed(_))); // Removes the fragment if present on the URL let url = UrlString("https://example.com/path?query#fragment".into()); assert_eq!( &*url.without_fragment(), &UrlString("https://example.com/path?query".into()) ); assert!(matches!(url.without_fragment(), Cow::Owned(_))); } } uv-0.9.17+ds1/crates/uv-distribution-types/src/hash.rs000066400000000000000000000067231520155276700226210ustar00rootroot00000000000000use uv_pypi_types::{HashAlgorithm, HashDigest}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HashPolicy<'a> { /// No hash policy is specified. None, /// Hashes should be generated (specifically, a SHA-256 hash), but not validated. Generate(HashGeneration), /// Hashes should be validated against a pre-defined list of hashes. If necessary, hashes should /// be generated so as to ensure that the archive is valid. Validate(&'a [HashDigest]), } impl HashPolicy<'_> { /// Returns `true` if the hash policy is `None`. pub fn is_none(&self) -> bool { matches!(self, Self::None) } /// Returns `true` if the hash policy is `Validate`. pub fn is_validate(&self) -> bool { matches!(self, Self::Validate(_)) } /// Returns `true` if the hash policy indicates that hashes should be generated. pub fn is_generate(&self, dist: &crate::BuiltDist) -> bool { match self { Self::Generate(HashGeneration::Url) => dist.file().is_none(), Self::Generate(HashGeneration::All) => { dist.file().is_none_or(|file| file.hashes.is_empty()) } Self::Validate(_) => false, Self::None => false, } } /// Return the algorithms used in the hash policy. pub fn algorithms(&self) -> Vec { match self { Self::None => vec![], Self::Generate(_) => vec![HashAlgorithm::Sha256], Self::Validate(hashes) => { let mut algorithms = hashes.iter().map(HashDigest::algorithm).collect::>(); algorithms.sort(); algorithms.dedup(); algorithms } } } /// Return the digests used in the hash policy. pub fn digests(&self) -> &[HashDigest] { match self { Self::None => &[], Self::Generate(_) => &[], Self::Validate(hashes) => hashes, } } } /// The context in which hashes should be generated. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HashGeneration { /// Generate hashes for direct URL distributions. Url, /// Generate hashes for direct URL distributions, along with any distributions that are hosted /// on a registry that does _not_ provide hashes. All, } pub trait Hashed { /// Return the [`HashDigest`]s for the archive. fn hashes(&self) -> &[HashDigest]; /// Returns `true` if the archive satisfies the given hash policy. fn satisfies(&self, hashes: HashPolicy) -> bool { match hashes { HashPolicy::None => true, HashPolicy::Generate(_) => self .hashes() .iter() .any(|hash| hash.algorithm == HashAlgorithm::Sha256), HashPolicy::Validate(hashes) => self.hashes().iter().any(|hash| hashes.contains(hash)), } } /// Returns `true` if the archive includes a hash for at least one of the given algorithms. fn has_digests(&self, hashes: HashPolicy) -> bool { match hashes { HashPolicy::None => true, HashPolicy::Generate(_) => self .hashes() .iter() .any(|hash| hash.algorithm == HashAlgorithm::Sha256), HashPolicy::Validate(hashes) => hashes .iter() .map(HashDigest::algorithm) .any(|algorithm| self.hashes().iter().any(|hash| hash.algorithm == algorithm)), } } } uv-0.9.17+ds1/crates/uv-distribution-types/src/id.rs000066400000000000000000000074641520155276700222750ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use std::path::PathBuf; use uv_cache_key::{CanonicalUrl, RepositoryUrl}; use uv_normalize::PackageName; use uv_pep440::Version; use uv_pypi_types::HashDigest; use uv_redacted::DisplaySafeUrl; /// A unique identifier for a package. A package can either be identified by a name (e.g., `black`) /// or a URL (e.g., `git+https://github.com/psf/black`). #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum PackageId { /// The identifier consists of a package name. Name(PackageName), /// The identifier consists of a URL. Url(CanonicalUrl), } impl PackageId { /// Create a new [`PackageId`] from a package name and version. pub fn from_registry(name: PackageName) -> Self { Self::Name(name) } /// Create a new [`PackageId`] from a URL. pub fn from_url(url: &DisplaySafeUrl) -> Self { Self::Url(CanonicalUrl::new(url)) } } impl Display for PackageId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Name(name) => write!(f, "{name}"), Self::Url(url) => write!(f, "{url}"), } } } /// A unique identifier for a package at a specific version (e.g., `black==23.10.0`). #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum VersionId { /// The identifier consists of a package name and version. NameVersion(PackageName, Version), /// The identifier consists of a URL. Url(CanonicalUrl), } impl VersionId { /// Create a new [`VersionId`] from a package name and version. pub fn from_registry(name: PackageName, version: Version) -> Self { Self::NameVersion(name, version) } /// Create a new [`VersionId`] from a URL. pub fn from_url(url: &DisplaySafeUrl) -> Self { Self::Url(CanonicalUrl::new(url)) } } impl Display for VersionId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::NameVersion(name, version) => write!(f, "{name}-{version}"), Self::Url(url) => write!(f, "{url}"), } } } /// A unique resource identifier for the distribution, like a SHA-256 hash of the distribution's /// contents. /// /// A distribution is a specific archive of a package at a specific version. For a given package /// version, there may be multiple distributions, e.g., source distribution, along with /// multiple binary distributions (wheels) for different platforms. As a concrete example, /// `black-23.10.0-py3-none-any.whl` would represent a (binary) distribution of the `black` package /// at version `23.10.0`. /// /// The distribution ID is used to uniquely identify a distribution. Ideally, the distribution /// ID should be a hash of the distribution's contents, though in practice, it's only required /// that the ID is unique within a single invocation of the resolver (and so, e.g., a hash of /// the URL would also be sufficient). #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum DistributionId { Url(CanonicalUrl), PathBuf(PathBuf), Digest(HashDigest), AbsoluteUrl(String), RelativeUrl(String, String), } /// A unique identifier for a resource, like a URL or a Git repository. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum ResourceId { Url(RepositoryUrl), PathBuf(PathBuf), Digest(HashDigest), AbsoluteUrl(String), RelativeUrl(String, String), } impl From<&Self> for VersionId { /// Required for `WaitMap::wait`. fn from(value: &Self) -> Self { value.clone() } } impl From<&Self> for DistributionId { /// Required for `WaitMap::wait`. fn from(value: &Self) -> Self { value.clone() } } impl From<&Self> for ResourceId { /// Required for `WaitMap::wait`. fn from(value: &Self) -> Self { value.clone() } } uv-0.9.17+ds1/crates/uv-distribution-types/src/index.rs000066400000000000000000000451061520155276700230030ustar00rootroot00000000000000use std::path::Path; use std::str::FromStr; use serde::{Deserialize, Serialize}; use thiserror::Error; use url::Url; use uv_auth::{AuthPolicy, Credentials}; use uv_redacted::DisplaySafeUrl; use uv_small_str::SmallString; use crate::index_name::{IndexName, IndexNameError}; use crate::origin::Origin; use crate::{IndexStatusCodeStrategy, IndexUrl, IndexUrlError, SerializableStatusCode}; /// Cache control configuration for an index. #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Default)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "kebab-case")] pub struct IndexCacheControl { /// Cache control header for Simple API requests. pub api: Option, /// Cache control header for file downloads. pub files: Option, } impl IndexCacheControl { /// Return the default Simple API cache control headers for the given index URL, if applicable. pub fn simple_api_cache_control(_url: &Url) -> Option<&'static str> { None } /// Return the default files cache control headers for the given index URL, if applicable. pub fn artifact_cache_control(url: &Url) -> Option<&'static str> { if url .host_str() .is_some_and(|host| host.ends_with("pytorch.org")) { // Some wheels in the PyTorch registry were accidentally uploaded with `no-cache,no-store,must-revalidate`. // The PyTorch team plans to correct this in the future, but in the meantime we override // the cache control headers to allow caching of static files. // // See: https://github.com/pytorch/pytorch/pull/149218 Some("max-age=365000000, immutable, public") } else { None } } } #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "kebab-case")] pub struct Index { /// The name of the index. /// /// Index names can be used to reference indexes elsewhere in the configuration. For example, /// you can pin a package to a specific index by name: /// /// ```toml /// [[tool.uv.index]] /// name = "pytorch" /// url = "https://download.pytorch.org/whl/cu121" /// /// [tool.uv.sources] /// torch = { index = "pytorch" } /// ``` pub name: Option, /// The URL of the index. /// /// Expects to receive a URL (e.g., `https://pypi.org/simple`) or a local path. pub url: IndexUrl, /// Mark the index as explicit. /// /// Explicit indexes will _only_ be used when explicitly requested via a `[tool.uv.sources]` /// definition, as in: /// /// ```toml /// [[tool.uv.index]] /// name = "pytorch" /// url = "https://download.pytorch.org/whl/cu121" /// explicit = true /// /// [tool.uv.sources] /// torch = { index = "pytorch" } /// ``` #[serde(default)] pub explicit: bool, /// Mark the index as the default index. /// /// By default, uv uses PyPI as the default index, such that even if additional indexes are /// defined via `[[tool.uv.index]]`, PyPI will still be used as a fallback for packages that /// aren't found elsewhere. To disable the PyPI default, set `default = true` on at least one /// other index. /// /// Marking an index as default will move it to the front of the list of indexes, such that it /// is given the highest priority when resolving packages. #[serde(default)] pub default: bool, /// The origin of the index (e.g., a CLI flag, a user-level configuration file, etc.). #[serde(skip)] pub origin: Option, /// The format used by the index. /// /// Indexes can either be PEP 503-compliant (i.e., a PyPI-style registry implementing the Simple /// API) or structured as a flat list of distributions (e.g., `--find-links`). In both cases, /// indexes can point to either local or remote resources. #[serde(default)] pub format: IndexFormat, /// The URL of the upload endpoint. /// /// When using `uv publish --index `, this URL is used for publishing. /// /// A configuration for the default index PyPI would look as follows: /// /// ```toml /// [[tool.uv.index]] /// name = "pypi" /// url = "https://pypi.org/simple" /// publish-url = "https://upload.pypi.org/legacy/" /// ``` pub publish_url: Option, /// When uv should use authentication for requests to the index. /// /// ```toml /// [[tool.uv.index]] /// name = "my-index" /// url = "https:///simple" /// authenticate = "always" /// ``` #[serde(default)] pub authenticate: AuthPolicy, /// Status codes that uv should ignore when deciding whether /// to continue searching in the next index after a failure. /// /// ```toml /// [[tool.uv.index]] /// name = "my-index" /// url = "https:///simple" /// ignore-error-codes = [401, 403] /// ``` #[serde(default)] pub ignore_error_codes: Option>, /// Cache control configuration for this index. /// /// When set, these headers will override the server's cache control headers /// for both package metadata requests and artifact downloads. /// /// ```toml /// [[tool.uv.index]] /// name = "my-index" /// url = "https:///simple" /// cache-control = { api = "max-age=600", files = "max-age=3600" } /// ``` #[serde(default)] pub cache_control: Option, } impl PartialEq for Index { fn eq(&self, other: &Self) -> bool { let Self { name, url, explicit, default, origin: _, format, publish_url, authenticate, ignore_error_codes, cache_control, } = self; *url == other.url && *name == other.name && *explicit == other.explicit && *default == other.default && *format == other.format && *publish_url == other.publish_url && *authenticate == other.authenticate && *ignore_error_codes == other.ignore_error_codes && *cache_control == other.cache_control } } impl Eq for Index {} impl PartialOrd for Index { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Index { fn cmp(&self, other: &Self) -> std::cmp::Ordering { let Self { name, url, explicit, default, origin: _, format, publish_url, authenticate, ignore_error_codes, cache_control, } = self; url.cmp(&other.url) .then_with(|| name.cmp(&other.name)) .then_with(|| explicit.cmp(&other.explicit)) .then_with(|| default.cmp(&other.default)) .then_with(|| format.cmp(&other.format)) .then_with(|| publish_url.cmp(&other.publish_url)) .then_with(|| authenticate.cmp(&other.authenticate)) .then_with(|| ignore_error_codes.cmp(&other.ignore_error_codes)) .then_with(|| cache_control.cmp(&other.cache_control)) } } impl std::hash::Hash for Index { fn hash(&self, state: &mut H) { let Self { name, url, explicit, default, origin: _, format, publish_url, authenticate, ignore_error_codes, cache_control, } = self; url.hash(state); name.hash(state); explicit.hash(state); default.hash(state); format.hash(state); publish_url.hash(state); authenticate.hash(state); ignore_error_codes.hash(state); cache_control.hash(state); } } #[derive( Default, Debug, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize, )] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "kebab-case")] pub enum IndexFormat { /// A PyPI-style index implementing the Simple Repository API. #[default] Simple, /// A `--find-links`-style index containing a flat list of wheels and source distributions. Flat, } impl Index { /// Initialize an [`Index`] from a pip-style `--index-url`. pub fn from_index_url(url: IndexUrl) -> Self { Self { url, name: None, explicit: false, default: true, origin: None, format: IndexFormat::Simple, publish_url: None, authenticate: AuthPolicy::default(), ignore_error_codes: None, cache_control: None, } } /// Initialize an [`Index`] from a pip-style `--extra-index-url`. pub fn from_extra_index_url(url: IndexUrl) -> Self { Self { url, name: None, explicit: false, default: false, origin: None, format: IndexFormat::Simple, publish_url: None, authenticate: AuthPolicy::default(), ignore_error_codes: None, cache_control: None, } } /// Initialize an [`Index`] from a pip-style `--find-links`. pub fn from_find_links(url: IndexUrl) -> Self { Self { url, name: None, explicit: false, default: false, origin: None, format: IndexFormat::Flat, publish_url: None, authenticate: AuthPolicy::default(), ignore_error_codes: None, cache_control: None, } } /// Set the [`Origin`] of the index. #[must_use] pub fn with_origin(mut self, origin: Origin) -> Self { self.origin = Some(origin); self } /// Return the [`IndexUrl`] of the index. pub fn url(&self) -> &IndexUrl { &self.url } /// Consume the [`Index`] and return the [`IndexUrl`]. pub fn into_url(self) -> IndexUrl { self.url } /// Return the raw [`Url`] of the index. pub fn raw_url(&self) -> &DisplaySafeUrl { self.url.url() } /// Return the root [`Url`] of the index, if applicable. /// /// For indexes with a `/simple` endpoint, this is simply the URL with the final segment /// removed. This is useful, e.g., for credential propagation to other endpoints on the index. pub fn root_url(&self) -> Option { self.url.root() } /// Retrieve the credentials for the index, either from the environment, or from the URL itself. pub fn credentials(&self) -> Option { // If the index is named, and credentials are provided via the environment, prefer those. if let Some(name) = self.name.as_ref() { if let Some(credentials) = Credentials::from_env(name.to_env_var()) { return Some(credentials); } } // Otherwise, extract the credentials from the URL. Credentials::from_url(self.url.url()) } /// Resolve the index relative to the given root directory. pub fn relative_to(mut self, root_dir: &Path) -> Result { if let IndexUrl::Path(ref url) = self.url { if let Some(given) = url.given() { self.url = IndexUrl::parse(given, Some(root_dir))?; } } Ok(self) } /// Return the [`IndexStatusCodeStrategy`] for this index. pub fn status_code_strategy(&self) -> IndexStatusCodeStrategy { if let Some(ignore_error_codes) = &self.ignore_error_codes { IndexStatusCodeStrategy::from_ignored_error_codes(ignore_error_codes) } else { IndexStatusCodeStrategy::from_index_url(self.url.url()) } } /// Return the cache control header for file requests to this index, if any. pub fn artifact_cache_control(&self) -> Option<&str> { if let Some(artifact_cache_control) = self .cache_control .as_ref() .and_then(|cache_control| cache_control.files.as_deref()) { Some(artifact_cache_control) } else { IndexCacheControl::artifact_cache_control(self.url.url()) } } /// Return the cache control header for API requests to this index, if any. pub fn simple_api_cache_control(&self) -> Option<&str> { if let Some(api_cache_control) = self .cache_control .as_ref() .and_then(|cache_control| cache_control.api.as_deref()) { Some(api_cache_control) } else { IndexCacheControl::simple_api_cache_control(self.url.url()) } } } impl From for Index { fn from(value: IndexUrl) -> Self { Self { name: None, url: value, explicit: false, default: false, origin: None, format: IndexFormat::Simple, publish_url: None, authenticate: AuthPolicy::default(), ignore_error_codes: None, cache_control: None, } } } impl FromStr for Index { type Err = IndexSourceError; fn from_str(s: &str) -> Result { // Determine whether the source is prefixed with a name, as in `name=https://pypi.org/simple`. if let Some((name, url)) = s.split_once('=') { if !name.chars().any(|c| c == ':') { let name = IndexName::from_str(name)?; let url = IndexUrl::from_str(url)?; return Ok(Self { name: Some(name), url, explicit: false, default: false, origin: None, format: IndexFormat::Simple, publish_url: None, authenticate: AuthPolicy::default(), ignore_error_codes: None, cache_control: None, }); } } // Otherwise, assume the source is a URL. let url = IndexUrl::from_str(s)?; Ok(Self { name: None, url, explicit: false, default: false, origin: None, format: IndexFormat::Simple, publish_url: None, authenticate: AuthPolicy::default(), ignore_error_codes: None, cache_control: None, }) } } /// An [`IndexUrl`] along with the metadata necessary to query the index. #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct IndexMetadata { /// The URL of the index. pub url: IndexUrl, /// The format used by the index. pub format: IndexFormat, } impl IndexMetadata { /// Return a reference to the [`IndexMetadata`]. pub fn as_ref(&self) -> IndexMetadataRef<'_> { let Self { url, format: kind } = self; IndexMetadataRef { url, format: *kind } } /// Consume the [`IndexMetadata`] and return the [`IndexUrl`]. pub fn into_url(self) -> IndexUrl { self.url } } /// A reference to an [`IndexMetadata`]. #[derive(Debug, Copy, Clone)] pub struct IndexMetadataRef<'a> { /// The URL of the index. pub url: &'a IndexUrl, /// The format used by the index. pub format: IndexFormat, } impl IndexMetadata { /// Return the [`IndexUrl`] of the index. pub fn url(&self) -> &IndexUrl { &self.url } } impl IndexMetadataRef<'_> { /// Return the [`IndexUrl`] of the index. pub fn url(&self) -> &IndexUrl { self.url } } impl<'a> From<&'a Index> for IndexMetadataRef<'a> { fn from(value: &'a Index) -> Self { Self { url: &value.url, format: value.format, } } } impl<'a> From<&'a IndexMetadata> for IndexMetadataRef<'a> { fn from(value: &'a IndexMetadata) -> Self { Self { url: &value.url, format: value.format, } } } impl From for IndexMetadata { fn from(value: IndexUrl) -> Self { Self { url: value, format: IndexFormat::Simple, } } } impl<'a> From<&'a IndexUrl> for IndexMetadataRef<'a> { fn from(value: &'a IndexUrl) -> Self { Self { url: value, format: IndexFormat::Simple, } } } /// An error that can occur when parsing an [`Index`]. #[derive(Error, Debug)] pub enum IndexSourceError { #[error(transparent)] Url(#[from] IndexUrlError), #[error(transparent)] IndexName(#[from] IndexNameError), #[error("Index included a name, but the name was empty")] EmptyName, } #[cfg(test)] mod tests { use super::*; #[test] fn test_index_cache_control_headers() { // Test that cache control headers are properly parsed from TOML let toml_str = r#" name = "test-index" url = "https://test.example.com/simple" cache-control = { api = "max-age=600", files = "max-age=3600" } "#; let index: Index = toml::from_str(toml_str).unwrap(); assert_eq!(index.name.as_ref().unwrap().as_ref(), "test-index"); assert!(index.cache_control.is_some()); let cache_control = index.cache_control.as_ref().unwrap(); assert_eq!(cache_control.api.as_deref(), Some("max-age=600")); assert_eq!(cache_control.files.as_deref(), Some("max-age=3600")); } #[test] fn test_index_without_cache_control() { // Test that indexes work without cache control headers let toml_str = r#" name = "test-index" url = "https://test.example.com/simple" "#; let index: Index = toml::from_str(toml_str).unwrap(); assert_eq!(index.name.as_ref().unwrap().as_ref(), "test-index"); assert_eq!(index.cache_control, None); } #[test] fn test_index_partial_cache_control() { // Test that cache control can have just one field let toml_str = r#" name = "test-index" url = "https://test.example.com/simple" cache-control = { api = "max-age=300" } "#; let index: Index = toml::from_str(toml_str).unwrap(); assert_eq!(index.name.as_ref().unwrap().as_ref(), "test-index"); assert!(index.cache_control.is_some()); let cache_control = index.cache_control.as_ref().unwrap(); assert_eq!(cache_control.api.as_deref(), Some("max-age=300")); assert_eq!(cache_control.files, None); } } uv-0.9.17+ds1/crates/uv-distribution-types/src/index_name.rs000066400000000000000000000054741520155276700240070ustar00rootroot00000000000000use std::borrow::Cow; use std::ops::Deref; use std::str::FromStr; use thiserror::Error; use uv_small_str::SmallString; /// The normalized name of an index. /// /// Index names may contain letters, digits, hyphens, underscores, and periods, and must be ASCII. #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, serde::Serialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct IndexName(SmallString); impl IndexName { /// Validates the given index name and returns [`IndexName`] if it's valid, or an error /// otherwise. pub fn new(name: &str) -> Result { for c in name.chars() { match c { 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => {} c if c.is_ascii() => { return Err(IndexNameError::UnsupportedCharacter(c, name.to_string())); } c => { return Err(IndexNameError::NonAsciiName(c, name.to_string())); } } } Ok(Self(SmallString::from(name))) } /// Converts the index name to an environment variable name. /// /// For example, given `IndexName("foo-bar")`, this will return `"FOO_BAR"`. pub fn to_env_var(&self) -> String { self.0 .chars() .map(|c| { if c.is_ascii_alphanumeric() { c.to_ascii_uppercase() } else { '_' } }) .collect::() } } impl FromStr for IndexName { type Err = IndexNameError; fn from_str(s: &str) -> Result { Self::new(s) } } impl<'de> serde::de::Deserialize<'de> for IndexName { fn deserialize(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { let s = Cow::<'_, str>::deserialize(deserializer)?; Self::new(&s).map_err(serde::de::Error::custom) } } impl std::fmt::Display for IndexName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } impl AsRef for IndexName { fn as_ref(&self) -> &str { &self.0 } } impl Deref for IndexName { type Target = str; fn deref(&self) -> &Self::Target { &self.0 } } /// An error that can occur when parsing an [`IndexName`]. #[derive(Error, Debug)] pub enum IndexNameError { #[error("Index included a name, but the name was empty")] EmptyName, #[error( "Index names may only contain letters, digits, hyphens, underscores, and periods, but found unsupported character (`{0}`) in: `{1}`" )] UnsupportedCharacter(char, String), #[error("Index names must be ASCII, but found non-ASCII character (`{0}`) in: `{1}`")] NonAsciiName(char, String), } uv-0.9.17+ds1/crates/uv-distribution-types/src/index_url.rs000066400000000000000000000722541520155276700236710ustar00rootroot00000000000000use std::borrow::Cow; use std::fmt::{Display, Formatter}; use std::ops::Deref; use std::path::Path; use std::str::FromStr; use std::sync::{Arc, LazyLock, RwLock}; use itertools::Either; use rustc_hash::{FxHashMap, FxHashSet}; use thiserror::Error; use url::{ParseError, Url}; use uv_auth::RealmRef; use uv_cache_key::CanonicalUrl; use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme}; use uv_redacted::DisplaySafeUrl; use uv_warnings::warn_user; use crate::{Index, IndexStatusCodeStrategy, Verbatim}; static PYPI_URL: LazyLock = LazyLock::new(|| DisplaySafeUrl::parse("https://pypi.org/simple").unwrap()); static DEFAULT_INDEX: LazyLock = LazyLock::new(|| { Index::from_index_url(IndexUrl::Pypi(Arc::new(VerbatimUrl::from_url( PYPI_URL.clone(), )))) }); /// The URL of an index to use for fetching packages (e.g., PyPI). #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub enum IndexUrl { Pypi(Arc), Url(Arc), Path(Arc), } impl IndexUrl { /// Parse an [`IndexUrl`] from a string, relative to an optional root directory. /// /// If no root directory is provided, relative paths are resolved against the current working /// directory. pub fn parse(path: &str, root_dir: Option<&Path>) -> Result { let url = VerbatimUrl::from_url_or_path(path, root_dir)?; Ok(Self::from(url)) } /// Return the root [`Url`] of the index, if applicable. /// /// For indexes with a `/simple` endpoint, this is simply the URL with the final segment /// removed. This is useful, e.g., for credential propagation to other endpoints on the index. pub fn root(&self) -> Option { let mut segments = self.url().path_segments()?; let last = match segments.next_back()? { // If the last segment is empty due to a trailing `/`, skip it (as in `pop_if_empty`) "" => segments.next_back()?, segment => segment, }; // We also handle `/+simple` as it's used in devpi if !(last.eq_ignore_ascii_case("simple") || last.eq_ignore_ascii_case("+simple")) { return None; } let mut url = self.url().clone(); url.path_segments_mut().ok()?.pop_if_empty().pop(); Some(url) } } #[cfg(feature = "schemars")] impl schemars::JsonSchema for IndexUrl { fn schema_name() -> Cow<'static, str> { Cow::Borrowed("IndexUrl") } fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { schemars::json_schema!({ "type": "string", "description": "The URL of an index to use for fetching packages (e.g., `https://pypi.org/simple`), or a local path." }) } } impl IndexUrl { #[inline] fn inner(&self) -> &VerbatimUrl { match self { Self::Pypi(url) | Self::Url(url) | Self::Path(url) => url, } } /// Return the raw URL for the index. pub fn url(&self) -> &DisplaySafeUrl { self.inner().raw() } /// Convert the index URL into a [`DisplaySafeUrl`]. pub fn into_url(self) -> DisplaySafeUrl { self.inner().to_url() } /// Return the redacted URL for the index, omitting any sensitive credentials. pub fn without_credentials(&self) -> Cow<'_, DisplaySafeUrl> { let url = self.url(); if url.username().is_empty() && url.password().is_none() { Cow::Borrowed(url) } else { let mut url = url.clone(); let _ = url.set_username(""); let _ = url.set_password(None); Cow::Owned(url) } } /// Warn user if the given URL was provided as an ambiguous relative path. /// /// This is a temporary warning. Ambiguous values will not be /// accepted in the future. pub fn warn_on_disambiguated_relative_path(&self) { let Self::Path(verbatim_url) = &self else { return; }; if let Some(path) = verbatim_url.given() { if !is_disambiguated_path(path) { if cfg!(windows) { warn_user!( "Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `.\\{path}` or `./{path}`). Support for ambiguous values will be removed in the future" ); } else { warn_user!( "Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `./{path}`). Support for ambiguous values will be removed in the future" ); } } } } } impl Display for IndexUrl { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(self.inner(), f) } } impl Verbatim for IndexUrl { fn verbatim(&self) -> Cow<'_, str> { self.inner().verbatim() } } /// Checks if a path is disambiguated. /// /// Disambiguated paths are absolute paths, paths with valid schemes, /// and paths starting with "./" or "../" on Unix or ".\\", "..\\", /// "./", or "../" on Windows. fn is_disambiguated_path(path: &str) -> bool { if cfg!(windows) { if path.starts_with(".\\") || path.starts_with("..\\") || path.starts_with('/') { return true; } } if path.starts_with("./") || path.starts_with("../") || Path::new(path).is_absolute() { return true; } // Check if the path has a scheme (like `file://`) if let Some((scheme, _)) = split_scheme(path) { return Scheme::parse(scheme).is_some(); } // This is an ambiguous relative path false } /// An error that can occur when parsing an [`IndexUrl`]. #[derive(Error, Debug)] pub enum IndexUrlError { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] Url(#[from] ParseError), #[error(transparent)] VerbatimUrl(#[from] VerbatimUrlError), } impl FromStr for IndexUrl { type Err = IndexUrlError; fn from_str(s: &str) -> Result { Self::parse(s, None) } } impl serde::ser::Serialize for IndexUrl { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { self.inner().without_credentials().serialize(serializer) } } impl<'de> serde::de::Deserialize<'de> for IndexUrl { fn deserialize(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { struct Visitor; impl serde::de::Visitor<'_> for Visitor { type Value = IndexUrl; fn expecting(&self, f: &mut Formatter) -> std::fmt::Result { f.write_str("a string") } fn visit_str(self, v: &str) -> Result { IndexUrl::from_str(v).map_err(serde::de::Error::custom) } } deserializer.deserialize_str(Visitor) } } impl From for IndexUrl { fn from(url: VerbatimUrl) -> Self { if url.scheme() == "file" { Self::Path(Arc::new(url)) } else if *url.raw() == *PYPI_URL { Self::Pypi(Arc::new(url)) } else { Self::Url(Arc::new(url)) } } } impl From for DisplaySafeUrl { fn from(index: IndexUrl) -> Self { index.inner().to_url() } } impl Deref for IndexUrl { type Target = Url; fn deref(&self) -> &Self::Target { self.inner() } } /// The index locations to use for fetching packages. By default, uses the PyPI index. /// /// This type merges the legacy `--index-url`, `--extra-index-url`, and `--find-links` options, /// along with the uv-specific `--index` and `--default-index`. #[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct IndexLocations { indexes: Vec, flat_index: Vec, no_index: bool, } impl IndexLocations { /// Determine the index URLs to use for fetching packages. pub fn new(indexes: Vec, flat_index: Vec, no_index: bool) -> Self { Self { indexes, flat_index, no_index, } } /// Combine a set of index locations. /// /// If either the current or the other index locations have `no_index` set, the result will /// have `no_index` set. /// /// If the current index location has an `index` set, it will be preserved. #[must_use] pub fn combine(self, indexes: Vec, flat_index: Vec, no_index: bool) -> Self { Self { indexes: self.indexes.into_iter().chain(indexes).collect(), flat_index: self.flat_index.into_iter().chain(flat_index).collect(), no_index: self.no_index || no_index, } } /// Returns `true` if no index configuration is set, i.e., the [`IndexLocations`] matches the /// default configuration. pub fn is_none(&self) -> bool { *self == Self::default() } } /// Returns `true` if two [`IndexUrl`]s refer to the same index. fn is_same_index(a: &IndexUrl, b: &IndexUrl) -> bool { RealmRef::from(&**b.url()) == RealmRef::from(&**a.url()) && CanonicalUrl::new(a.url()) == CanonicalUrl::new(b.url()) } impl<'a> IndexLocations { /// Return the default [`Index`] entry. /// /// If `--no-index` is set, return `None`. /// /// If no index is provided, use the `PyPI` index. pub fn default_index(&'a self) -> Option<&'a Index> { if self.no_index { None } else { let mut seen = FxHashSet::default(); self.indexes .iter() .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name))) .find(|index| index.default) .or_else(|| Some(&DEFAULT_INDEX)) } } /// Return an iterator over the implicit [`Index`] entries. /// /// Default and explicit indexes are excluded. pub fn implicit_indexes(&'a self) -> impl Iterator + 'a { if self.no_index { Either::Left(std::iter::empty()) } else { let mut seen = FxHashSet::default(); Either::Right( self.indexes .iter() .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name))) .filter(|index| !index.default && !index.explicit), ) } } /// Return an iterator over all [`Index`] entries in order. /// /// Explicit indexes are excluded. /// /// Prioritizes the extra indexes over the default index. /// /// If `no_index` was enabled, then this always returns an empty /// iterator. pub fn indexes(&'a self) -> impl Iterator + 'a { self.implicit_indexes() .chain(self.default_index()) .filter(|index| !index.explicit) } /// Return an iterator over all simple [`Index`] entries in order. /// /// If `no_index` was enabled, then this always returns an empty iterator. pub fn simple_indexes(&'a self) -> impl Iterator + 'a { if self.no_index { Either::Left(std::iter::empty()) } else { let mut seen = FxHashSet::default(); Either::Right( self.indexes .iter() .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name))), ) } } /// Return an iterator over the [`FlatIndexLocation`] entries. pub fn flat_indexes(&'a self) -> impl Iterator + 'a { self.flat_index.iter() } /// Return the `--no-index` flag. pub fn no_index(&self) -> bool { self.no_index } /// Clone the index locations into a [`IndexUrls`] instance. pub fn index_urls(&'a self) -> IndexUrls { IndexUrls { indexes: self.indexes.clone(), no_index: self.no_index, } } /// Return a vector containing all allowed [`Index`] entries. /// /// This includes explicit indexes, implicit indexes, flat indexes, and the default index. /// /// The indexes will be returned in the reverse of the order in which they were defined, such /// that the last-defined index is the first item in the vector. pub fn allowed_indexes(&'a self) -> Vec<&'a Index> { if self.no_index { self.flat_index.iter().rev().collect() } else { let mut indexes = vec![]; let mut seen = FxHashSet::default(); let mut default = false; for index in { self.indexes .iter() .chain(self.flat_index.iter()) .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name))) } { if index.default { if default { continue; } default = true; } indexes.push(index); } if !default { indexes.push(&*DEFAULT_INDEX); } indexes.reverse(); indexes } } /// Return a vector containing all known [`Index`] entries. /// /// This includes explicit indexes, implicit indexes, flat indexes, and default indexes; /// in short, it includes all defined indexes, even if they're overridden by some other index /// definition. /// /// The indexes will be returned in the reverse of the order in which they were defined, such /// that the last-defined index is the first item in the vector. pub fn known_indexes(&'a self) -> impl Iterator { if self.no_index { Either::Left(self.flat_index.iter().rev()) } else { Either::Right( std::iter::once(&*DEFAULT_INDEX) .chain(self.flat_index.iter().rev()) .chain(self.indexes.iter().rev()), ) } } /// Return the Simple API cache control header for an [`IndexUrl`], if configured. pub fn simple_api_cache_control_for(&self, url: &IndexUrl) -> Option<&str> { for index in &self.indexes { if is_same_index(index.url(), url) { return index.simple_api_cache_control(); } } None } /// Return the artifact cache control header for an [`IndexUrl`], if configured. pub fn artifact_cache_control_for(&self, url: &IndexUrl) -> Option<&str> { for index in &self.indexes { if is_same_index(index.url(), url) { return index.artifact_cache_control(); } } None } } impl From<&IndexLocations> for uv_auth::Indexes { fn from(index_locations: &IndexLocations) -> Self { Self::from_indexes(index_locations.allowed_indexes().into_iter().map(|index| { let mut url = index.url().url().clone(); url.set_username("").ok(); url.set_password(None).ok(); let mut root_url = index.url().root().unwrap_or_else(|| url.clone()); root_url.set_username("").ok(); root_url.set_password(None).ok(); uv_auth::Index { url, root_url, auth_policy: index.authenticate, } })) } } /// The index URLs to use for fetching packages. /// /// This type merges the legacy `--index-url` and `--extra-index-url` options, along with the /// uv-specific `--index` and `--default-index`. #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct IndexUrls { indexes: Vec, no_index: bool, } impl<'a> IndexUrls { pub fn from_indexes(indexes: Vec) -> Self { Self { indexes, no_index: false, } } /// Return the default [`Index`] entry. /// /// If `--no-index` is set, return `None`. /// /// If no index is provided, use the `PyPI` index. fn default_index(&'a self) -> Option<&'a Index> { if self.no_index { None } else { let mut seen = FxHashSet::default(); self.indexes .iter() .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name))) .find(|index| index.default) .or_else(|| Some(&DEFAULT_INDEX)) } } /// Return an iterator over the implicit [`Index`] entries. /// /// Default and explicit indexes are excluded. fn implicit_indexes(&'a self) -> impl Iterator + 'a { if self.no_index { Either::Left(std::iter::empty()) } else { let mut seen = FxHashSet::default(); Either::Right( self.indexes .iter() .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name))) .filter(|index| !index.default && !index.explicit), ) } } /// Return an iterator over all [`IndexUrl`] entries in order. /// /// Prioritizes the `[tool.uv.index]` definitions over the `--extra-index-url` definitions /// over the `--index-url` definition. /// /// If `no_index` was enabled, then this always returns an empty /// iterator. pub fn indexes(&'a self) -> impl Iterator + 'a { let mut seen = FxHashSet::default(); self.implicit_indexes() .chain(self.default_index()) .filter(|index| !index.explicit) .filter(move |index| seen.insert(index.raw_url())) // Filter out redundant raw URLs } /// Return an iterator over all user-defined [`Index`] entries in order. /// /// Prioritizes the `[tool.uv.index]` definitions over the `--extra-index-url` definitions /// over the `--index-url` definition. /// /// Unlike [`IndexUrl::indexes`], this includes explicit indexes and does _not_ insert PyPI /// as a fallback default. /// /// If `no_index` was enabled, then this always returns an empty /// iterator. pub fn defined_indexes(&'a self) -> impl Iterator + 'a { if self.no_index { return Either::Left(std::iter::empty()); } let mut seen = FxHashSet::default(); let (non_default, default) = self .indexes .iter() .filter(move |index| { if let Some(name) = &index.name { seen.insert(name) } else { true } }) .partition::, _>(|index| !index.default); Either::Right(non_default.into_iter().chain(default)) } /// Return the `--no-index` flag. pub fn no_index(&self) -> bool { self.no_index } /// Return the [`IndexStatusCodeStrategy`] for an [`IndexUrl`]. pub fn status_code_strategy_for(&self, url: &IndexUrl) -> IndexStatusCodeStrategy { for index in &self.indexes { if is_same_index(index.url(), url) { return index.status_code_strategy(); } } IndexStatusCodeStrategy::Default } /// Return the Simple API cache control header for an [`IndexUrl`], if configured. pub fn simple_api_cache_control_for(&self, url: &IndexUrl) -> Option<&str> { for index in &self.indexes { if is_same_index(index.url(), url) { return index.simple_api_cache_control(); } } None } /// Return the artifact cache control header for an [`IndexUrl`], if configured. pub fn artifact_cache_control_for(&self, url: &IndexUrl) -> Option<&str> { for index in &self.indexes { if is_same_index(index.url(), url) { return index.artifact_cache_control(); } } None } } bitflags::bitflags! { #[derive(Debug, Copy, Clone)] struct Flags: u8 { /// Whether the index supports range requests. const NO_RANGE_REQUESTS = 1; /// Whether the index returned a `401 Unauthorized` status code. const UNAUTHORIZED = 1 << 2; /// Whether the index returned a `403 Forbidden` status code. const FORBIDDEN = 1 << 1; } } /// A map of [`IndexUrl`]s to their capabilities. /// /// We only store indexes that lack capabilities (i.e., don't support range requests, aren't /// authorized). The benefit is that the map is almost always empty, so validating capabilities is /// extremely cheap. #[derive(Debug, Default, Clone)] pub struct IndexCapabilities(Arc>>); impl IndexCapabilities { /// Returns `true` if the given [`IndexUrl`] supports range requests. pub fn supports_range_requests(&self, index_url: &IndexUrl) -> bool { !self .0 .read() .unwrap() .get(index_url) .is_some_and(|flags| flags.intersects(Flags::NO_RANGE_REQUESTS)) } /// Mark an [`IndexUrl`] as not supporting range requests. pub fn set_no_range_requests(&self, index_url: IndexUrl) { self.0 .write() .unwrap() .entry(index_url) .or_insert(Flags::empty()) .insert(Flags::NO_RANGE_REQUESTS); } /// Returns `true` if the given [`IndexUrl`] returns a `401 Unauthorized` status code. pub fn unauthorized(&self, index_url: &IndexUrl) -> bool { self.0 .read() .unwrap() .get(index_url) .is_some_and(|flags| flags.intersects(Flags::UNAUTHORIZED)) } /// Mark an [`IndexUrl`] as returning a `401 Unauthorized` status code. pub fn set_unauthorized(&self, index_url: IndexUrl) { self.0 .write() .unwrap() .entry(index_url) .or_insert(Flags::empty()) .insert(Flags::UNAUTHORIZED); } /// Returns `true` if the given [`IndexUrl`] returns a `403 Forbidden` status code. pub fn forbidden(&self, index_url: &IndexUrl) -> bool { self.0 .read() .unwrap() .get(index_url) .is_some_and(|flags| flags.intersects(Flags::FORBIDDEN)) } /// Mark an [`IndexUrl`] as returning a `403 Forbidden` status code. pub fn set_forbidden(&self, index_url: IndexUrl) { self.0 .write() .unwrap() .entry(index_url) .or_insert(Flags::empty()) .insert(Flags::FORBIDDEN); } } #[cfg(test)] mod tests { use super::*; use crate::{IndexCacheControl, IndexFormat, IndexName}; use uv_small_str::SmallString; #[test] fn test_index_url_parse_valid_paths() { // Absolute path assert!(is_disambiguated_path("/absolute/path")); // Relative path assert!(is_disambiguated_path("./relative/path")); assert!(is_disambiguated_path("../../relative/path")); if cfg!(windows) { // Windows absolute path assert!(is_disambiguated_path("C:/absolute/path")); // Windows relative path assert!(is_disambiguated_path(".\\relative\\path")); assert!(is_disambiguated_path("..\\..\\relative\\path")); } } #[test] fn test_index_url_parse_ambiguous_paths() { // Test single-segment ambiguous path assert!(!is_disambiguated_path("index")); // Test multi-segment ambiguous path assert!(!is_disambiguated_path("relative/path")); } #[test] fn test_index_url_parse_with_schemes() { assert!(is_disambiguated_path("file:///absolute/path")); assert!(is_disambiguated_path("https://registry.com/simple/")); assert!(is_disambiguated_path( "git+https://github.com/example/repo.git" )); } #[test] fn test_cache_control_lookup() { use std::str::FromStr; use uv_small_str::SmallString; use crate::IndexFormat; use crate::index_name::IndexName; let indexes = vec![ Index { name: Some(IndexName::from_str("index1").unwrap()), url: IndexUrl::from_str("https://index1.example.com/simple").unwrap(), cache_control: Some(crate::IndexCacheControl { api: Some(SmallString::from("max-age=300")), files: Some(SmallString::from("max-age=1800")), }), explicit: false, default: false, origin: None, format: IndexFormat::Simple, publish_url: None, authenticate: uv_auth::AuthPolicy::default(), ignore_error_codes: None, }, Index { name: Some(IndexName::from_str("index2").unwrap()), url: IndexUrl::from_str("https://index2.example.com/simple").unwrap(), cache_control: None, explicit: false, default: false, origin: None, format: IndexFormat::Simple, publish_url: None, authenticate: uv_auth::AuthPolicy::default(), ignore_error_codes: None, }, ]; let index_urls = IndexUrls::from_indexes(indexes); let url1 = IndexUrl::from_str("https://index1.example.com/simple").unwrap(); assert_eq!( index_urls.simple_api_cache_control_for(&url1), Some("max-age=300") ); assert_eq!( index_urls.artifact_cache_control_for(&url1), Some("max-age=1800") ); let url2 = IndexUrl::from_str("https://index2.example.com/simple").unwrap(); assert_eq!(index_urls.simple_api_cache_control_for(&url2), None); assert_eq!(index_urls.artifact_cache_control_for(&url2), None); let url3 = IndexUrl::from_str("https://index3.example.com/simple").unwrap(); assert_eq!(index_urls.simple_api_cache_control_for(&url3), None); assert_eq!(index_urls.artifact_cache_control_for(&url3), None); } #[test] fn test_pytorch_default_cache_control() { // Test that PyTorch indexes get default cache control from the getter methods let indexes = vec![Index { name: Some(IndexName::from_str("pytorch").unwrap()), url: IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(), cache_control: None, // No explicit cache control explicit: false, default: false, origin: None, format: IndexFormat::Simple, publish_url: None, authenticate: uv_auth::AuthPolicy::default(), ignore_error_codes: None, }]; let index_urls = IndexUrls::from_indexes(indexes.clone()); let index_locations = IndexLocations::new(indexes, Vec::new(), false); let pytorch_url = IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(); // IndexUrls should return the default for PyTorch assert_eq!(index_urls.simple_api_cache_control_for(&pytorch_url), None); assert_eq!( index_urls.artifact_cache_control_for(&pytorch_url), Some("max-age=365000000, immutable, public") ); // IndexLocations should also return the default for PyTorch assert_eq!( index_locations.simple_api_cache_control_for(&pytorch_url), None ); assert_eq!( index_locations.artifact_cache_control_for(&pytorch_url), Some("max-age=365000000, immutable, public") ); } #[test] fn test_pytorch_user_override_cache_control() { // Test that user-specified cache control overrides PyTorch defaults let indexes = vec![Index { name: Some(IndexName::from_str("pytorch").unwrap()), url: IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(), cache_control: Some(IndexCacheControl { api: Some(SmallString::from("no-cache")), files: Some(SmallString::from("max-age=3600")), }), explicit: false, default: false, origin: None, format: IndexFormat::Simple, publish_url: None, authenticate: uv_auth::AuthPolicy::default(), ignore_error_codes: None, }]; let index_urls = IndexUrls::from_indexes(indexes.clone()); let index_locations = IndexLocations::new(indexes, Vec::new(), false); let pytorch_url = IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(); // User settings should override defaults assert_eq!( index_urls.simple_api_cache_control_for(&pytorch_url), Some("no-cache") ); assert_eq!( index_urls.artifact_cache_control_for(&pytorch_url), Some("max-age=3600") ); // Same for IndexLocations assert_eq!( index_locations.simple_api_cache_control_for(&pytorch_url), Some("no-cache") ); assert_eq!( index_locations.artifact_cache_control_for(&pytorch_url), Some("max-age=3600") ); } } uv-0.9.17+ds1/crates/uv-distribution-types/src/installed.rs000066400000000000000000000566601520155276700236620ustar00rootroot00000000000000use std::borrow::Cow; use std::io::BufReader; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::OnceLock; use fs_err as fs; use thiserror::Error; use tracing::warn; use url::Url; use uv_cache_info::CacheInfo; use uv_distribution_filename::{EggInfoFilename, ExpandedTags}; use uv_fs::Simplified; use uv_install_wheel::WheelFile; use uv_normalize::PackageName; use uv_pep440::Version; use uv_pypi_types::{DirectUrl, MetadataError}; use uv_redacted::DisplaySafeUrl; use crate::{ BuildInfo, DistributionMetadata, InstalledMetadata, InstalledVersion, Name, VersionOrUrlRef, }; #[derive(Error, Debug)] pub enum InstalledDistError { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] UrlParse(#[from] url::ParseError), #[error(transparent)] Json(#[from] serde_json::Error), #[error(transparent)] EggInfoParse(#[from] uv_distribution_filename::EggInfoFilenameError), #[error(transparent)] VersionParse(#[from] uv_pep440::VersionParseError), #[error(transparent)] PackageNameParse(#[from] uv_normalize::InvalidNameError), #[error(transparent)] WheelFileParse(#[from] uv_install_wheel::Error), #[error(transparent)] ExpandedTagParse(#[from] uv_distribution_filename::ExpandedTagError), #[error("Invalid .egg-link path: `{}`", _0.user_display())] InvalidEggLinkPath(PathBuf), #[error("Invalid .egg-link target: `{}`", _0.user_display())] InvalidEggLinkTarget(PathBuf), #[error("Failed to parse METADATA file: `{}`", path.user_display())] MetadataParse { path: PathBuf, #[source] err: Box, }, #[error("Failed to parse `PKG-INFO` file: `{}`", path.user_display())] PkgInfoParse { path: PathBuf, #[source] err: Box, }, } #[derive(Debug, Clone)] pub struct InstalledDist { pub kind: InstalledDistKind, // Cache data that must be read from the `.dist-info` directory. These are safe to cache as // the `InstalledDist` is immutable after creation. metadata_cache: OnceLock, tags_cache: OnceLock>, } impl From for InstalledDist { fn from(kind: InstalledDistKind) -> Self { Self { kind, metadata_cache: OnceLock::new(), tags_cache: OnceLock::new(), } } } impl std::hash::Hash for InstalledDist { fn hash(&self, state: &mut H) { self.kind.hash(state); } } impl PartialEq for InstalledDist { fn eq(&self, other: &Self) -> bool { self.kind == other.kind } } impl Eq for InstalledDist {} /// A built distribution (wheel) that is installed in a virtual environment. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum InstalledDistKind { /// The distribution was derived from a registry, like `PyPI`. Registry(InstalledRegistryDist), /// The distribution was derived from an arbitrary URL. Url(InstalledDirectUrlDist), /// The distribution was derived from pre-existing `.egg-info` file (as installed by distutils). EggInfoFile(InstalledEggInfoFile), /// The distribution was derived from pre-existing `.egg-info` directory. EggInfoDirectory(InstalledEggInfoDirectory), /// The distribution was derived from an `.egg-link` pointer. LegacyEditable(InstalledLegacyEditable), } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct InstalledRegistryDist { pub name: PackageName, pub version: Version, pub path: Box, pub cache_info: Option, pub build_info: Option, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct InstalledDirectUrlDist { pub name: PackageName, pub version: Version, pub direct_url: Box, pub url: DisplaySafeUrl, pub editable: bool, pub path: Box, pub cache_info: Option, pub build_info: Option, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct InstalledEggInfoFile { pub name: PackageName, pub version: Version, pub path: Box, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct InstalledEggInfoDirectory { pub name: PackageName, pub version: Version, pub path: Box, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct InstalledLegacyEditable { pub name: PackageName, pub version: Version, pub egg_link: Box, pub target: Box, pub target_url: DisplaySafeUrl, pub egg_info: Box, } impl InstalledDist { /// Try to parse a distribution from a `.dist-info` directory name (like `django-5.0a1.dist-info`). /// /// See: pub fn try_from_path(path: &Path) -> Result, InstalledDistError> { // Ex) `cffi-1.16.0.dist-info` if path.extension().is_some_and(|ext| ext == "dist-info") { let Some(file_stem) = path.file_stem() else { return Ok(None); }; let Some(file_stem) = file_stem.to_str() else { return Ok(None); }; let Some((name, version)) = file_stem.split_once('-') else { return Ok(None); }; let name = PackageName::from_str(name)?; let version = Version::from_str(version)?; let cache_info = Self::read_cache_info(path)?; let build_info = Self::read_build_info(path)?; return if let Some(direct_url) = Self::read_direct_url(path)? { match DisplaySafeUrl::try_from(&direct_url) { Ok(url) => Ok(Some(Self::from(InstalledDistKind::Url( InstalledDirectUrlDist { name, version, editable: matches!(&direct_url, DirectUrl::LocalDirectory { dir_info, .. } if dir_info.editable == Some(true)), direct_url: Box::new(direct_url), url, path: path.to_path_buf().into_boxed_path(), cache_info, build_info, }, )))), Err(err) => { warn!("Failed to parse direct URL: {err}"); Ok(Some(Self::from(InstalledDistKind::Registry( InstalledRegistryDist { name, version, path: path.to_path_buf().into_boxed_path(), cache_info, build_info, }, )))) } } } else { Ok(Some(Self::from(InstalledDistKind::Registry( InstalledRegistryDist { name, version, path: path.to_path_buf().into_boxed_path(), cache_info, build_info, }, )))) }; } // Ex) `zstandard-0.22.0-py3.12.egg-info` or `vtk-9.2.6.egg-info` if path.extension().is_some_and(|ext| ext == "egg-info") { let metadata = match fs_err::metadata(path) { Ok(metadata) => metadata, Err(err) => { warn!("Invalid `.egg-info` path: {err}"); return Ok(None); } }; let Some(file_stem) = path.file_stem() else { return Ok(None); }; let Some(file_stem) = file_stem.to_str() else { return Ok(None); }; let file_name = EggInfoFilename::parse(file_stem)?; if let Some(version) = file_name.version { if metadata.is_dir() { return Ok(Some(Self::from(InstalledDistKind::EggInfoDirectory( InstalledEggInfoDirectory { name: file_name.name, version, path: path.to_path_buf().into_boxed_path(), }, )))); } if metadata.is_file() { return Ok(Some(Self::from(InstalledDistKind::EggInfoFile( InstalledEggInfoFile { name: file_name.name, version, path: path.to_path_buf().into_boxed_path(), }, )))); } } if metadata.is_dir() { let Some(egg_metadata) = read_metadata(&path.join("PKG-INFO")) else { return Ok(None); }; return Ok(Some(Self::from(InstalledDistKind::EggInfoDirectory( InstalledEggInfoDirectory { name: file_name.name, version: Version::from_str(&egg_metadata.version)?, path: path.to_path_buf().into_boxed_path(), }, )))); } if metadata.is_file() { let Some(egg_metadata) = read_metadata(path) else { return Ok(None); }; return Ok(Some(Self::from(InstalledDistKind::EggInfoDirectory( InstalledEggInfoDirectory { name: file_name.name, version: Version::from_str(&egg_metadata.version)?, path: path.to_path_buf().into_boxed_path(), }, )))); } } // Ex) `zstandard.egg-link` if path.extension().is_some_and(|ext| ext == "egg-link") { let Some(file_stem) = path.file_stem() else { return Ok(None); }; let Some(file_stem) = file_stem.to_str() else { return Ok(None); }; // https://setuptools.pypa.io/en/latest/deprecated/python_eggs.html#egg-links // https://github.com/pypa/pip/blob/946f95d17431f645da8e2e0bf4054a72db5be766/src/pip/_internal/metadata/importlib/_envs.py#L86-L108 let contents = fs::read_to_string(path)?; let Some(target) = contents.lines().find_map(|line| { let line = line.trim(); if line.is_empty() { None } else { Some(PathBuf::from(line)) } }) else { warn!("Invalid `.egg-link` file: {path:?}"); return Ok(None); }; // Match pip, but note setuptools only puts absolute paths in `.egg-link` files. let target = path .parent() .ok_or_else(|| InstalledDistError::InvalidEggLinkPath(path.to_path_buf()))? .join(target); // Normalisation comes from `pkg_resources.to_filename`. let egg_info = target.join(file_stem.replace('-', "_") + ".egg-info"); let url = DisplaySafeUrl::from_file_path(&target) .map_err(|()| InstalledDistError::InvalidEggLinkTarget(path.to_path_buf()))?; // Mildly unfortunate that we must read metadata to get the version. let Some(egg_metadata) = read_metadata(&egg_info.join("PKG-INFO")) else { return Ok(None); }; return Ok(Some(Self::from(InstalledDistKind::LegacyEditable( InstalledLegacyEditable { name: egg_metadata.name, version: Version::from_str(&egg_metadata.version)?, egg_link: path.to_path_buf().into_boxed_path(), target: target.into_boxed_path(), target_url: url, egg_info: egg_info.into_boxed_path(), }, )))); } Ok(None) } /// Return the [`Path`] at which the distribution is stored on-disk. pub fn install_path(&self) -> &Path { match &self.kind { InstalledDistKind::Registry(dist) => &dist.path, InstalledDistKind::Url(dist) => &dist.path, InstalledDistKind::EggInfoDirectory(dist) => &dist.path, InstalledDistKind::EggInfoFile(dist) => &dist.path, InstalledDistKind::LegacyEditable(dist) => &dist.egg_info, } } /// Return the [`Version`] of the distribution. pub fn version(&self) -> &Version { match &self.kind { InstalledDistKind::Registry(dist) => &dist.version, InstalledDistKind::Url(dist) => &dist.version, InstalledDistKind::EggInfoDirectory(dist) => &dist.version, InstalledDistKind::EggInfoFile(dist) => &dist.version, InstalledDistKind::LegacyEditable(dist) => &dist.version, } } /// Return the [`CacheInfo`] of the distribution, if any. pub fn cache_info(&self) -> Option<&CacheInfo> { match &self.kind { InstalledDistKind::Registry(dist) => dist.cache_info.as_ref(), InstalledDistKind::Url(dist) => dist.cache_info.as_ref(), InstalledDistKind::EggInfoDirectory(..) => None, InstalledDistKind::EggInfoFile(..) => None, InstalledDistKind::LegacyEditable(..) => None, } } /// Return the [`BuildInfo`] of the distribution, if any. pub fn build_info(&self) -> Option<&BuildInfo> { match &self.kind { InstalledDistKind::Registry(dist) => dist.build_info.as_ref(), InstalledDistKind::Url(dist) => dist.build_info.as_ref(), InstalledDistKind::EggInfoDirectory(..) => None, InstalledDistKind::EggInfoFile(..) => None, InstalledDistKind::LegacyEditable(..) => None, } } /// Read the `direct_url.json` file from a `.dist-info` directory. pub fn read_direct_url(path: &Path) -> Result, InstalledDistError> { let path = path.join("direct_url.json"); let file = match fs_err::File::open(&path) { Ok(file) => file, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), Err(err) => return Err(err.into()), }; let direct_url = serde_json::from_reader::, DirectUrl>(BufReader::new(file))?; Ok(Some(direct_url)) } /// Read the `uv_cache.json` file from a `.dist-info` directory. pub fn read_cache_info(path: &Path) -> Result, InstalledDistError> { let path = path.join("uv_cache.json"); let file = match fs_err::File::open(&path) { Ok(file) => file, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), Err(err) => return Err(err.into()), }; let cache_info = serde_json::from_reader::, CacheInfo>(BufReader::new(file))?; Ok(Some(cache_info)) } /// Read the `uv_build.json` file from a `.dist-info` directory. pub fn read_build_info(path: &Path) -> Result, InstalledDistError> { let path = path.join("uv_build.json"); let file = match fs_err::File::open(&path) { Ok(file) => file, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), Err(err) => return Err(err.into()), }; let build_info = serde_json::from_reader::, BuildInfo>(BufReader::new(file))?; Ok(Some(build_info)) } /// Read the `METADATA` file from a `.dist-info` directory. pub fn read_metadata(&self) -> Result<&uv_pypi_types::ResolutionMetadata, InstalledDistError> { if let Some(metadata) = self.metadata_cache.get() { return Ok(metadata); } let metadata = match &self.kind { InstalledDistKind::Registry(_) | InstalledDistKind::Url(_) => { let path = self.install_path().join("METADATA"); let contents = fs::read(&path)?; // TODO(zanieb): Update this to use thiserror so we can unpack parse errors downstream uv_pypi_types::ResolutionMetadata::parse_metadata(&contents).map_err(|err| { InstalledDistError::MetadataParse { path: path.clone(), err: Box::new(err), } })? } InstalledDistKind::EggInfoFile(_) | InstalledDistKind::EggInfoDirectory(_) | InstalledDistKind::LegacyEditable(_) => { let path = match &self.kind { InstalledDistKind::EggInfoFile(dist) => Cow::Borrowed(&*dist.path), InstalledDistKind::EggInfoDirectory(dist) => { Cow::Owned(dist.path.join("PKG-INFO")) } InstalledDistKind::LegacyEditable(dist) => { Cow::Owned(dist.egg_info.join("PKG-INFO")) } _ => unreachable!(), }; let contents = fs::read(path.as_ref())?; uv_pypi_types::ResolutionMetadata::parse_metadata(&contents).map_err(|err| { InstalledDistError::PkgInfoParse { path: path.to_path_buf(), err: Box::new(err), } })? } }; let _ = self.metadata_cache.set(metadata); Ok(self.metadata_cache.get().expect("metadata should be set")) } /// Return the `INSTALLER` of the distribution. pub fn read_installer(&self) -> Result, InstalledDistError> { let path = self.install_path().join("INSTALLER"); match fs::read_to_string(path) { Ok(installer) => Ok(Some(installer.trim().to_owned())), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(err) => Err(err.into()), } } /// Return the supported wheel tags for the distribution from the `WHEEL` file, if available. pub fn read_tags(&self) -> Result, InstalledDistError> { if let Some(tags) = self.tags_cache.get() { return Ok(tags.as_ref()); } let path = match &self.kind { InstalledDistKind::Registry(dist) => &dist.path, InstalledDistKind::Url(dist) => &dist.path, InstalledDistKind::EggInfoFile(_) => return Ok(None), InstalledDistKind::EggInfoDirectory(_) => return Ok(None), InstalledDistKind::LegacyEditable(_) => return Ok(None), }; // Read the `WHEEL` file. let contents = fs_err::read_to_string(path.join("WHEEL"))?; let wheel_file = WheelFile::parse(&contents)?; // Parse the tags. let tags = if let Some(tags) = wheel_file.tags() { Some(ExpandedTags::parse(tags.iter().map(String::as_str))?) } else { None }; let _ = self.tags_cache.set(tags); Ok(self.tags_cache.get().expect("tags should be set").as_ref()) } /// Return true if the distribution is editable. pub fn is_editable(&self) -> bool { matches!( &self.kind, InstalledDistKind::LegacyEditable(_) | InstalledDistKind::Url(InstalledDirectUrlDist { editable: true, .. }) ) } /// Return the [`Url`] of the distribution, if it is editable. pub fn as_editable(&self) -> Option<&Url> { match &self.kind { InstalledDistKind::Registry(_) => None, InstalledDistKind::Url(dist) => dist.editable.then_some(&dist.url), InstalledDistKind::EggInfoFile(_) => None, InstalledDistKind::EggInfoDirectory(_) => None, InstalledDistKind::LegacyEditable(dist) => Some(&dist.target_url), } } /// Return true if the distribution refers to a local file or directory. pub fn is_local(&self) -> bool { match &self.kind { InstalledDistKind::Registry(_) => false, InstalledDistKind::Url(dist) => { matches!(&*dist.direct_url, DirectUrl::LocalDirectory { .. }) } InstalledDistKind::EggInfoFile(_) => false, InstalledDistKind::EggInfoDirectory(_) => false, InstalledDistKind::LegacyEditable(_) => true, } } } impl DistributionMetadata for InstalledDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { VersionOrUrlRef::Version(self.version()) } } impl Name for InstalledRegistryDist { fn name(&self) -> &PackageName { &self.name } } impl Name for InstalledDirectUrlDist { fn name(&self) -> &PackageName { &self.name } } impl Name for InstalledEggInfoFile { fn name(&self) -> &PackageName { &self.name } } impl Name for InstalledEggInfoDirectory { fn name(&self) -> &PackageName { &self.name } } impl Name for InstalledLegacyEditable { fn name(&self) -> &PackageName { &self.name } } impl Name for InstalledDist { fn name(&self) -> &PackageName { match &self.kind { InstalledDistKind::Registry(dist) => dist.name(), InstalledDistKind::Url(dist) => dist.name(), InstalledDistKind::EggInfoDirectory(dist) => dist.name(), InstalledDistKind::EggInfoFile(dist) => dist.name(), InstalledDistKind::LegacyEditable(dist) => dist.name(), } } } impl InstalledMetadata for InstalledRegistryDist { fn installed_version(&self) -> InstalledVersion<'_> { InstalledVersion::Version(&self.version) } } impl InstalledMetadata for InstalledDirectUrlDist { fn installed_version(&self) -> InstalledVersion<'_> { InstalledVersion::Url(&self.url, &self.version) } } impl InstalledMetadata for InstalledEggInfoFile { fn installed_version(&self) -> InstalledVersion<'_> { InstalledVersion::Version(&self.version) } } impl InstalledMetadata for InstalledEggInfoDirectory { fn installed_version(&self) -> InstalledVersion<'_> { InstalledVersion::Version(&self.version) } } impl InstalledMetadata for InstalledLegacyEditable { fn installed_version(&self) -> InstalledVersion<'_> { InstalledVersion::Version(&self.version) } } impl InstalledMetadata for InstalledDist { fn installed_version(&self) -> InstalledVersion<'_> { match &self.kind { InstalledDistKind::Registry(dist) => dist.installed_version(), InstalledDistKind::Url(dist) => dist.installed_version(), InstalledDistKind::EggInfoFile(dist) => dist.installed_version(), InstalledDistKind::EggInfoDirectory(dist) => dist.installed_version(), InstalledDistKind::LegacyEditable(dist) => dist.installed_version(), } } } fn read_metadata(path: &Path) -> Option { let content = match fs::read(path) { Ok(content) => content, Err(err) => { warn!("Failed to read metadata for {path:?}: {err}"); return None; } }; let metadata = match uv_pypi_types::Metadata10::parse_pkg_info(&content) { Ok(metadata) => metadata, Err(err) => { warn!("Failed to parse metadata for {path:?}: {err}"); return None; } }; Some(metadata) } uv-0.9.17+ds1/crates/uv-distribution-types/src/known_platform.rs000066400000000000000000000032571520155276700247350ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString}; /// A platform for which the resolver is solving. #[derive(Debug, Clone, Copy)] pub enum KnownPlatform { Linux, Windows, MacOS, } impl KnownPlatform { /// Return the platform's `sys.platform` value. pub fn sys_platform(self) -> &'static str { match self { Self::Linux => "linux", Self::Windows => "win32", Self::MacOS => "darwin", } } /// Return a [`MarkerTree`] for the platform. pub fn marker(self) -> MarkerTree { MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, value: match self { Self::Linux => arcstr::literal!("linux"), Self::Windows => arcstr::literal!("win32"), Self::MacOS => arcstr::literal!("darwin"), }, }) } /// Determine the [`KnownPlatform`] from a marker tree. pub fn from_marker(marker: MarkerTree) -> Option { if marker == Self::Linux.marker() { Some(Self::Linux) } else if marker == Self::Windows.marker() { Some(Self::Windows) } else if marker == Self::MacOS.marker() { Some(Self::MacOS) } else { None } } } impl Display for KnownPlatform { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Linux => write!(f, "Linux"), Self::Windows => write!(f, "Windows"), Self::MacOS => write!(f, "macOS"), } } } uv-0.9.17+ds1/crates/uv-distribution-types/src/lib.rs000066400000000000000000001275101520155276700224420ustar00rootroot00000000000000//! ## Type hierarchy //! //! When we receive the requirements from `pip sync`, we check which requirements already fulfilled //! in the users environment ([`InstalledDist`]), whether the matching package is in our wheel cache //! ([`CachedDist`]) or whether we need to download, (potentially build) and install it ([`Dist`]). //! These three variants make up [`BuiltDist`]. //! //! ## `Dist` //! A [`Dist`] is either a built distribution (a wheel), or a source distribution that exists at //! some location. We translate every PEP 508 requirement e.g. from `requirements.txt` or from //! `pyproject.toml`'s `[project] dependencies` into a [`Dist`] by checking each index. //! * [`BuiltDist`]: A wheel, with its three possible origins: //! * [`RegistryBuiltDist`] //! * [`DirectUrlBuiltDist`] //! * [`PathBuiltDist`] //! * [`SourceDist`]: A source distribution, with its four possible origins: //! * [`RegistrySourceDist`] //! * [`DirectUrlSourceDist`] //! * [`GitSourceDist`] //! * [`PathSourceDist`] //! //! ## `CachedDist` //! A [`CachedDist`] is a built distribution (wheel) that exists in the local cache, with the two //! possible origins we currently track: //! * [`CachedRegistryDist`] //! * [`CachedDirectUrlDist`] //! //! ## `InstalledDist` //! An [`InstalledDist`] is built distribution (wheel) that is installed in a virtual environment, //! with the two possible origins we currently track: //! * [`InstalledRegistryDist`] //! * [`InstalledDirectUrlDist`] //! //! Since we read this information from [`direct_url.json`](https://packaging.python.org/en/latest/specifications/direct-url-data-structure/), it doesn't match the information [`Dist`] exactly. use std::borrow::Cow; use std::path; use std::path::Path; use std::str::FromStr; use url::Url; use uv_distribution_filename::{ DistExtension, SourceDistExtension, SourceDistFilename, WheelFilename, }; use uv_fs::normalize_absolute_path; use uv_git_types::GitUrl; use uv_normalize::PackageName; use uv_pep440::Version; use uv_pep508::{Pep508Url, VerbatimUrl}; use uv_pypi_types::{ ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, VerbatimParsedUrl, }; use uv_redacted::DisplaySafeUrl; pub use crate::annotation::*; pub use crate::any::*; pub use crate::build_info::*; pub use crate::build_requires::*; pub use crate::buildable::*; pub use crate::cached::*; pub use crate::config_settings::*; pub use crate::dependency_metadata::*; pub use crate::diagnostic::*; pub use crate::dist_error::*; pub use crate::error::*; pub use crate::file::*; pub use crate::hash::*; pub use crate::id::*; pub use crate::index::*; pub use crate::index_name::*; pub use crate::index_url::*; pub use crate::installed::*; pub use crate::known_platform::*; pub use crate::origin::*; pub use crate::pip_index::*; pub use crate::prioritized_distribution::*; pub use crate::requested::*; pub use crate::requirement::*; pub use crate::requires_python::*; pub use crate::resolution::*; pub use crate::resolved::*; pub use crate::specified_requirement::*; pub use crate::status_code_strategy::*; pub use crate::traits::*; mod annotation; mod any; mod build_info; mod build_requires; mod buildable; mod cached; mod config_settings; mod dependency_metadata; mod diagnostic; mod dist_error; mod error; mod file; mod hash; mod id; mod index; mod index_name; mod index_url; mod installed; mod known_platform; mod origin; mod pip_index; mod prioritized_distribution; mod requested; mod requirement; mod requires_python; mod resolution; mod resolved; mod specified_requirement; mod status_code_strategy; mod traits; #[derive(Debug, Clone)] pub enum VersionOrUrlRef<'a, T: Pep508Url = VerbatimUrl> { /// A PEP 440 version specifier, used to identify a distribution in a registry. Version(&'a Version), /// A URL, used to identify a distribution at an arbitrary location. Url(&'a T), } impl VersionOrUrlRef<'_, T> { /// If it is a URL, return its value. pub fn url(&self) -> Option<&T> { match self { Self::Version(_) => None, Self::Url(url) => Some(url), } } } impl Verbatim for VersionOrUrlRef<'_> { fn verbatim(&self) -> Cow<'_, str> { match self { Self::Version(version) => Cow::Owned(format!("=={version}")), Self::Url(url) => Cow::Owned(format!(" @ {}", url.verbatim())), } } } impl std::fmt::Display for VersionOrUrlRef<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Version(version) => write!(f, "=={version}"), Self::Url(url) => write!(f, " @ {url}"), } } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum InstalledVersion<'a> { /// A PEP 440 version specifier, used to identify a distribution in a registry. Version(&'a Version), /// A URL, used to identify a distribution at an arbitrary location, along with the version /// specifier to which it resolved. Url(&'a DisplaySafeUrl, &'a Version), } impl InstalledVersion<'_> { /// If it is a URL, return its value. pub fn url(&self) -> Option<&DisplaySafeUrl> { match self { Self::Version(_) => None, Self::Url(url, _) => Some(url), } } /// If it is a version, return its value. pub fn version(&self) -> &Version { match self { Self::Version(version) => version, Self::Url(_, version) => version, } } } impl std::fmt::Display for InstalledVersion<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Version(version) => write!(f, "=={version}"), Self::Url(url, version) => write!(f, "=={version} (from {url})"), } } } /// Either a built distribution, a wheel, or a source distribution that exists at some location. /// /// The location can be an index, URL or path (wheel), or index, URL, path or Git repository (source distribution). #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum Dist { Built(BuiltDist), Source(SourceDist), } /// A reference to a built or source distribution. #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub enum DistRef<'a> { Built(&'a BuiltDist), Source(&'a SourceDist), } /// A wheel, with its three possible origins (index, url, path) #[derive(Debug, Clone, Hash, PartialEq, Eq)] #[allow(clippy::large_enum_variant)] pub enum BuiltDist { Registry(RegistryBuiltDist), DirectUrl(DirectUrlBuiltDist), Path(PathBuiltDist), } /// A source distribution, with its possible origins (index, url, path, git) #[derive(Debug, Clone, Hash, PartialEq, Eq)] #[allow(clippy::large_enum_variant)] pub enum SourceDist { Registry(RegistrySourceDist), DirectUrl(DirectUrlSourceDist), Git(GitSourceDist), Path(PathSourceDist), Directory(DirectorySourceDist), } /// A built distribution (wheel) that exists in a registry, like `PyPI`. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct RegistryBuiltWheel { pub filename: WheelFilename, pub file: Box, pub index: IndexUrl, } /// A built distribution (wheel) that exists in a registry, like `PyPI`. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct RegistryBuiltDist { /// All wheels associated with this distribution. It is guaranteed /// that there is at least one wheel. pub wheels: Vec, /// The "best" wheel selected based on the current wheel tag /// environment. /// /// This is guaranteed to point into a valid entry in `wheels`. pub best_wheel_index: usize, /// A source distribution if one exists for this distribution. /// /// It is possible for this to be `None`. For example, when a distribution /// has no source distribution, or if it does have one but isn't compatible /// with the user configuration. (e.g., If `Requires-Python` isn't /// compatible with the installed/target Python versions, or if something /// like `--exclude-newer` was used.) pub sdist: Option, // Ideally, this type would have an index URL on it, and the // `RegistryBuiltDist` and `RegistrySourceDist` types would *not* have an // index URL on them. Alas, the --find-links feature makes it technically // possible for the indexes to diverge across wheels/sdists in the same // distribution. // // Note though that at time of writing, when generating a universal lock // file, we require that all index URLs across wheels/sdists for a single // distribution are equivalent. } /// A built distribution (wheel) that exists at an arbitrary URL. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct DirectUrlBuiltDist { /// We require that wheel urls end in the full wheel filename, e.g. /// `https://example.org/packages/flask-3.0.0-py3-none-any.whl` pub filename: WheelFilename, /// The URL without the subdirectory fragment. pub location: Box, /// The URL as it was provided by the user. pub url: VerbatimUrl, } /// A built distribution (wheel) that exists in a local directory. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct PathBuiltDist { pub filename: WheelFilename, /// The absolute path to the wheel which we use for installing. pub install_path: Box, /// The URL as it was provided by the user. pub url: VerbatimUrl, } /// A source distribution that exists in a registry, like `PyPI`. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct RegistrySourceDist { pub name: PackageName, pub version: Version, pub file: Box, /// The file extension, e.g. `tar.gz`, `zip`, etc. pub ext: SourceDistExtension, pub index: IndexUrl, /// When an sdist is selected, it may be the case that there were /// available wheels too. There are many reasons why a wheel might not /// have been chosen (maybe none available are compatible with the /// current environment), but we still want to track that they exist. In /// particular, for generating a universal lockfile, we do not want to /// skip emitting wheels to the lockfile just because the host generating /// the lockfile didn't have any compatible wheels available. pub wheels: Vec, } /// A source distribution that exists at an arbitrary URL. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct DirectUrlSourceDist { /// Unlike [`DirectUrlBuiltDist`], we can't require a full filename with a version here, people /// like using e.g. `foo @ https://github.com/org/repo/archive/master.zip` pub name: PackageName, /// The URL without the subdirectory fragment. pub location: Box, /// The subdirectory within the archive in which the source distribution is located. pub subdirectory: Option>, /// The file extension, e.g. `tar.gz`, `zip`, etc. pub ext: SourceDistExtension, /// The URL as it was provided by the user, including the subdirectory fragment. pub url: VerbatimUrl, } /// A source distribution that exists in a Git repository. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct GitSourceDist { pub name: PackageName, /// The URL without the revision and subdirectory fragment. pub git: Box, /// The subdirectory within the Git repository in which the source distribution is located. pub subdirectory: Option>, /// The URL as it was provided by the user, including the revision and subdirectory fragment. pub url: VerbatimUrl, } /// A source distribution that exists in a local archive (e.g., a `.tar.gz` file). #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct PathSourceDist { pub name: PackageName, pub version: Option, /// The absolute path to the distribution which we use for installing. pub install_path: Box, /// The file extension, e.g. `tar.gz`, `zip`, etc. pub ext: SourceDistExtension, /// The URL as it was provided by the user. pub url: VerbatimUrl, } /// A source distribution that exists in a local directory. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct DirectorySourceDist { pub name: PackageName, /// The absolute path to the distribution which we use for installing. pub install_path: Box, /// Whether the package should be installed in editable mode. pub editable: Option, /// Whether the package should be built and installed. pub r#virtual: Option, /// The URL as it was provided by the user. pub url: VerbatimUrl, } impl Dist { /// A remote built distribution (`.whl`) or source distribution from a `http://` or `https://` /// URL. pub fn from_http_url( name: PackageName, url: VerbatimUrl, location: DisplaySafeUrl, subdirectory: Option>, ext: DistExtension, ) -> Result { match ext { DistExtension::Wheel => { // Validate that the name in the wheel matches that of the requirement. let filename = WheelFilename::from_str(&url.filename()?)?; if filename.name != name { return Err(Error::PackageNameMismatch( name, filename.name, url.verbatim().to_string(), )); } Ok(Self::Built(BuiltDist::DirectUrl(DirectUrlBuiltDist { filename, location: Box::new(location), url, }))) } DistExtension::Source(ext) => { Ok(Self::Source(SourceDist::DirectUrl(DirectUrlSourceDist { name, location: Box::new(location), subdirectory, ext, url, }))) } } } /// A local built or source distribution from a `file://` URL. pub fn from_file_url( name: PackageName, url: VerbatimUrl, install_path: &Path, ext: DistExtension, ) -> Result { // Convert to an absolute path. let install_path = path::absolute(install_path)?; // Normalize the path. let install_path = normalize_absolute_path(&install_path)?; // Validate that the path exists. if !install_path.exists() { return Err(Error::NotFound(url.to_url())); } // Determine whether the path represents a built or source distribution. match ext { DistExtension::Wheel => { // Validate that the name in the wheel matches that of the requirement. let filename = WheelFilename::from_str(&url.filename()?)?; if filename.name != name { return Err(Error::PackageNameMismatch( name, filename.name, url.verbatim().to_string(), )); } Ok(Self::Built(BuiltDist::Path(PathBuiltDist { filename, install_path: install_path.into_boxed_path(), url, }))) } DistExtension::Source(ext) => { // If there is a version in the filename, record it. let version = url .filename() .ok() .and_then(|filename| { SourceDistFilename::parse(filename.as_ref(), ext, &name).ok() }) .map(|filename| filename.version); Ok(Self::Source(SourceDist::Path(PathSourceDist { name, version, install_path: install_path.into_boxed_path(), ext, url, }))) } } } /// A local source tree from a `file://` URL. pub fn from_directory_url( name: PackageName, url: VerbatimUrl, install_path: &Path, editable: Option, r#virtual: Option, ) -> Result { // Convert to an absolute path. let install_path = path::absolute(install_path)?; // Normalize the path. let install_path = normalize_absolute_path(&install_path)?; // Validate that the path exists. if !install_path.exists() { return Err(Error::NotFound(url.to_url())); } // Determine whether the path represents an archive or a directory. Ok(Self::Source(SourceDist::Directory(DirectorySourceDist { name, install_path: install_path.into_boxed_path(), editable, r#virtual, url, }))) } /// A remote source distribution from a `git+https://` or `git+ssh://` url. pub fn from_git_url( name: PackageName, url: VerbatimUrl, git: GitUrl, subdirectory: Option>, ) -> Result { Ok(Self::Source(SourceDist::Git(GitSourceDist { name, git: Box::new(git), subdirectory, url, }))) } /// Create a [`Dist`] for a URL-based distribution. pub fn from_url(name: PackageName, url: VerbatimParsedUrl) -> Result { match url.parsed_url { ParsedUrl::Archive(archive) => Self::from_http_url( name, url.verbatim, archive.url, archive.subdirectory, archive.ext, ), ParsedUrl::Path(file) => { Self::from_file_url(name, url.verbatim, &file.install_path, file.ext) } ParsedUrl::Directory(directory) => Self::from_directory_url( name, url.verbatim, &directory.install_path, directory.editable, directory.r#virtual, ), ParsedUrl::Git(git) => { Self::from_git_url(name, url.verbatim, git.url, git.subdirectory) } } } /// Return true if the distribution is editable. pub fn is_editable(&self) -> bool { match self { Self::Source(dist) => dist.is_editable(), Self::Built(_) => false, } } /// Return true if the distribution refers to a local file or directory. pub fn is_local(&self) -> bool { match self { Self::Source(dist) => dist.is_local(), Self::Built(dist) => dist.is_local(), } } /// Returns the [`IndexUrl`], if the distribution is from a registry. pub fn index(&self) -> Option<&IndexUrl> { match self { Self::Built(dist) => dist.index(), Self::Source(dist) => dist.index(), } } /// Returns the [`File`] instance, if this dist is from a registry with simple json api support pub fn file(&self) -> Option<&File> { match self { Self::Built(built) => built.file(), Self::Source(source) => source.file(), } } /// Return the source tree of the distribution, if available. pub fn source_tree(&self) -> Option<&Path> { match self { Self::Built { .. } => None, Self::Source(source) => source.source_tree(), } } /// Returns the version of the distribution, if it is known. pub fn version(&self) -> Option<&Version> { match self { Self::Built(wheel) => Some(wheel.version()), Self::Source(source_dist) => source_dist.version(), } } /// Convert this distribution into a reference. pub fn as_ref(&self) -> DistRef<'_> { match self { Self::Built(dist) => DistRef::Built(dist), Self::Source(dist) => DistRef::Source(dist), } } } impl<'a> From<&'a Dist> for DistRef<'a> { fn from(dist: &'a Dist) -> Self { match dist { Dist::Built(built) => DistRef::Built(built), Dist::Source(source) => DistRef::Source(source), } } } impl<'a> From<&'a SourceDist> for DistRef<'a> { fn from(dist: &'a SourceDist) -> Self { DistRef::Source(dist) } } impl<'a> From<&'a BuiltDist> for DistRef<'a> { fn from(dist: &'a BuiltDist) -> Self { DistRef::Built(dist) } } impl BuiltDist { /// Return true if the distribution refers to a local file or directory. pub fn is_local(&self) -> bool { matches!(self, Self::Path(_)) } /// Returns the [`IndexUrl`], if the distribution is from a registry. pub fn index(&self) -> Option<&IndexUrl> { match self { Self::Registry(registry) => Some(®istry.best_wheel().index), Self::DirectUrl(_) => None, Self::Path(_) => None, } } /// Returns the [`File`] instance, if this distribution is from a registry. pub fn file(&self) -> Option<&File> { match self { Self::Registry(registry) => Some(®istry.best_wheel().file), Self::DirectUrl(_) | Self::Path(_) => None, } } pub fn version(&self) -> &Version { match self { Self::Registry(wheels) => &wheels.best_wheel().filename.version, Self::DirectUrl(wheel) => &wheel.filename.version, Self::Path(wheel) => &wheel.filename.version, } } } impl SourceDist { /// Returns the [`IndexUrl`], if the distribution is from a registry. pub fn index(&self) -> Option<&IndexUrl> { match self { Self::Registry(registry) => Some(®istry.index), Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) | Self::Directory(_) => None, } } /// Returns the [`File`] instance, if this dist is from a registry with simple json api support pub fn file(&self) -> Option<&File> { match self { Self::Registry(registry) => Some(®istry.file), Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) | Self::Directory(_) => None, } } /// Returns the [`Version`] of the distribution, if it is known. pub fn version(&self) -> Option<&Version> { match self { Self::Registry(source_dist) => Some(&source_dist.version), Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) | Self::Directory(_) => None, } } /// Returns `true` if the distribution is editable. pub fn is_editable(&self) -> bool { match self { Self::Directory(DirectorySourceDist { editable, .. }) => editable.unwrap_or(false), _ => false, } } /// Returns `true` if the distribution is virtual. pub fn is_virtual(&self) -> bool { match self { Self::Directory(DirectorySourceDist { r#virtual, .. }) => r#virtual.unwrap_or(false), _ => false, } } /// Returns `true` if the distribution refers to a local file or directory. pub fn is_local(&self) -> bool { matches!(self, Self::Directory(_) | Self::Path(_)) } /// Returns the path to the source distribution, if it's a local distribution. pub fn as_path(&self) -> Option<&Path> { match self { Self::Path(dist) => Some(&dist.install_path), Self::Directory(dist) => Some(&dist.install_path), _ => None, } } /// Returns the source tree of the distribution, if available. pub fn source_tree(&self) -> Option<&Path> { match self { Self::Directory(dist) => Some(&dist.install_path), _ => None, } } } impl RegistryBuiltDist { /// Returns the best or "most compatible" wheel in this distribution. pub fn best_wheel(&self) -> &RegistryBuiltWheel { &self.wheels[self.best_wheel_index] } } impl DirectUrlBuiltDist { /// Return the [`ParsedUrl`] for the distribution. pub fn parsed_url(&self) -> ParsedUrl { ParsedUrl::Archive(ParsedArchiveUrl::from_source( (*self.location).clone(), None, DistExtension::Wheel, )) } } impl PathBuiltDist { /// Return the [`ParsedUrl`] for the distribution. pub fn parsed_url(&self) -> ParsedUrl { ParsedUrl::Path(ParsedPathUrl::from_source( self.install_path.clone(), DistExtension::Wheel, self.url.to_url(), )) } } impl PathSourceDist { /// Return the [`ParsedUrl`] for the distribution. pub fn parsed_url(&self) -> ParsedUrl { ParsedUrl::Path(ParsedPathUrl::from_source( self.install_path.clone(), DistExtension::Source(self.ext), self.url.to_url(), )) } } impl DirectUrlSourceDist { /// Return the [`ParsedUrl`] for the distribution. pub fn parsed_url(&self) -> ParsedUrl { ParsedUrl::Archive(ParsedArchiveUrl::from_source( (*self.location).clone(), self.subdirectory.clone(), DistExtension::Source(self.ext), )) } } impl GitSourceDist { /// Return the [`ParsedUrl`] for the distribution. pub fn parsed_url(&self) -> ParsedUrl { ParsedUrl::Git(ParsedGitUrl::from_source( (*self.git).clone(), self.subdirectory.clone(), )) } } impl DirectorySourceDist { /// Return the [`ParsedUrl`] for the distribution. pub fn parsed_url(&self) -> ParsedUrl { ParsedUrl::Directory(ParsedDirectoryUrl::from_source( self.install_path.clone(), self.editable, self.r#virtual, self.url.to_url(), )) } } impl Name for RegistryBuiltWheel { fn name(&self) -> &PackageName { &self.filename.name } } impl Name for RegistryBuiltDist { fn name(&self) -> &PackageName { self.best_wheel().name() } } impl Name for DirectUrlBuiltDist { fn name(&self) -> &PackageName { &self.filename.name } } impl Name for PathBuiltDist { fn name(&self) -> &PackageName { &self.filename.name } } impl Name for RegistrySourceDist { fn name(&self) -> &PackageName { &self.name } } impl Name for DirectUrlSourceDist { fn name(&self) -> &PackageName { &self.name } } impl Name for GitSourceDist { fn name(&self) -> &PackageName { &self.name } } impl Name for PathSourceDist { fn name(&self) -> &PackageName { &self.name } } impl Name for DirectorySourceDist { fn name(&self) -> &PackageName { &self.name } } impl Name for SourceDist { fn name(&self) -> &PackageName { match self { Self::Registry(dist) => dist.name(), Self::DirectUrl(dist) => dist.name(), Self::Git(dist) => dist.name(), Self::Path(dist) => dist.name(), Self::Directory(dist) => dist.name(), } } } impl Name for BuiltDist { fn name(&self) -> &PackageName { match self { Self::Registry(dist) => dist.name(), Self::DirectUrl(dist) => dist.name(), Self::Path(dist) => dist.name(), } } } impl Name for Dist { fn name(&self) -> &PackageName { match self { Self::Built(dist) => dist.name(), Self::Source(dist) => dist.name(), } } } impl Name for CompatibleDist<'_> { fn name(&self) -> &PackageName { match self { Self::InstalledDist(dist) => dist.name(), Self::SourceDist { sdist, prioritized: _, } => sdist.name(), Self::CompatibleWheel { wheel, priority: _, prioritized: _, } => wheel.name(), Self::IncompatibleWheel { sdist, wheel: _, prioritized: _, } => sdist.name(), } } } impl DistributionMetadata for RegistryBuiltWheel { fn version_or_url(&self) -> VersionOrUrlRef<'_> { VersionOrUrlRef::Version(&self.filename.version) } } impl DistributionMetadata for RegistryBuiltDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { self.best_wheel().version_or_url() } } impl DistributionMetadata for DirectUrlBuiltDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { VersionOrUrlRef::Url(&self.url) } } impl DistributionMetadata for PathBuiltDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { VersionOrUrlRef::Url(&self.url) } } impl DistributionMetadata for RegistrySourceDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { VersionOrUrlRef::Version(&self.version) } } impl DistributionMetadata for DirectUrlSourceDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { VersionOrUrlRef::Url(&self.url) } } impl DistributionMetadata for GitSourceDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { VersionOrUrlRef::Url(&self.url) } } impl DistributionMetadata for PathSourceDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { VersionOrUrlRef::Url(&self.url) } } impl DistributionMetadata for DirectorySourceDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { VersionOrUrlRef::Url(&self.url) } } impl DistributionMetadata for SourceDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { match self { Self::Registry(dist) => dist.version_or_url(), Self::DirectUrl(dist) => dist.version_or_url(), Self::Git(dist) => dist.version_or_url(), Self::Path(dist) => dist.version_or_url(), Self::Directory(dist) => dist.version_or_url(), } } } impl DistributionMetadata for BuiltDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { match self { Self::Registry(dist) => dist.version_or_url(), Self::DirectUrl(dist) => dist.version_or_url(), Self::Path(dist) => dist.version_or_url(), } } } impl DistributionMetadata for Dist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { match self { Self::Built(dist) => dist.version_or_url(), Self::Source(dist) => dist.version_or_url(), } } } impl RemoteSource for File { fn filename(&self) -> Result, Error> { Ok(Cow::Borrowed(&self.filename)) } fn size(&self) -> Option { self.size } } impl RemoteSource for Url { fn filename(&self) -> Result, Error> { // Identify the last segment of the URL as the filename. let mut path_segments = self .path_segments() .ok_or_else(|| Error::MissingPathSegments(self.to_string()))?; // This is guaranteed by the contract of `Url::path_segments`. let last = path_segments .next_back() .expect("path segments is non-empty"); // Decode the filename, which may be percent-encoded. let filename = percent_encoding::percent_decode_str(last).decode_utf8()?; Ok(filename) } fn size(&self) -> Option { None } } impl RemoteSource for UrlString { fn filename(&self) -> Result, Error> { // Take the last segment, stripping any query or fragment. let last = self .base_str() .split('/') .next_back() .ok_or_else(|| Error::MissingPathSegments(self.to_string()))?; // Decode the filename, which may be percent-encoded. let filename = percent_encoding::percent_decode_str(last).decode_utf8()?; Ok(filename) } fn size(&self) -> Option { None } } impl RemoteSource for RegistryBuiltWheel { fn filename(&self) -> Result, Error> { self.file.filename() } fn size(&self) -> Option { self.file.size() } } impl RemoteSource for RegistryBuiltDist { fn filename(&self) -> Result, Error> { self.best_wheel().filename() } fn size(&self) -> Option { self.best_wheel().size() } } impl RemoteSource for RegistrySourceDist { fn filename(&self) -> Result, Error> { self.file.filename() } fn size(&self) -> Option { self.file.size() } } impl RemoteSource for DirectUrlBuiltDist { fn filename(&self) -> Result, Error> { self.url.filename() } fn size(&self) -> Option { self.url.size() } } impl RemoteSource for DirectUrlSourceDist { fn filename(&self) -> Result, Error> { self.url.filename() } fn size(&self) -> Option { self.url.size() } } impl RemoteSource for GitSourceDist { fn filename(&self) -> Result, Error> { // The filename is the last segment of the URL, before any `@`. match self.url.filename()? { Cow::Borrowed(filename) => { if let Some((_, filename)) = filename.rsplit_once('@') { Ok(Cow::Borrowed(filename)) } else { Ok(Cow::Borrowed(filename)) } } Cow::Owned(filename) => { if let Some((_, filename)) = filename.rsplit_once('@') { Ok(Cow::Owned(filename.to_owned())) } else { Ok(Cow::Owned(filename)) } } } } fn size(&self) -> Option { self.url.size() } } impl RemoteSource for PathBuiltDist { fn filename(&self) -> Result, Error> { self.url.filename() } fn size(&self) -> Option { self.url.size() } } impl RemoteSource for PathSourceDist { fn filename(&self) -> Result, Error> { self.url.filename() } fn size(&self) -> Option { self.url.size() } } impl RemoteSource for DirectorySourceDist { fn filename(&self) -> Result, Error> { self.url.filename() } fn size(&self) -> Option { self.url.size() } } impl RemoteSource for SourceDist { fn filename(&self) -> Result, Error> { match self { Self::Registry(dist) => dist.filename(), Self::DirectUrl(dist) => dist.filename(), Self::Git(dist) => dist.filename(), Self::Path(dist) => dist.filename(), Self::Directory(dist) => dist.filename(), } } fn size(&self) -> Option { match self { Self::Registry(dist) => dist.size(), Self::DirectUrl(dist) => dist.size(), Self::Git(dist) => dist.size(), Self::Path(dist) => dist.size(), Self::Directory(dist) => dist.size(), } } } impl RemoteSource for BuiltDist { fn filename(&self) -> Result, Error> { match self { Self::Registry(dist) => dist.filename(), Self::DirectUrl(dist) => dist.filename(), Self::Path(dist) => dist.filename(), } } fn size(&self) -> Option { match self { Self::Registry(dist) => dist.size(), Self::DirectUrl(dist) => dist.size(), Self::Path(dist) => dist.size(), } } } impl RemoteSource for Dist { fn filename(&self) -> Result, Error> { match self { Self::Built(dist) => dist.filename(), Self::Source(dist) => dist.filename(), } } fn size(&self) -> Option { match self { Self::Built(dist) => dist.size(), Self::Source(dist) => dist.size(), } } } impl Identifier for DisplaySafeUrl { fn distribution_id(&self) -> DistributionId { DistributionId::Url(uv_cache_key::CanonicalUrl::new(self)) } fn resource_id(&self) -> ResourceId { ResourceId::Url(uv_cache_key::RepositoryUrl::new(self)) } } impl Identifier for File { fn distribution_id(&self) -> DistributionId { self.hashes .first() .cloned() .map(DistributionId::Digest) .unwrap_or_else(|| self.url.distribution_id()) } fn resource_id(&self) -> ResourceId { self.hashes .first() .cloned() .map(ResourceId::Digest) .unwrap_or_else(|| self.url.resource_id()) } } impl Identifier for Path { fn distribution_id(&self) -> DistributionId { DistributionId::PathBuf(self.to_path_buf()) } fn resource_id(&self) -> ResourceId { ResourceId::PathBuf(self.to_path_buf()) } } impl Identifier for FileLocation { fn distribution_id(&self) -> DistributionId { match self { Self::RelativeUrl(base, url) => { DistributionId::RelativeUrl(base.to_string(), url.to_string()) } Self::AbsoluteUrl(url) => DistributionId::AbsoluteUrl(url.to_string()), } } fn resource_id(&self) -> ResourceId { match self { Self::RelativeUrl(base, url) => { ResourceId::RelativeUrl(base.to_string(), url.to_string()) } Self::AbsoluteUrl(url) => ResourceId::AbsoluteUrl(url.to_string()), } } } impl Identifier for RegistryBuiltWheel { fn distribution_id(&self) -> DistributionId { self.file.distribution_id() } fn resource_id(&self) -> ResourceId { self.file.resource_id() } } impl Identifier for RegistryBuiltDist { fn distribution_id(&self) -> DistributionId { self.best_wheel().distribution_id() } fn resource_id(&self) -> ResourceId { self.best_wheel().resource_id() } } impl Identifier for RegistrySourceDist { fn distribution_id(&self) -> DistributionId { self.file.distribution_id() } fn resource_id(&self) -> ResourceId { self.file.resource_id() } } impl Identifier for DirectUrlBuiltDist { fn distribution_id(&self) -> DistributionId { self.url.distribution_id() } fn resource_id(&self) -> ResourceId { self.url.resource_id() } } impl Identifier for DirectUrlSourceDist { fn distribution_id(&self) -> DistributionId { self.url.distribution_id() } fn resource_id(&self) -> ResourceId { self.url.resource_id() } } impl Identifier for PathBuiltDist { fn distribution_id(&self) -> DistributionId { self.url.distribution_id() } fn resource_id(&self) -> ResourceId { self.url.resource_id() } } impl Identifier for PathSourceDist { fn distribution_id(&self) -> DistributionId { self.url.distribution_id() } fn resource_id(&self) -> ResourceId { self.url.resource_id() } } impl Identifier for DirectorySourceDist { fn distribution_id(&self) -> DistributionId { self.url.distribution_id() } fn resource_id(&self) -> ResourceId { self.url.resource_id() } } impl Identifier for GitSourceDist { fn distribution_id(&self) -> DistributionId { self.url.distribution_id() } fn resource_id(&self) -> ResourceId { self.url.resource_id() } } impl Identifier for SourceDist { fn distribution_id(&self) -> DistributionId { match self { Self::Registry(dist) => dist.distribution_id(), Self::DirectUrl(dist) => dist.distribution_id(), Self::Git(dist) => dist.distribution_id(), Self::Path(dist) => dist.distribution_id(), Self::Directory(dist) => dist.distribution_id(), } } fn resource_id(&self) -> ResourceId { match self { Self::Registry(dist) => dist.resource_id(), Self::DirectUrl(dist) => dist.resource_id(), Self::Git(dist) => dist.resource_id(), Self::Path(dist) => dist.resource_id(), Self::Directory(dist) => dist.resource_id(), } } } impl Identifier for BuiltDist { fn distribution_id(&self) -> DistributionId { match self { Self::Registry(dist) => dist.distribution_id(), Self::DirectUrl(dist) => dist.distribution_id(), Self::Path(dist) => dist.distribution_id(), } } fn resource_id(&self) -> ResourceId { match self { Self::Registry(dist) => dist.resource_id(), Self::DirectUrl(dist) => dist.resource_id(), Self::Path(dist) => dist.resource_id(), } } } impl Identifier for InstalledDist { fn distribution_id(&self) -> DistributionId { self.install_path().distribution_id() } fn resource_id(&self) -> ResourceId { self.install_path().resource_id() } } impl Identifier for Dist { fn distribution_id(&self) -> DistributionId { match self { Self::Built(dist) => dist.distribution_id(), Self::Source(dist) => dist.distribution_id(), } } fn resource_id(&self) -> ResourceId { match self { Self::Built(dist) => dist.resource_id(), Self::Source(dist) => dist.resource_id(), } } } impl Identifier for DirectSourceUrl<'_> { fn distribution_id(&self) -> DistributionId { self.url.distribution_id() } fn resource_id(&self) -> ResourceId { self.url.resource_id() } } impl Identifier for GitSourceUrl<'_> { fn distribution_id(&self) -> DistributionId { self.url.distribution_id() } fn resource_id(&self) -> ResourceId { self.url.resource_id() } } impl Identifier for PathSourceUrl<'_> { fn distribution_id(&self) -> DistributionId { self.url.distribution_id() } fn resource_id(&self) -> ResourceId { self.url.resource_id() } } impl Identifier for DirectorySourceUrl<'_> { fn distribution_id(&self) -> DistributionId { self.url.distribution_id() } fn resource_id(&self) -> ResourceId { self.url.resource_id() } } impl Identifier for SourceUrl<'_> { fn distribution_id(&self) -> DistributionId { match self { Self::Direct(url) => url.distribution_id(), Self::Git(url) => url.distribution_id(), Self::Path(url) => url.distribution_id(), Self::Directory(url) => url.distribution_id(), } } fn resource_id(&self) -> ResourceId { match self { Self::Direct(url) => url.resource_id(), Self::Git(url) => url.resource_id(), Self::Path(url) => url.resource_id(), Self::Directory(url) => url.resource_id(), } } } impl Identifier for BuildableSource<'_> { fn distribution_id(&self) -> DistributionId { match self { Self::Dist(source) => source.distribution_id(), Self::Url(source) => source.distribution_id(), } } fn resource_id(&self) -> ResourceId { match self { Self::Dist(source) => source.resource_id(), Self::Url(source) => source.resource_id(), } } } #[cfg(test)] mod test { use crate::{BuiltDist, Dist, RemoteSource, SourceDist, UrlString}; use uv_redacted::DisplaySafeUrl; /// Ensure that we don't accidentally grow the `Dist` sizes. #[test] fn dist_size() { assert!(size_of::() <= 200, "{}", size_of::()); assert!(size_of::() <= 200, "{}", size_of::()); assert!( size_of::() <= 176, "{}", size_of::() ); } #[test] fn remote_source() { for url in [ "https://example.com/foo-0.1.0.tar.gz", "https://example.com/foo-0.1.0.tar.gz#fragment", "https://example.com/foo-0.1.0.tar.gz?query", "https://example.com/foo-0.1.0.tar.gz?query#fragment", "https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment", "https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment/3", ] { let url = DisplaySafeUrl::parse(url).unwrap(); assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}"); let url = UrlString::from(url.clone()); assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}"); } } } uv-0.9.17+ds1/crates/uv-distribution-types/src/origin.rs000066400000000000000000000006661520155276700231650ustar00rootroot00000000000000/// The origin of a piece of configuration. #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub enum Origin { /// The setting was provided via the CLI. Cli, /// The setting was provided via a user-level configuration file. User, /// The setting was provided via a project-level configuration file. Project, /// The setting was provided via a `requirements.txt` file. RequirementsTxt, } uv-0.9.17+ds1/crates/uv-distribution-types/src/pip_index.rs000066400000000000000000000040251520155276700236460ustar00rootroot00000000000000//! Compatibility structs for converting between [`IndexUrl`] and [`Index`]. These structs are //! parsed and deserialized as [`IndexUrl`], but are stored as [`Index`] with the appropriate //! flags set. use serde::{Deserialize, Deserializer, Serialize}; #[cfg(feature = "schemars")] use std::borrow::Cow; use std::path::Path; use crate::{Index, IndexUrl}; macro_rules! impl_index { ($name:ident, $from:expr) => { #[derive(Debug, Clone, Eq, PartialEq)] pub struct $name(Index); impl $name { pub fn relative_to(self, root_dir: &Path) -> Result { Ok(Self(self.0.relative_to(root_dir)?)) } } impl From<$name> for Index { fn from(value: $name) -> Self { value.0 } } impl From for $name { fn from(value: Index) -> Self { Self(value) } } impl Serialize for $name { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { self.0.url().serialize(serializer) } } impl<'de> Deserialize<'de> for $name { fn deserialize(deserializer: D) -> Result<$name, D::Error> where D: Deserializer<'de>, { IndexUrl::deserialize(deserializer).map($from).map(Self) } } #[cfg(feature = "schemars")] impl schemars::JsonSchema for $name { fn schema_name() -> Cow<'static, str> { IndexUrl::schema_name() } fn json_schema( generator: &mut schemars::generate::SchemaGenerator, ) -> schemars::Schema { IndexUrl::json_schema(generator) } } }; } impl_index!(PipIndex, Index::from_index_url); impl_index!(PipExtraIndex, Index::from_extra_index_url); impl_index!(PipFindLinks, Index::from_find_links); uv-0.9.17+ds1/crates/uv-distribution-types/src/prioritized_distribution.rs000066400000000000000000001333701520155276700270400ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use arcstr::ArcStr; use owo_colors::OwoColorize; use tracing::debug; use uv_distribution_filename::{BuildTag, WheelFilename}; use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers}; use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString}; use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, PlatformTag, TagPriority, Tags}; use uv_pypi_types::{HashDigest, Yanked}; use crate::{ File, InstalledDist, KnownPlatform, RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist, ResolvedDistRef, }; /// A collection of distributions that have been filtered by relevance. #[derive(Debug, Default, Clone)] pub struct PrioritizedDist(Box); /// [`PrioritizedDist`] is boxed because [`Dist`] is large. #[derive(Debug, Clone)] struct PrioritizedDistInner { /// The highest-priority source distribution. Between compatible source distributions this priority is arbitrary. source: Option<(RegistrySourceDist, SourceDistCompatibility)>, /// The highest-priority wheel index. When present, it is /// guaranteed to be a valid index into `wheels`. best_wheel_index: Option, /// The set of all wheels associated with this distribution. wheels: Vec<(RegistryBuiltWheel, WheelCompatibility)>, /// The hashes for each distribution. hashes: Vec, /// The set of supported platforms for the distribution, described in terms of their markers. markers: MarkerTree, } impl Default for PrioritizedDistInner { fn default() -> Self { Self { source: None, best_wheel_index: None, wheels: Vec::new(), hashes: Vec::new(), markers: MarkerTree::FALSE, } } } /// A distribution that can be used for both resolution and installation. #[derive(Debug, Copy, Clone)] pub enum CompatibleDist<'a> { /// The distribution is already installed and can be used. InstalledDist(&'a InstalledDist), /// The distribution should be resolved and installed using a source distribution. SourceDist { /// The source distribution that should be used. sdist: &'a RegistrySourceDist, /// The prioritized distribution that the sdist came from. prioritized: &'a PrioritizedDist, }, /// The distribution should be resolved and installed using a wheel distribution. CompatibleWheel { /// The wheel that should be used. wheel: &'a RegistryBuiltWheel, /// The platform priority associated with the wheel. priority: Option, /// The prioritized distribution that the wheel came from. prioritized: &'a PrioritizedDist, }, /// The distribution should be resolved using an incompatible wheel distribution, but /// installed using a source distribution. IncompatibleWheel { /// The sdist to be used during installation. sdist: &'a RegistrySourceDist, /// The wheel to be used during resolution. wheel: &'a RegistryBuiltWheel, /// The prioritized distribution that the wheel and sdist came from. prioritized: &'a PrioritizedDist, }, } impl CompatibleDist<'_> { /// Return the `requires-python` specifier for the distribution, if any. pub fn requires_python(&self) -> Option<&VersionSpecifiers> { match self { Self::InstalledDist(_) => None, Self::SourceDist { sdist, .. } => sdist.file.requires_python.as_ref(), Self::CompatibleWheel { wheel, .. } => wheel.file.requires_python.as_ref(), Self::IncompatibleWheel { sdist, .. } => sdist.file.requires_python.as_ref(), } } // For installable distributions, return the prioritized distribution it was derived from. pub fn prioritized(&self) -> Option<&PrioritizedDist> { match self { Self::InstalledDist(_) => None, Self::SourceDist { prioritized, .. } | Self::CompatibleWheel { prioritized, .. } | Self::IncompatibleWheel { prioritized, .. } => Some(prioritized), } } /// Return the set of supported platform the distribution, in terms of their markers. pub fn implied_markers(&self) -> MarkerTree { match self.prioritized() { Some(prioritized) => prioritized.0.markers, None => MarkerTree::TRUE, } } } #[derive(Debug, PartialEq, Eq, Clone)] pub enum IncompatibleDist { /// An incompatible wheel is available. Wheel(IncompatibleWheel), /// An incompatible source distribution is available. Source(IncompatibleSource), /// No distributions are available Unavailable, } impl IncompatibleDist { pub fn singular_message(&self) -> String { match self { Self::Wheel(incompatibility) => match incompatibility { IncompatibleWheel::NoBinary => format!("has {self}"), IncompatibleWheel::Tag(_) => format!("has {self}"), IncompatibleWheel::Yanked(_) => format!("was {self}"), IncompatibleWheel::ExcludeNewer(ts) => match ts { Some(_) => format!("was {self}"), None => format!("has {self}"), }, IncompatibleWheel::RequiresPython(..) => format!("requires {self}"), IncompatibleWheel::MissingPlatform(_) => format!("has {self}"), }, Self::Source(incompatibility) => match incompatibility { IncompatibleSource::NoBuild => format!("has {self}"), IncompatibleSource::Yanked(_) => format!("was {self}"), IncompatibleSource::ExcludeNewer(ts) => match ts { Some(_) => format!("was {self}"), None => format!("has {self}"), }, IncompatibleSource::RequiresPython(..) => { format!("requires {self}") } }, Self::Unavailable => format!("has {self}"), } } pub fn plural_message(&self) -> String { match self { Self::Wheel(incompatibility) => match incompatibility { IncompatibleWheel::NoBinary => format!("have {self}"), IncompatibleWheel::Tag(_) => format!("have {self}"), IncompatibleWheel::Yanked(_) => format!("were {self}"), IncompatibleWheel::ExcludeNewer(ts) => match ts { Some(_) => format!("were {self}"), None => format!("have {self}"), }, IncompatibleWheel::RequiresPython(..) => format!("require {self}"), IncompatibleWheel::MissingPlatform(_) => format!("have {self}"), }, Self::Source(incompatibility) => match incompatibility { IncompatibleSource::NoBuild => format!("have {self}"), IncompatibleSource::Yanked(_) => format!("were {self}"), IncompatibleSource::ExcludeNewer(ts) => match ts { Some(_) => format!("were {self}"), None => format!("have {self}"), }, IncompatibleSource::RequiresPython(..) => { format!("require {self}") } }, Self::Unavailable => format!("have {self}"), } } pub fn context_message( &self, tags: Option<&Tags>, requires_python: Option, ) -> Option { match self { Self::Wheel(incompatibility) => match incompatibility { IncompatibleWheel::Tag(IncompatibleTag::Python) => { let tag = tags?.python_tag().as_ref().map(ToString::to_string)?; Some(format!("(e.g., `{tag}`)", tag = tag.cyan())) } IncompatibleWheel::Tag(IncompatibleTag::Abi) => { let tag = tags?.abi_tag().as_ref().map(ToString::to_string)?; Some(format!("(e.g., `{tag}`)", tag = tag.cyan())) } IncompatibleWheel::Tag(IncompatibleTag::AbiPythonVersion) => { let tag = requires_python?; Some(format!("(e.g., `{tag}`)", tag = tag.cyan())) } IncompatibleWheel::Tag(IncompatibleTag::Platform) => { let tag = tags?.platform_tag().map(ToString::to_string)?; Some(format!("(e.g., `{tag}`)", tag = tag.cyan())) } IncompatibleWheel::Tag(IncompatibleTag::Invalid) => None, IncompatibleWheel::NoBinary => None, IncompatibleWheel::Yanked(..) => None, IncompatibleWheel::ExcludeNewer(..) => None, IncompatibleWheel::RequiresPython(..) => None, IncompatibleWheel::MissingPlatform(..) => None, }, Self::Source(..) => None, Self::Unavailable => None, } } } impl Display for IncompatibleDist { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Wheel(incompatibility) => match incompatibility { IncompatibleWheel::NoBinary => f.write_str("no source distribution"), IncompatibleWheel::Tag(tag) => match tag { IncompatibleTag::Invalid => f.write_str("no wheels with valid tags"), IncompatibleTag::Python => { f.write_str("no wheels with a matching Python implementation tag") } IncompatibleTag::Abi => f.write_str("no wheels with a matching Python ABI tag"), IncompatibleTag::AbiPythonVersion => { f.write_str("no wheels with a matching Python version tag") } IncompatibleTag::Platform => { f.write_str("no wheels with a matching platform tag") } }, IncompatibleWheel::Yanked(yanked) => match yanked { Yanked::Bool(_) => f.write_str("yanked"), Yanked::Reason(reason) => write!( f, "yanked (reason: {})", reason.trim().trim_end_matches('.') ), }, IncompatibleWheel::ExcludeNewer(ts) => match ts { Some(_) => f.write_str("published after the exclude newer time"), None => f.write_str("no publish time"), }, IncompatibleWheel::RequiresPython(python, _) => { write!(f, "Python {python}") } IncompatibleWheel::MissingPlatform(marker) => { if let Some(platform) = KnownPlatform::from_marker(*marker) { write!(f, "no {platform}-compatible wheels") } else if let Some(marker) = marker.try_to_string() { write!(f, "no `{marker}`-compatible wheels") } else { write!(f, "no compatible wheels") } } }, Self::Source(incompatibility) => match incompatibility { IncompatibleSource::NoBuild => f.write_str("no usable wheels"), IncompatibleSource::Yanked(yanked) => match yanked { Yanked::Bool(_) => f.write_str("yanked"), Yanked::Reason(reason) => write!( f, "yanked (reason: {})", reason.trim().trim_end_matches('.') ), }, IncompatibleSource::ExcludeNewer(ts) => match ts { Some(_) => f.write_str("published after the exclude newer time"), None => f.write_str("no publish time"), }, IncompatibleSource::RequiresPython(python, _) => { write!(f, "Python {python}") } }, Self::Unavailable => f.write_str("no available distributions"), } } } #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum PythonRequirementKind { /// The installed version of Python. Installed, /// The target version of Python; that is, the version of Python for which we are resolving /// dependencies. This is typically the same as the installed version, but may be different /// when specifying an alternate Python version for the resolution. Target, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum WheelCompatibility { Incompatible(IncompatibleWheel), Compatible(HashComparison, Option, Option), } #[derive(Debug, PartialEq, Eq, Clone)] pub enum IncompatibleWheel { /// The wheel was published after the exclude newer time. ExcludeNewer(Option), /// The wheel tags do not match those of the target Python platform. Tag(IncompatibleTag), /// The required Python version is not a superset of the target Python version range. RequiresPython(VersionSpecifiers, PythonRequirementKind), /// The wheel was yanked. Yanked(Yanked), /// The use of binary wheels is disabled. NoBinary, /// Wheels are not available for the current platform. MissingPlatform(MarkerTree), } #[derive(Debug, Clone, PartialEq, Eq)] pub enum SourceDistCompatibility { Incompatible(IncompatibleSource), Compatible(HashComparison), } #[derive(Debug, PartialEq, Eq, Clone)] pub enum IncompatibleSource { ExcludeNewer(Option), RequiresPython(VersionSpecifiers, PythonRequirementKind), Yanked(Yanked), NoBuild, } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum HashComparison { /// The hash is present, but does not match the expected value. Mismatched, /// The hash is missing. Missing, /// The hash matches the expected value. Matched, } impl PrioritizedDist { /// Create a new [`PrioritizedDist`] from the given wheel distribution. pub fn from_built( dist: RegistryBuiltWheel, hashes: Vec, compatibility: WheelCompatibility, ) -> Self { Self(Box::new(PrioritizedDistInner { markers: implied_markers(&dist.filename), best_wheel_index: Some(0), wheels: vec![(dist, compatibility)], source: None, hashes, })) } /// Create a new [`PrioritizedDist`] from the given source distribution. pub fn from_source( dist: RegistrySourceDist, hashes: Vec, compatibility: SourceDistCompatibility, ) -> Self { Self(Box::new(PrioritizedDistInner { markers: MarkerTree::TRUE, best_wheel_index: None, wheels: vec![], source: Some((dist, compatibility)), hashes, })) } /// Insert the given built distribution into the [`PrioritizedDist`]. pub fn insert_built( &mut self, dist: RegistryBuiltWheel, hashes: impl IntoIterator, compatibility: WheelCompatibility, ) { // Track the implied markers. if compatibility.is_compatible() { if !self.0.markers.is_true() { self.0.markers.or(implied_markers(&dist.filename)); } } // Track the hashes. if !compatibility.is_excluded() { self.0.hashes.extend(hashes); } // Track the highest-priority wheel. if let Some((.., existing_compatibility)) = self.best_wheel() { if compatibility.is_more_compatible(existing_compatibility) { self.0.best_wheel_index = Some(self.0.wheels.len()); } } else { self.0.best_wheel_index = Some(self.0.wheels.len()); } self.0.wheels.push((dist, compatibility)); } /// Insert the given source distribution into the [`PrioritizedDist`]. pub fn insert_source( &mut self, dist: RegistrySourceDist, hashes: impl IntoIterator, compatibility: SourceDistCompatibility, ) { // Track the implied markers. if compatibility.is_compatible() { self.0.markers = MarkerTree::TRUE; } // Track the hashes. if !compatibility.is_excluded() { self.0.hashes.extend(hashes); } // Track the highest-priority source. if let Some((.., existing_compatibility)) = &self.0.source { if compatibility.is_more_compatible(existing_compatibility) { self.0.source = Some((dist, compatibility)); } } else { self.0.source = Some((dist, compatibility)); } } /// Return the highest-priority distribution for the package version, if any. pub fn get(&self) -> Option> { let best_wheel = self.0.best_wheel_index.map(|i| &self.0.wheels[i]); match (&best_wheel, &self.0.source) { // If both are compatible, break ties based on the hash outcome. For example, prefer a // source distribution with a matching hash over a wheel with a mismatched hash. When // the outcomes are equivalent (e.g., both have a matching hash), prefer the wheel. ( Some((wheel, WheelCompatibility::Compatible(wheel_hash, tag_priority, ..))), Some((sdist, SourceDistCompatibility::Compatible(sdist_hash))), ) => { if sdist_hash > wheel_hash { Some(CompatibleDist::SourceDist { sdist, prioritized: self, }) } else { Some(CompatibleDist::CompatibleWheel { wheel, priority: *tag_priority, prioritized: self, }) } } // Prefer the highest-priority, platform-compatible wheel. (Some((wheel, WheelCompatibility::Compatible(_, tag_priority, ..))), _) => { Some(CompatibleDist::CompatibleWheel { wheel, priority: *tag_priority, prioritized: self, }) } // If we have a compatible source distribution and an incompatible wheel, return the // wheel. We assume that all distributions have the same metadata for a given package // version. If a compatible source distribution exists, we assume we can build it, but // using the wheel is faster. // // (If the incompatible wheel should actually be ignored entirely, fall through to // using the source distribution.) ( Some((wheel, compatibility @ WheelCompatibility::Incompatible(_))), Some((sdist, SourceDistCompatibility::Compatible(_))), ) if !compatibility.is_excluded() => Some(CompatibleDist::IncompatibleWheel { sdist, wheel, prioritized: self, }), // Otherwise, if we have a source distribution, return it. (.., Some((sdist, SourceDistCompatibility::Compatible(_)))) => { Some(CompatibleDist::SourceDist { sdist, prioritized: self, }) } _ => None, } } /// Return the incompatibility for the best source distribution, if any. pub fn incompatible_source(&self) -> Option<&IncompatibleSource> { self.0 .source .as_ref() .and_then(|(_, compatibility)| match compatibility { SourceDistCompatibility::Compatible(_) => None, SourceDistCompatibility::Incompatible(incompatibility) => Some(incompatibility), }) } /// Return the incompatibility for the best wheel, if any. pub fn incompatible_wheel(&self) -> Option<&IncompatibleWheel> { self.0 .best_wheel_index .map(|i| &self.0.wheels[i]) .and_then(|(_, compatibility)| match compatibility { WheelCompatibility::Compatible(_, _, _) => None, WheelCompatibility::Incompatible(incompatibility) => Some(incompatibility), }) } /// Return the hashes for each distribution. pub fn hashes(&self) -> &[HashDigest] { &self.0.hashes } /// Returns true if and only if this distribution does not contain any /// source distributions or wheels. pub fn is_empty(&self) -> bool { self.0.source.is_none() && self.0.wheels.is_empty() } /// If this prioritized dist has at least one wheel, then this creates /// a built distribution with the best wheel in this prioritized dist. pub fn built_dist(&self) -> Option { let best_wheel_index = self.0.best_wheel_index?; // Remove any excluded wheels from the list of wheels, and adjust the wheel index to be // relative to the filtered list. let mut adjusted_wheels = Vec::with_capacity(self.0.wheels.len()); let mut adjusted_best_index = 0; for (i, (wheel, compatibility)) in self.0.wheels.iter().enumerate() { if compatibility.is_excluded() { continue; } if i == best_wheel_index { adjusted_best_index = adjusted_wheels.len(); } adjusted_wheels.push(wheel.clone()); } let sdist = self.0.source.as_ref().map(|(sdist, _)| sdist.clone()); Some(RegistryBuiltDist { wheels: adjusted_wheels, best_wheel_index: adjusted_best_index, sdist, }) } /// If this prioritized dist has an sdist, then this creates a source /// distribution. pub fn source_dist(&self) -> Option { let mut sdist = self .0 .source .as_ref() .filter(|(_, compatibility)| !compatibility.is_excluded()) .map(|(sdist, _)| sdist.clone())?; assert!( sdist.wheels.is_empty(), "source distribution should not have any wheels yet" ); sdist.wheels = self .0 .wheels .iter() .map(|(wheel, _)| wheel.clone()) .collect(); Some(sdist) } /// Returns the "best" wheel in this prioritized distribution, if one /// exists. pub fn best_wheel(&self) -> Option<&(RegistryBuiltWheel, WheelCompatibility)> { self.0.best_wheel_index.map(|i| &self.0.wheels[i]) } /// Returns an iterator of all wheels and the source distribution, if any. pub fn files(&self) -> impl Iterator { self.0 .wheels .iter() .map(|(wheel, _)| wheel.file.as_ref()) .chain( self.0 .source .as_ref() .map(|(source_dist, _)| source_dist.file.as_ref()), ) } /// Returns an iterator over all Python tags for the distribution. pub fn python_tags(&self) -> impl Iterator + '_ { self.0 .wheels .iter() .flat_map(|(wheel, _)| wheel.filename.python_tags().iter().copied()) } /// Returns an iterator over all ABI tags for the distribution. pub fn abi_tags(&self) -> impl Iterator + '_ { self.0 .wheels .iter() .flat_map(|(wheel, _)| wheel.filename.abi_tags().iter().copied()) } /// Returns the set of platform tags for the distribution that are ABI-compatible with the given /// tags. pub fn platform_tags<'a>( &'a self, tags: &'a Tags, ) -> impl Iterator + 'a { self.0.wheels.iter().flat_map(move |(wheel, _)| { if wheel.filename.python_tags().iter().any(|wheel_py| { wheel .filename .abi_tags() .iter() .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi)) }) { wheel.filename.platform_tags().iter() } else { [].iter() } }) } } impl<'a> CompatibleDist<'a> { /// Return the [`ResolvedDistRef`] to use during resolution. pub fn for_resolution(&self) -> ResolvedDistRef<'a> { match self { Self::InstalledDist(dist) => ResolvedDistRef::Installed { dist }, Self::SourceDist { sdist, prioritized } => { ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized } } Self::CompatibleWheel { wheel, prioritized, .. } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized }, Self::IncompatibleWheel { wheel, prioritized, .. } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized }, } } /// Return the [`ResolvedDistRef`] to use during installation. pub fn for_installation(&self) -> ResolvedDistRef<'a> { match self { Self::InstalledDist(dist) => ResolvedDistRef::Installed { dist }, Self::SourceDist { sdist, prioritized } => { ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized } } Self::CompatibleWheel { wheel, prioritized, .. } => ResolvedDistRef::InstallableRegistryBuiltDist { wheel, prioritized }, Self::IncompatibleWheel { sdist, prioritized, .. } => ResolvedDistRef::InstallableRegistrySourceDist { sdist, prioritized }, } } /// Returns a [`RegistryBuiltWheel`] if the distribution includes a compatible or incompatible /// wheel. pub fn wheel(&self) -> Option<&RegistryBuiltWheel> { match self { Self::InstalledDist(_) => None, Self::SourceDist { .. } => None, Self::CompatibleWheel { wheel, .. } => Some(wheel), Self::IncompatibleWheel { wheel, .. } => Some(wheel), } } } impl WheelCompatibility { /// Return `true` if the distribution is compatible. pub fn is_compatible(&self) -> bool { matches!(self, Self::Compatible(_, _, _)) } /// Return `true` if the distribution is excluded. pub fn is_excluded(&self) -> bool { matches!(self, Self::Incompatible(IncompatibleWheel::ExcludeNewer(_))) } /// Return `true` if the current compatibility is more compatible than another. /// /// Compatible wheels are always higher more compatible than incompatible wheels. /// Compatible wheel ordering is determined by tag priority. pub fn is_more_compatible(&self, other: &Self) -> bool { match (self, other) { (Self::Compatible(_, _, _), Self::Incompatible(_)) => true, ( Self::Compatible(hash, tag_priority, build_tag), Self::Compatible(other_hash, other_tag_priority, other_build_tag), ) => { (hash, tag_priority, build_tag) > (other_hash, other_tag_priority, other_build_tag) } (Self::Incompatible(_), Self::Compatible(_, _, _)) => false, (Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => { incompatibility.is_more_compatible(other_incompatibility) } } } } impl SourceDistCompatibility { /// Return `true` if the distribution is compatible. pub fn is_compatible(&self) -> bool { matches!(self, Self::Compatible(_)) } /// Return `true` if the distribution is excluded. pub fn is_excluded(&self) -> bool { matches!( self, Self::Incompatible(IncompatibleSource::ExcludeNewer(_)) ) } /// Return the higher priority compatibility. /// /// Compatible source distributions are always higher priority than incompatible source distributions. /// Compatible source distribution priority is arbitrary. /// Incompatible source distribution priority selects a source distribution that was "closest" to being usable. pub fn is_more_compatible(&self, other: &Self) -> bool { match (self, other) { (Self::Compatible(_), Self::Incompatible(_)) => true, (Self::Compatible(compatibility), Self::Compatible(other_compatibility)) => { compatibility > other_compatibility } (Self::Incompatible(_), Self::Compatible(_)) => false, (Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => { incompatibility.is_more_compatible(other_incompatibility) } } } } impl IncompatibleSource { fn is_more_compatible(&self, other: &Self) -> bool { match self { Self::ExcludeNewer(timestamp_self) => match other { // Smaller timestamps are closer to the cut-off time Self::ExcludeNewer(timestamp_other) => timestamp_other < timestamp_self, Self::NoBuild | Self::RequiresPython(_, _) | Self::Yanked(_) => true, }, Self::RequiresPython(_, _) => match other { Self::ExcludeNewer(_) => false, // Version specifiers cannot be reasonably compared Self::RequiresPython(_, _) => false, Self::NoBuild | Self::Yanked(_) => true, }, Self::Yanked(_) => match other { Self::ExcludeNewer(_) | Self::RequiresPython(_, _) => false, // Yanks with a reason are more helpful for errors Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)), Self::NoBuild => true, }, Self::NoBuild => false, } } } impl IncompatibleWheel { #[allow(clippy::match_like_matches_macro)] fn is_more_compatible(&self, other: &Self) -> bool { match self { Self::ExcludeNewer(timestamp_self) => match other { // Smaller timestamps are closer to the cut-off time Self::ExcludeNewer(timestamp_other) => match (timestamp_self, timestamp_other) { (None, _) => true, (_, None) => false, (Some(timestamp_self), Some(timestamp_other)) => { timestamp_other < timestamp_self } }, Self::MissingPlatform(_) | Self::NoBinary | Self::RequiresPython(_, _) | Self::Tag(_) | Self::Yanked(_) => true, }, Self::Tag(tag_self) => match other { Self::ExcludeNewer(_) => false, Self::Tag(tag_other) => tag_self > tag_other, Self::MissingPlatform(_) | Self::NoBinary | Self::RequiresPython(_, _) | Self::Yanked(_) => true, }, Self::RequiresPython(_, _) => match other { Self::ExcludeNewer(_) | Self::Tag(_) => false, // Version specifiers cannot be reasonably compared Self::RequiresPython(_, _) => false, Self::MissingPlatform(_) | Self::NoBinary | Self::Yanked(_) => true, }, Self::Yanked(_) => match other { Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_, _) => false, // Yanks with a reason are more helpful for errors Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)), Self::MissingPlatform(_) | Self::NoBinary => true, }, Self::NoBinary => match other { Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_, _) | Self::Yanked(_) => false, Self::NoBinary => false, Self::MissingPlatform(_) => true, }, Self::MissingPlatform(_) => false, } } } /// Given a wheel filename, determine the set of supported markers. pub fn implied_markers(filename: &WheelFilename) -> MarkerTree { let mut marker = implied_platform_markers(filename); marker.and(implied_python_markers(filename)); marker } /// Given a wheel filename, determine the set of supported platforms, in terms of their markers. /// /// This is roughly the inverse of platform tag generation: given a tag, we want to infer the /// supported platforms (rather than generating the supported tags from a given platform). fn implied_platform_markers(filename: &WheelFilename) -> MarkerTree { let mut marker = MarkerTree::FALSE; for platform_tag in filename.platform_tags() { match platform_tag { PlatformTag::Any => { return MarkerTree::TRUE; } // Windows PlatformTag::Win32 => { let mut tag_marker = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, value: arcstr::literal!("win32"), }); tag_marker.and(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformMachine, operator: MarkerOperator::Equal, value: arcstr::literal!("x86"), })); marker.or(tag_marker); } PlatformTag::WinAmd64 => { let mut tag_marker = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, value: arcstr::literal!("win32"), }); tag_marker.and(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformMachine, operator: MarkerOperator::Equal, value: arcstr::literal!("AMD64"), })); marker.or(tag_marker); } PlatformTag::WinArm64 => { let mut tag_marker = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, value: arcstr::literal!("win32"), }); tag_marker.and(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformMachine, operator: MarkerOperator::Equal, value: arcstr::literal!("ARM64"), })); marker.or(tag_marker); } // macOS PlatformTag::Macos { binary_format, .. } => { let mut tag_marker = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, value: arcstr::literal!("darwin"), }); // Extract the architecture from the end of the tag. let mut arch_marker = MarkerTree::FALSE; for arch in binary_format.platform_machine() { arch_marker.or(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformMachine, operator: MarkerOperator::Equal, value: ArcStr::from(arch.name()), })); } tag_marker.and(arch_marker); marker.or(tag_marker); } // Linux PlatformTag::Manylinux { arch, .. } | PlatformTag::Manylinux1 { arch, .. } | PlatformTag::Manylinux2010 { arch, .. } | PlatformTag::Manylinux2014 { arch, .. } | PlatformTag::Musllinux { arch, .. } | PlatformTag::Linux { arch } => { let mut tag_marker = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, operator: MarkerOperator::Equal, value: arcstr::literal!("linux"), }); tag_marker.and(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformMachine, operator: MarkerOperator::Equal, value: ArcStr::from(arch.name()), })); marker.or(tag_marker); } tag => { debug!("Unknown platform tag in wheel tag: {tag}"); } } } marker } /// Given a wheel filename, determine the set of supported Python versions, in terms of their markers. /// /// This is roughly the inverse of Python tag generation: given a tag, we want to infer the /// supported Python version (rather than generating the supported tags from a given Python version). fn implied_python_markers(filename: &WheelFilename) -> MarkerTree { let mut marker = MarkerTree::FALSE; for python_tag in filename.python_tags() { // First, construct the version marker based on the tag let mut tree = match python_tag { LanguageTag::None => { // No Python tag means no Python version requirement. return MarkerTree::TRUE; } LanguageTag::Python { major, minor: None } => { MarkerTree::expression(MarkerExpression::Version { key: uv_pep508::MarkerValueVersion::PythonVersion, specifier: VersionSpecifier::equals_star_version(Version::new([u64::from( *major, )])), }) } LanguageTag::Python { major, minor: Some(minor), } | LanguageTag::CPython { python_version: (major, minor), } | LanguageTag::PyPy { python_version: (major, minor), } | LanguageTag::GraalPy { python_version: (major, minor), } | LanguageTag::Pyston { python_version: (major, minor), } => MarkerTree::expression(MarkerExpression::Version { key: uv_pep508::MarkerValueVersion::PythonVersion, specifier: VersionSpecifier::equals_star_version(Version::new([ u64::from(*major), u64::from(*minor), ])), }), }; // Then, add implementation markers for implementation-specific tags match python_tag { LanguageTag::None | LanguageTag::Python { .. } => { // No implementation marker needed } LanguageTag::CPython { .. } => { tree.and(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformPythonImplementation, operator: MarkerOperator::Equal, value: arcstr::literal!("CPython"), })); } LanguageTag::PyPy { .. } => { tree.and(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformPythonImplementation, operator: MarkerOperator::Equal, value: arcstr::literal!("PyPy"), })); } LanguageTag::GraalPy { .. } => { tree.and(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformPythonImplementation, operator: MarkerOperator::Equal, value: arcstr::literal!("GraalPy"), })); } LanguageTag::Pyston { .. } => { tree.and(MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::PlatformPythonImplementation, operator: MarkerOperator::Equal, value: arcstr::literal!("Pyston"), })); } } marker.or(tree); } marker } #[cfg(test)] mod tests { use std::str::FromStr; use super::*; #[track_caller] fn assert_platform_markers(filename: &str, expected: &str) { let filename = WheelFilename::from_str(filename).unwrap(); assert_eq!( implied_platform_markers(&filename), expected.parse::().unwrap() ); } #[track_caller] fn assert_python_markers(filename: &str, expected: &str) { let filename = WheelFilename::from_str(filename).unwrap(); assert_eq!( implied_python_markers(&filename), expected.parse::().unwrap() ); } #[track_caller] fn assert_implied_markers(filename: &str, expected: &str) { let filename = WheelFilename::from_str(filename).unwrap(); assert_eq!( implied_markers(&filename), expected.parse::().unwrap() ); } #[test] fn test_implied_platform_markers() { let filename = WheelFilename::from_str("example-1.0-py3-none-any.whl").unwrap(); assert_eq!(implied_platform_markers(&filename), MarkerTree::TRUE); assert_platform_markers( "example-1.0-cp310-cp310-win32.whl", "sys_platform == 'win32' and platform_machine == 'x86'", ); assert_platform_markers( "numpy-2.2.1-cp313-cp313t-win_amd64.whl", "sys_platform == 'win32' and platform_machine == 'AMD64'", ); assert_platform_markers( "numpy-2.2.1-cp313-cp313t-win_arm64.whl", "sys_platform == 'win32' and platform_machine == 'ARM64'", ); assert_platform_markers( "numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", "sys_platform == 'linux' and platform_machine == 'aarch64'", ); assert_platform_markers( "numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", "sys_platform == 'linux' and platform_machine == 'x86_64'", ); assert_platform_markers( "numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", "sys_platform == 'linux' and platform_machine == 'aarch64'", ); assert_platform_markers( "numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl", "sys_platform == 'darwin' and platform_machine == 'x86_64'", ); assert_platform_markers( "numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", "sys_platform == 'darwin' and platform_machine == 'x86_64'", ); assert_platform_markers( "numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", "sys_platform == 'darwin' and platform_machine == 'arm64'", ); } #[test] fn test_implied_python_markers() { let filename = WheelFilename::from_str("example-1.0-none-none-any.whl").unwrap(); assert_eq!(implied_python_markers(&filename), MarkerTree::TRUE); assert_python_markers( "example-1.0-cp310-cp310-any.whl", "python_full_version == '3.10.*' and platform_python_implementation == 'CPython'", ); assert_python_markers( "example-1.0-cp311-cp311-any.whl", "python_full_version == '3.11.*' and platform_python_implementation == 'CPython'", ); assert_python_markers( "example-1.0-cp312-cp312-any.whl", "python_full_version == '3.12.*' and platform_python_implementation == 'CPython'", ); assert_python_markers( "example-1.0-cp313-cp313-any.whl", "python_full_version == '3.13.*' and platform_python_implementation == 'CPython'", ); assert_python_markers( "example-1.0-cp313-cp313t-any.whl", "python_full_version == '3.13.*' and platform_python_implementation == 'CPython'", ); assert_python_markers( "example-1.0-pp310-pypy310_pp73-any.whl", "python_full_version == '3.10.*' and platform_python_implementation == 'PyPy'", ); assert_python_markers( "example-1.0-py310-none-any.whl", "python_full_version >= '3.10' and python_full_version < '3.11'", ); assert_python_markers( "example-1.0-py3-none-any.whl", "python_full_version >= '3' and python_full_version < '4'", ); assert_python_markers( "example-1.0-py311.py312-none-any.whl", "python_full_version >= '3.11' and python_full_version < '3.13'", ); } #[test] fn test_implied_markers() { assert_implied_markers( "numpy-1.0-cp310-cp310-win32.whl", "python_full_version == '3.10.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'x86'", ); assert_implied_markers( "pywin32-311-cp314-cp314-win_arm64.whl", "python_full_version == '3.14.*' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and platform_machine == 'ARM64'", ); assert_implied_markers( "numpy-1.0-cp311-cp311-macosx_10_9_x86_64.whl", "python_full_version == '3.11.*' and platform_python_implementation == 'CPython' and sys_platform == 'darwin' and platform_machine == 'x86_64'", ); assert_implied_markers( "numpy-1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", "python_full_version == '3.12.*' and platform_python_implementation == 'CPython' and sys_platform == 'linux' and platform_machine == 'aarch64'", ); assert_implied_markers( "example-1.0-py3-none-any.whl", "python_full_version >= '3' and python_full_version < '4'", ); } } uv-0.9.17+ds1/crates/uv-distribution-types/src/requested.rs000066400000000000000000000036451520155276700236770ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use crate::{ Dist, DistributionId, DistributionMetadata, Identifier, InstalledDist, Name, ResourceId, VersionOrUrlRef, }; use uv_normalize::PackageName; use uv_pep440::Version; /// A distribution that can be requested during resolution. /// /// Either an already-installed distribution or a distribution that can be installed. #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub enum RequestedDist { Installed(InstalledDist), Installable(Dist), } impl RequestedDist { /// Returns the version of the distribution, if it is known. pub fn version(&self) -> Option<&Version> { match self { Self::Installed(dist) => Some(dist.version()), Self::Installable(dist) => dist.version(), } } } impl Name for RequestedDist { fn name(&self) -> &PackageName { match self { Self::Installable(dist) => dist.name(), Self::Installed(dist) => dist.name(), } } } impl DistributionMetadata for RequestedDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { match self { Self::Installed(dist) => dist.version_or_url(), Self::Installable(dist) => dist.version_or_url(), } } } impl Identifier for RequestedDist { fn distribution_id(&self) -> DistributionId { match self { Self::Installed(dist) => dist.distribution_id(), Self::Installable(dist) => dist.distribution_id(), } } fn resource_id(&self) -> ResourceId { match self { Self::Installed(dist) => dist.resource_id(), Self::Installable(dist) => dist.resource_id(), } } } impl Display for RequestedDist { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Installed(dist) => dist.fmt(f), Self::Installable(dist) => dist.fmt(f), } } } uv-0.9.17+ds1/crates/uv-distribution-types/src/requirement.rs000066400000000000000000001170431520155276700242340ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use std::io; use std::path::Path; use std::str::FromStr; use thiserror::Error; use uv_cache_key::{CacheKey, CacheKeyHasher}; use uv_distribution_filename::DistExtension; use uv_fs::{CWD, PortablePath, PortablePathBuf, relative_to}; use uv_git_types::{GitLfs, GitOid, GitReference, GitUrl, GitUrlParseError, OidParseError}; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep440::VersionSpecifiers; use uv_pep508::{ MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl, marker, }; use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError}; use crate::{IndexMetadata, IndexUrl}; use uv_pypi_types::{ ConflictItem, Hashes, ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, ParsedUrlError, VerbatimParsedUrl, }; #[derive(Debug, Error)] pub enum RequirementError { #[error(transparent)] VerbatimUrlError(#[from] uv_pep508::VerbatimUrlError), #[error(transparent)] ParsedUrlError(#[from] ParsedUrlError), #[error(transparent)] UrlParseError(#[from] DisplaySafeUrlError), #[error(transparent)] OidParseError(#[from] OidParseError), #[error(transparent)] GitUrlParse(#[from] GitUrlParseError), } /// A representation of dependency on a package, an extension over a PEP 508's requirement. /// /// The main change is using [`RequirementSource`] to represent all supported package sources over /// [`VersionOrUrl`], which collapses all URL sources into a single stringly type. /// /// Additionally, this requirement type makes room for dependency groups, which lack a standardized /// representation in PEP 508. In the context of this type, extras and groups are assumed to be /// mutually exclusive, in that if `extras` is non-empty, `groups` must be empty and vice versa. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Requirement { pub name: PackageName, #[serde(skip_serializing_if = "<[ExtraName]>::is_empty", default)] pub extras: Box<[ExtraName]>, #[serde(skip_serializing_if = "<[GroupName]>::is_empty", default)] pub groups: Box<[GroupName]>, #[serde( skip_serializing_if = "marker::ser::is_empty", serialize_with = "marker::ser::serialize", default )] pub marker: MarkerTree, #[serde(flatten)] pub source: RequirementSource, #[serde(skip)] pub origin: Option, } impl Requirement { /// Returns whether the markers apply for the given environment. /// /// When `env` is `None`, this specifically evaluates all marker /// expressions based on the environment to `true`. That is, this provides /// environment independent marker evaluation. pub fn evaluate_markers(&self, env: Option<&MarkerEnvironment>, extras: &[ExtraName]) -> bool { self.marker.evaluate_optional_environment(env, extras) } /// Returns `true` if the requirement is editable. pub fn is_editable(&self) -> bool { self.source.is_editable() } /// Convert to a [`Requirement`] with a relative path based on the given root. pub fn relative_to(self, path: &Path) -> Result { Ok(Self { source: self.source.relative_to(path)?, ..self }) } /// Convert to a [`Requirement`] with an absolute path based on the given root. #[must_use] pub fn to_absolute(self, path: &Path) -> Self { Self { source: self.source.to_absolute(path), ..self } } /// Return the hashes of the requirement, as specified in the URL fragment. pub fn hashes(&self) -> Option { let RequirementSource::Url { ref url, .. } = self.source else { return None; }; let fragment = url.fragment()?; Hashes::parse_fragment(fragment).ok() } /// Set the source file containing the requirement. #[must_use] pub fn with_origin(self, origin: RequirementOrigin) -> Self { Self { origin: Some(origin), ..self } } } impl std::hash::Hash for Requirement { fn hash(&self, state: &mut H) { let Self { name, extras, groups, marker, source, origin: _, } = self; name.hash(state); extras.hash(state); groups.hash(state); marker.hash(state); source.hash(state); } } impl PartialEq for Requirement { fn eq(&self, other: &Self) -> bool { let Self { name, extras, groups, marker, source, origin: _, } = self; let Self { name: other_name, extras: other_extras, groups: other_groups, marker: other_marker, source: other_source, origin: _, } = other; name == other_name && extras == other_extras && groups == other_groups && marker == other_marker && source == other_source } } impl Eq for Requirement {} impl Ord for Requirement { fn cmp(&self, other: &Self) -> std::cmp::Ordering { let Self { name, extras, groups, marker, source, origin: _, } = self; let Self { name: other_name, extras: other_extras, groups: other_groups, marker: other_marker, source: other_source, origin: _, } = other; name.cmp(other_name) .then_with(|| extras.cmp(other_extras)) .then_with(|| groups.cmp(other_groups)) .then_with(|| marker.cmp(other_marker)) .then_with(|| source.cmp(other_source)) } } impl PartialOrd for Requirement { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl From for uv_pep508::Requirement { /// Convert a [`Requirement`] to a [`uv_pep508::Requirement`]. fn from(requirement: Requirement) -> Self { Self { name: requirement.name, extras: requirement.extras, marker: requirement.marker, origin: requirement.origin, version_or_url: match requirement.source { RequirementSource::Registry { specifier, .. } => { Some(VersionOrUrl::VersionSpecifier(specifier)) } RequirementSource::Url { url, .. } | RequirementSource::Git { url, .. } | RequirementSource::Path { url, .. } | RequirementSource::Directory { url, .. } => Some(VersionOrUrl::Url(url)), }, } } } impl From for uv_pep508::Requirement { /// Convert a [`Requirement`] to a [`uv_pep508::Requirement`]. fn from(requirement: Requirement) -> Self { Self { name: requirement.name, extras: requirement.extras, marker: requirement.marker, origin: requirement.origin, version_or_url: match requirement.source { RequirementSource::Registry { specifier, .. } => { Some(VersionOrUrl::VersionSpecifier(specifier)) } RequirementSource::Url { location, subdirectory, ext, url, } => Some(VersionOrUrl::Url(VerbatimParsedUrl { parsed_url: ParsedUrl::Archive(ParsedArchiveUrl { url: location, subdirectory, ext, }), verbatim: url, })), RequirementSource::Git { git, subdirectory, url, } => Some(VersionOrUrl::Url(VerbatimParsedUrl { parsed_url: ParsedUrl::Git(ParsedGitUrl { url: git, subdirectory, }), verbatim: url, })), RequirementSource::Path { install_path, ext, url, } => Some(VersionOrUrl::Url(VerbatimParsedUrl { parsed_url: ParsedUrl::Path(ParsedPathUrl { url: url.to_url(), install_path, ext, }), verbatim: url, })), RequirementSource::Directory { install_path, editable, r#virtual, url, } => Some(VersionOrUrl::Url(VerbatimParsedUrl { parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl { url: url.to_url(), install_path, editable, r#virtual, }), verbatim: url, })), }, } } } impl From> for Requirement { /// Convert a [`uv_pep508::Requirement`] to a [`Requirement`]. fn from(requirement: uv_pep508::Requirement) -> Self { let source = match requirement.version_or_url { None => RequirementSource::Registry { specifier: VersionSpecifiers::empty(), index: None, conflict: None, }, // The most popular case: just a name, a version range and maybe extras. Some(VersionOrUrl::VersionSpecifier(specifier)) => RequirementSource::Registry { specifier, index: None, conflict: None, }, Some(VersionOrUrl::Url(url)) => { RequirementSource::from_parsed_url(url.parsed_url, url.verbatim) } }; Self { name: requirement.name, groups: Box::new([]), extras: requirement.extras, marker: requirement.marker, source, origin: requirement.origin, } } } impl Display for Requirement { /// Display the [`Requirement`], with the intention of being shown directly to a user, rather /// than for inclusion in a `requirements.txt` file. fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name)?; if !self.extras.is_empty() { write!( f, "[{}]", self.extras .iter() .map(ToString::to_string) .collect::>() .join(",") )?; } match &self.source { RequirementSource::Registry { specifier, index, .. } => { write!(f, "{specifier}")?; if let Some(index) = index { write!(f, " (index: {})", index.url)?; } } RequirementSource::Url { url, .. } => { write!(f, " @ {url}")?; } RequirementSource::Git { url: _, git, subdirectory, } => { write!(f, " @ git+{}", git.repository())?; if let Some(reference) = git.reference().as_str() { write!(f, "@{reference}")?; } if let Some(subdirectory) = subdirectory { writeln!(f, "#subdirectory={}", subdirectory.display())?; } if git.lfs().enabled() { writeln!( f, "{}lfs=true", if subdirectory.is_some() { "&" } else { "#" } )?; } } RequirementSource::Path { url, .. } => { write!(f, " @ {url}")?; } RequirementSource::Directory { url, .. } => { write!(f, " @ {url}")?; } } if let Some(marker) = self.marker.contents() { write!(f, " ; {marker}")?; } Ok(()) } } impl CacheKey for Requirement { fn cache_key(&self, state: &mut CacheKeyHasher) { self.name.as_str().cache_key(state); self.groups.len().cache_key(state); for group in &self.groups { group.as_str().cache_key(state); } self.extras.len().cache_key(state); for extra in &self.extras { extra.as_str().cache_key(state); } if let Some(marker) = self.marker.contents() { 1u8.cache_key(state); marker.to_string().cache_key(state); } else { 0u8.cache_key(state); } match &self.source { RequirementSource::Registry { specifier, index, conflict: _, } => { 0u8.cache_key(state); specifier.len().cache_key(state); for spec in specifier.iter() { spec.operator().as_str().cache_key(state); spec.version().cache_key(state); } if let Some(index) = index { 1u8.cache_key(state); index.url.cache_key(state); } else { 0u8.cache_key(state); } // `conflict` is intentionally omitted } RequirementSource::Url { location, subdirectory, ext, url, } => { 1u8.cache_key(state); location.cache_key(state); if let Some(subdirectory) = subdirectory { 1u8.cache_key(state); subdirectory.display().to_string().cache_key(state); } else { 0u8.cache_key(state); } ext.name().cache_key(state); url.cache_key(state); } RequirementSource::Git { git, subdirectory, url, } => { 2u8.cache_key(state); git.to_string().cache_key(state); if let Some(subdirectory) = subdirectory { 1u8.cache_key(state); subdirectory.display().to_string().cache_key(state); } else { 0u8.cache_key(state); } if git.lfs().enabled() { 1u8.cache_key(state); } url.cache_key(state); } RequirementSource::Path { install_path, ext, url, } => { 3u8.cache_key(state); install_path.cache_key(state); ext.name().cache_key(state); url.cache_key(state); } RequirementSource::Directory { install_path, editable, r#virtual, url, } => { 4u8.cache_key(state); install_path.cache_key(state); editable.cache_key(state); r#virtual.cache_key(state); url.cache_key(state); } } // `origin` is intentionally omitted } } /// The different locations with can install a distribution from: Version specifier (from an index), /// HTTP(S) URL, git repository, and path. /// /// We store both the parsed fields (such as the plain url and the subdirectory) and the joined /// PEP 508 style url (e.g. `file:///#subdirectory=`) since we need both in /// different locations. #[derive( Hash, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize, )] #[serde(try_from = "RequirementSourceWire", into = "RequirementSourceWire")] pub enum RequirementSource { /// The requirement has a version specifier, such as `foo >1,<2`. Registry { specifier: VersionSpecifiers, /// Choose a version from the index at the given URL. index: Option, /// The conflict item associated with the source, if any. conflict: Option, }, // TODO(konsti): Track and verify version specifier from `project.dependencies` matches the // version in remote location. /// A remote `http://` or `https://` URL, either a built distribution, /// e.g. `foo @ https://example.org/foo-1.0-py3-none-any.whl`, or a source distribution, /// e.g.`foo @ https://example.org/foo-1.0.zip`. Url { /// The remote location of the archive file, without subdirectory fragment. location: DisplaySafeUrl, /// For source distributions, the path to the distribution if it is not in the archive /// root. subdirectory: Option>, /// The file extension, e.g. `tar.gz`, `zip`, etc. ext: DistExtension, /// The PEP 508 style URL in the format /// `:///#subdirectory=`. url: VerbatimUrl, }, /// A remote Git repository, over either HTTPS or SSH. Git { /// The repository URL and reference to the commit to use. git: GitUrl, /// The path to the source distribution if it is not in the repository root. subdirectory: Option>, /// The PEP 508 style url in the format /// `git+:///@#subdirectory=`. url: VerbatimUrl, }, /// A local built or source distribution, either from a path or a `file://` URL. It can either /// be a binary distribution (a `.whl` file) or a source distribution archive (a `.zip` or /// `.tar.gz` file). Path { /// The absolute path to the distribution which we use for installing. install_path: Box, /// The file extension, e.g. `tar.gz`, `zip`, etc. ext: DistExtension, /// The PEP 508 style URL in the format /// `file:///#subdirectory=`. url: VerbatimUrl, }, /// A local source tree (a directory with a pyproject.toml in, or a legacy /// source distribution with only a setup.py but non pyproject.toml in it). Directory { /// The absolute path to the distribution which we use for installing. install_path: Box, /// For a source tree (a directory), whether to install as an editable. editable: Option, /// For a source tree (a directory), whether the project should be built and installed. r#virtual: Option, /// The PEP 508 style URL in the format /// `file:///#subdirectory=`. url: VerbatimUrl, }, } impl RequirementSource { /// Construct a [`RequirementSource`] for a URL source, given a URL parsed into components and /// the PEP 508 string (after the `@`) as [`VerbatimUrl`]. pub fn from_parsed_url(parsed_url: ParsedUrl, url: VerbatimUrl) -> Self { match parsed_url { ParsedUrl::Path(local_file) => Self::Path { install_path: local_file.install_path.clone(), ext: local_file.ext, url, }, ParsedUrl::Directory(directory) => Self::Directory { install_path: directory.install_path.clone(), editable: directory.editable, r#virtual: directory.r#virtual, url, }, ParsedUrl::Git(git) => Self::Git { git: git.url.clone(), url, subdirectory: git.subdirectory, }, ParsedUrl::Archive(archive) => Self::Url { url, location: archive.url, subdirectory: archive.subdirectory, ext: archive.ext, }, } } /// Convert the source to a [`VerbatimParsedUrl`], if it's a URL source. pub fn to_verbatim_parsed_url(&self) -> Option { match self { Self::Registry { .. } => None, Self::Url { location, subdirectory, ext, url, } => Some(VerbatimParsedUrl { parsed_url: ParsedUrl::Archive(ParsedArchiveUrl::from_source( location.clone(), subdirectory.clone(), *ext, )), verbatim: url.clone(), }), Self::Path { install_path, ext, url, } => Some(VerbatimParsedUrl { parsed_url: ParsedUrl::Path(ParsedPathUrl::from_source( install_path.clone(), *ext, url.to_url(), )), verbatim: url.clone(), }), Self::Directory { install_path, editable, r#virtual, url, } => Some(VerbatimParsedUrl { parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl::from_source( install_path.clone(), *editable, *r#virtual, url.to_url(), )), verbatim: url.clone(), }), Self::Git { git, subdirectory, url, } => Some(VerbatimParsedUrl { parsed_url: ParsedUrl::Git(ParsedGitUrl::from_source( git.clone(), subdirectory.clone(), )), verbatim: url.clone(), }), } } /// Convert the source to a version specifier or URL. /// /// If the source is a registry and the specifier is empty, it returns `None`. pub fn version_or_url(&self) -> Option> { match self { Self::Registry { specifier, .. } => { if specifier.is_empty() { None } else { Some(VersionOrUrl::VersionSpecifier(specifier.clone())) } } Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => { Some(VersionOrUrl::Url(self.to_verbatim_parsed_url()?)) } } } /// Returns `true` if the source is editable. pub fn is_editable(&self) -> bool { matches!( self, Self::Directory { editable: Some(true), .. } ) } /// Returns `true` if the source is empty. pub fn is_empty(&self) -> bool { match self { Self::Registry { specifier, .. } => specifier.is_empty(), Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => { false } } } /// If the source is the registry, return the version specifiers pub fn version_specifiers(&self) -> Option<&VersionSpecifiers> { match self { Self::Registry { specifier, .. } => Some(specifier), Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => { None } } } /// Convert the source to a [`RequirementSource`] relative to the given path. pub fn relative_to(self, path: &Path) -> Result { match self { Self::Registry { .. } | Self::Url { .. } | Self::Git { .. } => Ok(self), Self::Path { install_path, ext, url, } => Ok(Self::Path { install_path: relative_to(&install_path, path) .or_else(|_| std::path::absolute(install_path))? .into_boxed_path(), ext, url, }), Self::Directory { install_path, editable, r#virtual, url, .. } => Ok(Self::Directory { install_path: relative_to(&install_path, path) .or_else(|_| std::path::absolute(install_path))? .into_boxed_path(), editable, r#virtual, url, }), } } /// Convert the source to a [`RequirementSource`] with an absolute path based on the given root. #[must_use] pub fn to_absolute(self, root: &Path) -> Self { match self { Self::Registry { .. } | Self::Url { .. } | Self::Git { .. } => self, Self::Path { install_path, ext, url, } => Self::Path { install_path: uv_fs::normalize_path_buf(root.join(install_path)).into_boxed_path(), ext, url, }, Self::Directory { install_path, editable, r#virtual, url, .. } => Self::Directory { install_path: uv_fs::normalize_path_buf(root.join(install_path)).into_boxed_path(), editable, r#virtual, url, }, } } } impl Display for RequirementSource { /// Display the [`RequirementSource`], with the intention of being shown directly to a user, /// rather than for inclusion in a `requirements.txt` file. fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Registry { specifier, index, .. } => { write!(f, "{specifier}")?; if let Some(index) = index { write!(f, " (index: {})", index.url)?; } } Self::Url { url, .. } => { write!(f, " {url}")?; } Self::Git { url: _, git, subdirectory, } => { write!(f, " git+{}", git.repository())?; if let Some(reference) = git.reference().as_str() { write!(f, "@{reference}")?; } if let Some(subdirectory) = subdirectory { writeln!(f, "#subdirectory={}", subdirectory.display())?; } if git.lfs().enabled() { writeln!( f, "{}lfs=true", if subdirectory.is_some() { "&" } else { "#" } )?; } } Self::Path { url, .. } => { write!(f, "{url}")?; } Self::Directory { url, .. } => { write!(f, "{url}")?; } } Ok(()) } } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[serde(untagged)] enum RequirementSourceWire { /// Ex) `source = { git = "" }` Git { git: String }, /// Ex) `source = { url = "" }` Direct { url: DisplaySafeUrl, subdirectory: Option, }, /// Ex) `source = { path = "/home/ferris/iniconfig-2.0.0-py3-none-any.whl" }` Path { path: PortablePathBuf }, /// Ex) `source = { directory = "/home/ferris/iniconfig" }` Directory { directory: PortablePathBuf }, /// Ex) `source = { editable = "/home/ferris/iniconfig" }` Editable { editable: PortablePathBuf }, /// Ex) `source = { editable = "/home/ferris/iniconfig" }` Virtual { r#virtual: PortablePathBuf }, /// Ex) `source = { specifier = "foo >1,<2" }` Registry { #[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)] specifier: VersionSpecifiers, index: Option, conflict: Option, }, } impl From for RequirementSourceWire { fn from(value: RequirementSource) -> Self { match value { RequirementSource::Registry { specifier, index, conflict, } => { let index = index.map(|index| index.url.into_url()).map(|mut index| { index.remove_credentials(); index }); Self::Registry { specifier, index, conflict, } } RequirementSource::Url { subdirectory, location, ext: _, url: _, } => Self::Direct { url: location, subdirectory: subdirectory.map(PortablePathBuf::from), }, RequirementSource::Git { git, subdirectory, url: _, } => { let mut url = git.repository().clone(); // Remove the credentials. url.remove_credentials(); // Clear out any existing state. url.set_fragment(None); url.set_query(None); // Put the subdirectory in the query. if let Some(subdirectory) = subdirectory .as_deref() .map(PortablePath::from) .as_ref() .map(PortablePath::to_string) { url.query_pairs_mut() .append_pair("subdirectory", &subdirectory); } // Persist lfs=true in the distribution metadata only when explicitly enabled. if git.lfs().enabled() { url.query_pairs_mut().append_pair("lfs", "true"); } // Put the requested reference in the query. match git.reference() { GitReference::Branch(branch) => { url.query_pairs_mut().append_pair("branch", branch.as_str()); } GitReference::Tag(tag) => { url.query_pairs_mut().append_pair("tag", tag.as_str()); } GitReference::BranchOrTag(rev) | GitReference::BranchOrTagOrCommit(rev) | GitReference::NamedRef(rev) => { url.query_pairs_mut().append_pair("rev", rev.as_str()); } GitReference::DefaultBranch => {} } // Put the precise commit in the fragment. if let Some(precise) = git.precise() { url.set_fragment(Some(&precise.to_string())); } Self::Git { git: url.to_string(), } } RequirementSource::Path { install_path, ext: _, url: _, } => Self::Path { path: PortablePathBuf::from(install_path), }, RequirementSource::Directory { install_path, editable, r#virtual, url: _, } => { if editable.unwrap_or(false) { Self::Editable { editable: PortablePathBuf::from(install_path), } } else if r#virtual.unwrap_or(false) { Self::Virtual { r#virtual: PortablePathBuf::from(install_path), } } else { Self::Directory { directory: PortablePathBuf::from(install_path), } } } } } } impl TryFrom for RequirementSource { type Error = RequirementError; fn try_from(wire: RequirementSourceWire) -> Result { match wire { RequirementSourceWire::Registry { specifier, index, conflict, } => Ok(Self::Registry { specifier, index: index .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index)))), conflict, }), RequirementSourceWire::Git { git } => { let mut repository = DisplaySafeUrl::parse(&git)?; let mut reference = GitReference::DefaultBranch; let mut subdirectory: Option = None; let mut lfs = GitLfs::Disabled; for (key, val) in repository.query_pairs() { match &*key { "tag" => reference = GitReference::Tag(val.into_owned()), "branch" => reference = GitReference::Branch(val.into_owned()), "rev" => reference = GitReference::from_rev(val.into_owned()), "subdirectory" => { subdirectory = Some(PortablePathBuf::from(val.as_ref())); } "lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")), _ => {} } } let precise = repository.fragment().map(GitOid::from_str).transpose()?; // Clear out any existing state. repository.set_fragment(None); repository.set_query(None); // Remove the credentials. repository.remove_credentials(); // Create a PEP 508-compatible URL. let mut url = DisplaySafeUrl::parse(&format!("git+{repository}"))?; if let Some(rev) = reference.as_str() { let path = format!("{}@{}", url.path(), rev); url.set_path(&path); } let mut frags: Vec = Vec::new(); if let Some(subdirectory) = subdirectory.as_ref() { frags.push(format!("subdirectory={subdirectory}")); } // Preserve that we're using Git LFS in the Verbatim Url representations if lfs.enabled() { frags.push("lfs=true".to_string()); } if !frags.is_empty() { url.set_fragment(Some(&frags.join("&"))); } let url = VerbatimUrl::from_url(url); Ok(Self::Git { git: GitUrl::from_fields(repository, reference, precise, lfs)?, subdirectory: subdirectory.map(Box::::from), url, }) } RequirementSourceWire::Direct { url, subdirectory } => { let location = url.clone(); // Create a PEP 508-compatible URL. let mut url = url.clone(); if let Some(subdirectory) = &subdirectory { url.set_fragment(Some(&format!("subdirectory={subdirectory}"))); } Ok(Self::Url { location, subdirectory: subdirectory.map(Box::::from), ext: DistExtension::from_path(url.path()) .map_err(|err| ParsedUrlError::MissingExtensionUrl(url.to_string(), err))?, url: VerbatimUrl::from_url(url.clone()), }) } // TODO(charlie): The use of `CWD` here is incorrect. These should be resolved relative // to the workspace root, but we don't have access to it here. When comparing these // sources in the lockfile, we replace the URL anyway. Ideally, we'd either remove the // URL field or make it optional. RequirementSourceWire::Path { path } => { let path = Box::::from(path); let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(CWD.join(&path)))?; Ok(Self::Path { ext: DistExtension::from_path(&path).map_err(|err| { ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err) })?, install_path: path, url, }) } RequirementSourceWire::Directory { directory } => { let directory = Box::::from(directory); let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf( CWD.join(&directory), ))?; Ok(Self::Directory { install_path: directory, editable: Some(false), r#virtual: Some(false), url, }) } RequirementSourceWire::Editable { editable } => { let editable = Box::::from(editable); let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf( CWD.join(&editable), ))?; Ok(Self::Directory { install_path: editable, editable: Some(true), r#virtual: Some(false), url, }) } RequirementSourceWire::Virtual { r#virtual } => { let r#virtual = Box::::from(r#virtual); let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf( CWD.join(&r#virtual), ))?; Ok(Self::Directory { install_path: r#virtual, editable: Some(false), r#virtual: Some(true), url, }) } } } } #[cfg(test)] mod tests { use std::path::PathBuf; use uv_pep508::{MarkerTree, VerbatimUrl}; use crate::{Requirement, RequirementSource}; #[test] fn roundtrip() { let requirement = Requirement { name: "foo".parse().unwrap(), extras: Box::new([]), groups: Box::new([]), marker: MarkerTree::TRUE, source: RequirementSource::Registry { specifier: ">1,<2".parse().unwrap(), index: None, conflict: None, }, origin: None, }; let raw = toml::to_string(&requirement).unwrap(); let deserialized: Requirement = toml::from_str(&raw).unwrap(); assert_eq!(requirement, deserialized); let path = if cfg!(windows) { "C:\\home\\ferris\\foo" } else { "/home/ferris/foo" }; let requirement = Requirement { name: "foo".parse().unwrap(), extras: Box::new([]), groups: Box::new([]), marker: MarkerTree::TRUE, source: RequirementSource::Directory { install_path: PathBuf::from(path).into_boxed_path(), editable: Some(false), r#virtual: Some(false), url: VerbatimUrl::from_absolute_path(path).unwrap(), }, origin: None, }; let raw = toml::to_string(&requirement).unwrap(); let deserialized: Requirement = toml::from_str(&raw).unwrap(); assert_eq!(requirement, deserialized); } } uv-0.9.17+ds1/crates/uv-distribution-types/src/requires_python.rs000066400000000000000000001045561520155276700251410ustar00rootroot00000000000000use std::collections::Bound; use version_ranges::Ranges; use uv_distribution_filename::WheelFilename; use uv_pep440::{ LowerBound, UpperBound, Version, VersionSpecifier, VersionSpecifiers, release_specifiers_to_ranges, }; use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion}; use uv_platform_tags::{AbiTag, LanguageTag}; /// The `Requires-Python` requirement specifier. /// /// See: #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct RequiresPython { /// The supported Python versions as provides by the user, usually through the `requires-python` /// field in `pyproject.toml`. /// /// For a workspace, it's the intersection of all `requires-python` values in the workspace. If /// no bound was provided by the user, it's greater equal the current Python version. /// /// The specifiers remain static over the lifetime of the workspace, such that they /// represent the initial Python version constraints. specifiers: VersionSpecifiers, /// The lower and upper bounds of the given specifiers. /// /// The range may be narrowed over the course of dependency resolution as the resolver /// investigates environments with stricter Python version constraints. range: RequiresPythonRange, } impl RequiresPython { /// Returns a [`RequiresPython`] to express `>=` equality with the given version. pub fn greater_than_equal_version(version: &Version) -> Self { let version = version.only_release(); Self { specifiers: VersionSpecifiers::from(VersionSpecifier::greater_than_equal_version( version.clone(), )), range: RequiresPythonRange( LowerBound::new(Bound::Included(version.clone())), UpperBound::new(Bound::Unbounded), ), } } /// Returns a [`RequiresPython`] from a version specifier. pub fn from_specifiers(specifiers: &VersionSpecifiers) -> Self { let (lower_bound, upper_bound) = release_specifiers_to_ranges(specifiers.clone()) .bounding_range() .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned())) .unwrap_or((Bound::Unbounded, Bound::Unbounded)); Self { specifiers: specifiers.clone(), range: RequiresPythonRange(LowerBound::new(lower_bound), UpperBound::new(upper_bound)), } } /// Returns a [`RequiresPython`] to express the intersection of the given version specifiers. /// /// For example, given `>=3.8` and `>=3.9`, this would return `>=3.9`. pub fn intersection<'a>( specifiers: impl Iterator, ) -> Option { // Convert to PubGrub range and perform an intersection. let range = specifiers .map(|specs| release_specifiers_to_ranges(specs.clone())) .reduce(|acc, r| acc.intersection(&r))?; // If the intersection is empty, return `None`. if range.is_empty() { return None; } // Convert back to PEP 440 specifiers. let specifiers = VersionSpecifiers::from_release_only_bounds(range.iter()); // Extract the bounds. let range = RequiresPythonRange::from_range(&range); Some(Self { specifiers, range }) } /// Split the [`RequiresPython`] at the given version. /// /// For example, if the current requirement is `>=3.10`, and the split point is `3.11`, then /// the result will be `>=3.10 and <3.11` and `>=3.11`. pub fn split(&self, bound: Bound) -> Option<(Self, Self)> { let RequiresPythonRange(.., upper) = &self.range; let upper = Ranges::from_range_bounds((bound, upper.clone().into())); let lower = upper.complement(); // Intersect left and right with the existing range. let lower = lower.intersection(&Ranges::from(self.range.clone())); let upper = upper.intersection(&Ranges::from(self.range.clone())); if lower.is_empty() || upper.is_empty() { None } else { Some(( Self { specifiers: VersionSpecifiers::from_release_only_bounds(lower.iter()), range: RequiresPythonRange::from_range(&lower), }, Self { specifiers: VersionSpecifiers::from_release_only_bounds(upper.iter()), range: RequiresPythonRange::from_range(&upper), }, )) } } /// Narrow the [`RequiresPython`] by computing the intersection with the given range. /// /// Returns `None` if the given range is not narrower than the current range. pub fn narrow(&self, range: &RequiresPythonRange) -> Option { if *range == self.range { return None; } let lower = if range.0 >= self.range.0 { Some(&range.0) } else { None }; let upper = if range.1 <= self.range.1 { Some(&range.1) } else { None }; let range = match (lower, upper) { (Some(lower), Some(upper)) => Some(RequiresPythonRange(lower.clone(), upper.clone())), (Some(lower), None) => Some(RequiresPythonRange(lower.clone(), self.range.1.clone())), (None, Some(upper)) => Some(RequiresPythonRange(self.range.0.clone(), upper.clone())), (None, None) => None, }?; Some(Self { specifiers: range.specifiers(), range, }) } /// Returns this `Requires-Python` specifier as an equivalent /// [`MarkerTree`] utilizing the `python_full_version` marker field. /// /// This is useful for comparing a `Requires-Python` specifier with /// arbitrary marker expressions. For example, one can ask whether the /// returned marker expression is disjoint with another marker expression. /// If it is, then one can conclude that the `Requires-Python` specifier /// excludes the dependency with that other marker expression. /// /// If this `Requires-Python` specifier has no constraints, then this /// returns a marker tree that evaluates to `true` for all possible marker /// environments. pub fn to_marker_tree(&self) -> MarkerTree { match (self.range.0.as_ref(), self.range.1.as_ref()) { (Bound::Included(lower), Bound::Included(upper)) => { let mut lower = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::greater_than_equal_version(lower.clone()), }); let upper = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::less_than_equal_version(upper.clone()), }); lower.and(upper); lower } (Bound::Included(lower), Bound::Excluded(upper)) => { let mut lower = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::greater_than_equal_version(lower.clone()), }); let upper = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::less_than_version(upper.clone()), }); lower.and(upper); lower } (Bound::Excluded(lower), Bound::Included(upper)) => { let mut lower = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::greater_than_version(lower.clone()), }); let upper = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::less_than_equal_version(upper.clone()), }); lower.and(upper); lower } (Bound::Excluded(lower), Bound::Excluded(upper)) => { let mut lower = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::greater_than_version(lower.clone()), }); let upper = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::less_than_version(upper.clone()), }); lower.and(upper); lower } (Bound::Unbounded, Bound::Unbounded) => MarkerTree::TRUE, (Bound::Unbounded, Bound::Included(upper)) => { MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::less_than_equal_version(upper.clone()), }) } (Bound::Unbounded, Bound::Excluded(upper)) => { MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::less_than_version(upper.clone()), }) } (Bound::Included(lower), Bound::Unbounded) => { MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::greater_than_equal_version(lower.clone()), }) } (Bound::Excluded(lower), Bound::Unbounded) => { MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::greater_than_version(lower.clone()), }) } } } /// Returns `true` if the `Requires-Python` is compatible with the given version. /// /// N.B. This operation should primarily be used when evaluating compatibility of Python /// versions against the user's own project. For example, if the user defines a /// `requires-python` in a `pyproject.toml`, this operation could be used to determine whether /// a given Python interpreter is compatible with the user's project. pub fn contains(&self, version: &Version) -> bool { let version = version.only_release(); self.specifiers.contains(&version) } /// Returns `true` if the `Requires-Python` is contained by the given version specifiers. /// /// In this context, we treat `Requires-Python` as a lower bound. For example, if the /// requirement expresses `>=3.8, <4`, we treat it as `>=3.8`. `Requires-Python` itself was /// intended to enable packages to drop support for older versions of Python without breaking /// installations on those versions, and packages cannot know whether they are compatible with /// future, unreleased versions of Python. /// /// The specifiers are considered to "contain" the `Requires-Python` if the specifiers are /// compatible with all versions in the `Requires-Python` range (i.e., have a _lower_ lower /// bound). /// /// For example, if the `Requires-Python` is `>=3.8`, then `>=3.7` would be considered /// compatible, since all versions in the `Requires-Python` range are also covered by the /// provided range. However, `>=3.9` would not be considered compatible, as the /// `Requires-Python` includes Python 3.8, but `>=3.9` does not. /// /// N.B. This operation should primarily be used when evaluating the compatibility of a /// project's `Requires-Python` specifier against a dependency's `Requires-Python` specifier. pub fn is_contained_by(&self, target: &VersionSpecifiers) -> bool { let target = release_specifiers_to_ranges(target.clone()) .bounding_range() .map(|bounding_range| bounding_range.0.cloned()) .unwrap_or(Bound::Unbounded); // We want, e.g., `self.range.lower()` to be `>=3.8` and `target` to be `>=3.7`. // // That is: `target` should be less than or equal to `self.range.lower()`. *self.range.lower() >= LowerBound(target.clone()) } /// Returns the [`VersionSpecifiers`] for the `Requires-Python` specifier. pub fn specifiers(&self) -> &VersionSpecifiers { &self.specifiers } /// Returns `true` if the `Requires-Python` specifier is unbounded. pub fn is_unbounded(&self) -> bool { self.range.lower().as_ref() == Bound::Unbounded } /// Returns `true` if the `Requires-Python` specifier is set to an exact version /// without specifying a patch version. (e.g. `==3.10`) pub fn is_exact_without_patch(&self) -> bool { match self.range.lower().as_ref() { Bound::Included(version) => { version.release().len() == 2 && self.range.upper().as_ref() == Bound::Included(version) } _ => false, } } /// Returns the [`Range`] bounding the `Requires-Python` specifier. pub fn range(&self) -> &RequiresPythonRange { &self.range } /// Returns a wheel tag that's compatible with the `Requires-Python` specifier. pub fn abi_tag(&self) -> Option { match self.range.lower().as_ref() { Bound::Included(version) | Bound::Excluded(version) => { let major = version.release().first().copied()?; let major = u8::try_from(major).ok()?; let minor = version.release().get(1).copied()?; let minor = u8::try_from(minor).ok()?; Some(AbiTag::CPython { gil_disabled: false, python_version: (major, minor), }) } Bound::Unbounded => None, } } /// Simplifies the given markers in such a way as to assume that /// the Python version is constrained by this Python version bound. /// /// For example, with `requires-python = '>=3.8'`, a marker like this: /// /// ```text /// python_full_version >= '3.8' and python_full_version < '3.12' /// ``` /// /// Will be simplified to: /// /// ```text /// python_full_version < '3.12' /// ``` /// /// That is, `python_full_version >= '3.8'` is assumed to be true by virtue /// of `requires-python`, and is thus not needed in the marker. /// /// This should be used in contexts in which this assumption is valid to /// make. Generally, this means it should not be used inside the resolver, /// but instead near the boundaries of the system (like formatting error /// messages and writing the lock file). The reason for this is that /// this simplification fundamentally changes the meaning of the marker, /// and the *only* correct way to interpret it is in a context in which /// `requires-python` is known to be true. For example, when markers from /// a lock file are deserialized and turned into a `ResolutionGraph`, the /// markers are "complexified" to put the `requires-python` assumption back /// into the marker explicitly. pub fn simplify_markers(&self, marker: MarkerTree) -> MarkerTree { let (lower, upper) = (self.range().lower(), self.range().upper()); marker.simplify_python_versions(lower.as_ref(), upper.as_ref()) } /// The inverse of `simplify_markers`. /// /// This should be applied near the boundaries of uv when markers are /// deserialized from a context where `requires-python` is assumed. For /// example, with `requires-python = '>=3.8'` and a marker like: /// /// ```text /// python_full_version < '3.12' /// ``` /// /// It will be "complexified" to: /// /// ```text /// python_full_version >= '3.8' and python_full_version < '3.12' /// ``` pub fn complexify_markers(&self, marker: MarkerTree) -> MarkerTree { let (lower, upper) = (self.range().lower(), self.range().upper()); marker.complexify_python_versions(lower.as_ref(), upper.as_ref()) } /// Returns `false` if the wheel's tags state it can't be used in the given Python version /// range. /// /// It is meant to filter out clearly unusable wheels with perfect specificity and acceptable /// sensitivity, we return `true` if the tags are unknown. pub fn matches_wheel_tag(&self, wheel: &WheelFilename) -> bool { wheel.abi_tags().iter().any(|abi_tag| { if *abi_tag == AbiTag::Abi3 { // Universal tags are allowed. true } else if *abi_tag == AbiTag::None { wheel.python_tags().iter().any(|python_tag| { // Remove `py2-none-any` and `py27-none-any` and analogous `cp` and `pp` tags. if matches!( python_tag, LanguageTag::Python { major: 2, .. } | LanguageTag::CPython { python_version: (2, ..) } | LanguageTag::PyPy { python_version: (2, ..) } | LanguageTag::GraalPy { python_version: (2, ..) } | LanguageTag::Pyston { python_version: (2, ..) } ) { return false; } // Remove (e.g.) `py312-none-any` if the specifier is `==3.10.*`. However, // `py37-none-any` would be fine, since the `3.7` represents a lower bound. if let LanguageTag::Python { major: 3, minor: Some(minor), } = python_tag { // Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`. let wheel_bound = UpperBound(Bound::Included(Version::new([3, u64::from(*minor)]))); if wheel_bound > self.range.upper().major_minor() { return false; } return true; } // Remove (e.g.) `cp36-none-any` or `cp312-none-any` if the specifier is // `==3.10.*`, since these tags require an exact match. if let LanguageTag::CPython { python_version: (3, minor), } | LanguageTag::PyPy { python_version: (3, minor), } | LanguageTag::GraalPy { python_version: (3, minor), } | LanguageTag::Pyston { python_version: (3, minor), } = python_tag { // Ex) If the wheel bound is `3.6`, then it doesn't match `>=3.10`. let wheel_bound = LowerBound(Bound::Included(Version::new([3, u64::from(*minor)]))); if wheel_bound < self.range.lower().major_minor() { return false; } // Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`. let wheel_bound = UpperBound(Bound::Included(Version::new([3, u64::from(*minor)]))); if wheel_bound > self.range.upper().major_minor() { return false; } return true; } // Unknown tags are allowed. true }) } else if matches!( abi_tag, AbiTag::CPython { python_version: (2, ..), .. } | AbiTag::PyPy { python_version: None | Some((2, ..)), .. } | AbiTag::GraalPy { python_version: (2, ..), .. } ) { // Python 2 is never allowed. false } else if let AbiTag::CPython { python_version: (3, minor), .. } | AbiTag::PyPy { python_version: Some((3, minor)), .. } | AbiTag::GraalPy { python_version: (3, minor), .. } = abi_tag { // Ex) If the wheel bound is `3.6`, then it doesn't match `>=3.10`. let wheel_bound = LowerBound(Bound::Included(Version::new([3, u64::from(*minor)]))); if wheel_bound < self.range.lower().major_minor() { return false; } // Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`. let wheel_bound = UpperBound(Bound::Included(Version::new([3, u64::from(*minor)]))); if wheel_bound > self.range.upper().major_minor() { return false; } true } else { // Unknown tags are allowed. true } }) } } impl std::fmt::Display for RequiresPython { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(&self.specifiers, f) } } impl serde::Serialize for RequiresPython { fn serialize(&self, serializer: S) -> Result { self.specifiers.serialize(serializer) } } impl<'de> serde::Deserialize<'de> for RequiresPython { fn deserialize>(deserializer: D) -> Result { let specifiers = VersionSpecifiers::deserialize(deserializer)?; let range = release_specifiers_to_ranges(specifiers.clone()); let range = RequiresPythonRange::from_range(&range); Ok(Self { specifiers, range }) } } #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct RequiresPythonRange(LowerBound, UpperBound); impl RequiresPythonRange { /// Initialize a [`RequiresPythonRange`] from a [`Range`]. pub fn from_range(range: &Ranges) -> Self { let (lower, upper) = range .bounding_range() .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned())) .unwrap_or((Bound::Unbounded, Bound::Unbounded)); Self(LowerBound(lower), UpperBound(upper)) } /// Initialize a [`RequiresPythonRange`] with the given bounds. pub fn new(lower: LowerBound, upper: UpperBound) -> Self { Self(lower, upper) } /// Returns the lower bound. pub fn lower(&self) -> &LowerBound { &self.0 } /// Returns the upper bound. pub fn upper(&self) -> &UpperBound { &self.1 } /// Returns the [`VersionSpecifiers`] for the range. pub fn specifiers(&self) -> VersionSpecifiers { [self.0.specifier(), self.1.specifier()] .into_iter() .flatten() .collect() } } impl Default for RequiresPythonRange { fn default() -> Self { Self(LowerBound(Bound::Unbounded), UpperBound(Bound::Unbounded)) } } impl From for Ranges { fn from(value: RequiresPythonRange) -> Self { Self::from_range_bounds::<(Bound, Bound), _>(( value.0.into(), value.1.into(), )) } } /// A simplified marker is just like a normal marker, except it has possibly /// been simplified by `requires-python`. /// /// A simplified marker should only exist in contexts where a `requires-python` /// setting can be assumed. In order to get a "normal" marker out of /// a simplified marker, one must re-contextualize it by adding the /// `requires-python` constraint back to the marker. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)] pub struct SimplifiedMarkerTree(MarkerTree); impl SimplifiedMarkerTree { /// Simplifies the given markers by assuming the given `requires-python` /// bound is true. pub fn new(requires_python: &RequiresPython, marker: MarkerTree) -> Self { Self(requires_python.simplify_markers(marker)) } /// Complexifies the given markers by adding the given `requires-python` as /// a constraint to these simplified markers. pub fn into_marker(self, requires_python: &RequiresPython) -> MarkerTree { requires_python.complexify_markers(self.0) } /// Attempts to convert this simplified marker to a string. /// /// This only returns `None` when the underlying marker is always true, /// i.e., it matches all possible marker environments. pub fn try_to_string(self) -> Option { self.0.try_to_string() } /// Returns the underlying marker tree without re-complexifying them. pub fn as_simplified_marker_tree(self) -> MarkerTree { self.0 } } #[cfg(test)] mod tests { use std::cmp::Ordering; use std::collections::Bound; use std::str::FromStr; use uv_distribution_filename::WheelFilename; use uv_pep440::{LowerBound, UpperBound, Version, VersionSpecifiers}; use crate::RequiresPython; #[test] fn requires_python_included() { let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &[ "bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl", "black-24.4.2-cp310-cp310-win_amd64.whl", "black-24.4.2-cp310-none-win_amd64.whl", "cbor2-5.6.4-py3-none-any.whl", "solace_pubsubplus-1.8.0-py36-none-manylinux_2_12_x86_64.whl", "torch-1.10.0-py310-none-macosx_10_9_x86_64.whl", "torch-1.10.0-py37-none-macosx_10_9_x86_64.whl", "watchfiles-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", ]; for wheel_name in wheel_names { assert!( requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), "{wheel_name}" ); } let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &["dearpygui-1.11.1-cp312-cp312-win_amd64.whl"]; for wheel_name in wheel_names { assert!( requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), "{wheel_name}" ); } let version_specifiers = VersionSpecifiers::from_str("==3.12.6").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl"]; for wheel_name in wheel_names { assert!( requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), "{wheel_name}" ); } let version_specifiers = VersionSpecifiers::from_str("==3.12").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl"]; for wheel_name in wheel_names { assert!( requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), "{wheel_name}" ); } } #[test] fn requires_python_dropped() { let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &[ "PySocks-1.7.1-py27-none-any.whl", "black-24.4.2-cp39-cp39-win_amd64.whl", "dearpygui-1.11.1-cp312-cp312-win_amd64.whl", "psutil-6.0.0-cp27-none-win32.whl", "psutil-6.0.0-cp36-cp36m-win32.whl", "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", "torch-1.10.0-cp311-none-macosx_10_9_x86_64.whl", "torch-1.10.0-cp36-none-macosx_10_9_x86_64.whl", "torch-1.10.0-py311-none-macosx_10_9_x86_64.whl", ]; for wheel_name in wheel_names { assert!( !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), "{wheel_name}" ); } let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &["dearpygui-1.11.1-cp310-cp310-win_amd64.whl"]; for wheel_name in wheel_names { assert!( !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), "{wheel_name}" ); } } #[test] fn lower_bound_ordering() { let versions = &[ // No bound LowerBound::new(Bound::Unbounded), // >=3.8 LowerBound::new(Bound::Included(Version::new([3, 8]))), // >3.8 LowerBound::new(Bound::Excluded(Version::new([3, 8]))), // >=3.8.1 LowerBound::new(Bound::Included(Version::new([3, 8, 1]))), // >3.8.1 LowerBound::new(Bound::Excluded(Version::new([3, 8, 1]))), ]; for (i, v1) in versions.iter().enumerate() { for v2 in &versions[i + 1..] { assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}"); } } } #[test] fn upper_bound_ordering() { let versions = &[ // <3.8 UpperBound::new(Bound::Excluded(Version::new([3, 8]))), // <=3.8 UpperBound::new(Bound::Included(Version::new([3, 8]))), // <3.8.1 UpperBound::new(Bound::Excluded(Version::new([3, 8, 1]))), // <=3.8.1 UpperBound::new(Bound::Included(Version::new([3, 8, 1]))), // No bound UpperBound::new(Bound::Unbounded), ]; for (i, v1) in versions.iter().enumerate() { for v2 in &versions[i + 1..] { assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}"); } } } #[test] fn is_exact_without_patch() { let test_cases = [ ("==3.12", true), ("==3.10, <3.11", true), ("==3.10, <=3.11", true), ("==3.12.1", false), ("==3.12.*", false), ("==3.*", false), (">=3.10", false), (">3.9", false), ("<4.0", false), (">=3.10, <3.11", false), ("", false), ]; for (version, expected) in test_cases { let version_specifiers = VersionSpecifiers::from_str(version).unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); assert_eq!(requires_python.is_exact_without_patch(), expected); } } #[test] fn split_version() { // Splitting `>=3.10` on `>3.12` should result in `>=3.10, <=3.12` and `>3.12`. let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let (lower, upper) = requires_python .split(Bound::Excluded(Version::new([3, 12]))) .unwrap(); assert_eq!( lower, RequiresPython::from_specifiers( &VersionSpecifiers::from_str(">=3.10, <=3.12").unwrap() ) ); assert_eq!( upper, RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">3.12").unwrap()) ); // Splitting `>=3.10` on `>=3.12` should result in `>=3.10, <3.12` and `>=3.12`. let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let (lower, upper) = requires_python .split(Bound::Included(Version::new([3, 12]))) .unwrap(); assert_eq!( lower, RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.10, <3.12").unwrap()) ); assert_eq!( upper, RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap()) ); // Splitting `>=3.10` on `>=3.9` should return `None`. let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); assert!( requires_python .split(Bound::Included(Version::new([3, 9]))) .is_none() ); // Splitting `>=3.10` on `>=3.10` should return `None`. let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); assert!( requires_python .split(Bound::Included(Version::new([3, 10]))) .is_none() ); // Splitting `>=3.9, <3.13` on `>=3.11` should result in `>=3.9, <3.11` and `>=3.11, <3.13`. let version_specifiers = VersionSpecifiers::from_str(">=3.9, <3.13").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let (lower, upper) = requires_python .split(Bound::Included(Version::new([3, 11]))) .unwrap(); assert_eq!( lower, RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.9, <3.11").unwrap()) ); assert_eq!( upper, RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.11, <3.13").unwrap()) ); } } uv-0.9.17+ds1/crates/uv-distribution-types/src/resolution.rs000066400000000000000000000244251520155276700241000ustar00rootroot00000000000000use uv_distribution_filename::DistExtension; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pypi_types::{HashDigest, HashDigests}; use crate::{ BuiltDist, Diagnostic, Dist, IndexMetadata, Name, RequirementSource, ResolvedDist, SourceDist, }; /// A set of packages pinned at specific versions. /// /// This is similar to [`ResolverOutput`], but represents a resolution for a subset of all /// marker environments. For example, the resolution is guaranteed to contain at most one version /// for a given package. #[derive(Debug, Default, Clone)] pub struct Resolution { graph: petgraph::graph::DiGraph, diagnostics: Vec, } impl Resolution { /// Create a [`Resolution`] from the given pinned packages. pub fn new(graph: petgraph::graph::DiGraph) -> Self { Self { graph, diagnostics: Vec::new(), } } /// Return the underlying graph of the resolution. pub fn graph(&self) -> &petgraph::graph::DiGraph { &self.graph } /// Add [`Diagnostics`] to the resolution. #[must_use] pub fn with_diagnostics(mut self, diagnostics: Vec) -> Self { self.diagnostics.extend(diagnostics); self } /// Return the hashes for the given package name, if they exist. pub fn hashes(&self) -> impl Iterator { self.graph .node_indices() .filter_map(move |node| match &self.graph[node] { Node::Dist { dist, hashes, install, .. } if *install => Some((dist, hashes.as_slice())), _ => None, }) } /// Iterate over the [`ResolvedDist`] entities in this resolution. pub fn distributions(&self) -> impl Iterator { self.graph .raw_nodes() .iter() .filter_map(|node| match &node.weight { Node::Dist { dist, install, .. } if *install => Some(dist), _ => None, }) } /// Return the number of distributions in this resolution. pub fn len(&self) -> usize { self.distributions().count() } /// Return `true` if there are no pinned packages in this resolution. pub fn is_empty(&self) -> bool { self.distributions().next().is_none() } /// Return the [`ResolutionDiagnostic`]s that were produced during resolution. pub fn diagnostics(&self) -> &[ResolutionDiagnostic] { &self.diagnostics } /// Filter the resolution to only include packages that match the given predicate. #[must_use] pub fn filter(mut self, predicate: impl Fn(&ResolvedDist) -> bool) -> Self { for node in self.graph.node_weights_mut() { if let Node::Dist { dist, install, .. } = node { if !predicate(dist) { *install = false; } } } self } /// Map over the resolved distributions in this resolution. /// /// For efficiency, the map function should return `None` if the resolved distribution is /// unchanged. #[must_use] pub fn map(mut self, predicate: impl Fn(&ResolvedDist) -> Option) -> Self { for node in self.graph.node_weights_mut() { if let Node::Dist { dist, .. } = node { if let Some(transformed) = predicate(dist) { *dist = transformed; } } } self } } #[derive(Debug, Clone, Hash)] pub enum ResolutionDiagnostic { MissingExtra { /// The distribution that was requested with a non-existent extra. For example, /// `black==23.10.0`. dist: ResolvedDist, /// The extra that was requested. For example, `colorama` in `black[colorama]`. extra: ExtraName, }, MissingGroup { /// The distribution that was requested with a non-existent development dependency group. dist: ResolvedDist, /// The development dependency group that was requested. group: GroupName, }, YankedVersion { /// The package that was requested with a yanked version. For example, `black==23.10.0`. dist: ResolvedDist, /// The reason that the version was yanked, if any. reason: Option, }, MissingLowerBound { /// The name of the package that had no lower bound from any other package in the /// resolution. For example, `black`. package_name: PackageName, }, } impl Diagnostic for ResolutionDiagnostic { /// Convert the diagnostic into a user-facing message. fn message(&self) -> String { match self { Self::MissingExtra { dist, extra } => { format!("The package `{dist}` does not have an extra named `{extra}`") } Self::MissingGroup { dist, group } => { format!( "The package `{dist}` does not have a development dependency group named `{group}`" ) } Self::YankedVersion { dist, reason } => { if let Some(reason) = reason { format!("`{dist}` is yanked (reason: \"{reason}\")") } else { format!("`{dist}` is yanked") } } Self::MissingLowerBound { package_name: name } => { format!( "The transitive dependency `{name}` is unpinned. \ Consider setting a lower bound with a constraint when using \ `--resolution lowest` to avoid using outdated versions." ) } } } /// Returns `true` if the [`PackageName`] is involved in this diagnostic. fn includes(&self, name: &PackageName) -> bool { match self { Self::MissingExtra { dist, .. } => name == dist.name(), Self::MissingGroup { dist, .. } => name == dist.name(), Self::YankedVersion { dist, .. } => name == dist.name(), Self::MissingLowerBound { package_name } => name == package_name, } } } /// A node in the resolution, along with whether its been filtered out. /// /// We retain filtered nodes as we still need to be able to trace dependencies through the graph /// (e.g., to determine why a package was included in the resolution). #[derive(Debug, Clone)] pub enum Node { Root, Dist { dist: ResolvedDist, hashes: HashDigests, install: bool, }, } impl Node { /// Returns `true` if the node should be installed. pub fn install(&self) -> bool { match self { Self::Root => false, Self::Dist { install, .. } => *install, } } } /// An edge in the resolution graph. #[derive(Debug, Clone)] pub enum Edge { Prod, Optional(ExtraName), Dev(GroupName), } impl From<&ResolvedDist> for RequirementSource { fn from(resolved_dist: &ResolvedDist) -> Self { match resolved_dist { ResolvedDist::Installable { dist, .. } => match dist.as_ref() { Dist::Built(BuiltDist::Registry(wheels)) => { let wheel = wheels.best_wheel(); Self::Registry { specifier: uv_pep440::VersionSpecifiers::from( uv_pep440::VersionSpecifier::equals_version( wheel.filename.version.clone(), ), ), index: Some(IndexMetadata::from(wheel.index.clone())), conflict: None, } } Dist::Built(BuiltDist::DirectUrl(wheel)) => { let mut location = wheel.url.to_url(); location.set_fragment(None); Self::Url { url: wheel.url.clone(), location, subdirectory: None, ext: DistExtension::Wheel, } } Dist::Built(BuiltDist::Path(wheel)) => Self::Path { install_path: wheel.install_path.clone(), url: wheel.url.clone(), ext: DistExtension::Wheel, }, Dist::Source(SourceDist::Registry(sdist)) => Self::Registry { specifier: uv_pep440::VersionSpecifiers::from( uv_pep440::VersionSpecifier::equals_version(sdist.version.clone()), ), index: Some(IndexMetadata::from(sdist.index.clone())), conflict: None, }, Dist::Source(SourceDist::DirectUrl(sdist)) => { let mut location = sdist.url.to_url(); location.set_fragment(None); Self::Url { url: sdist.url.clone(), location, subdirectory: sdist.subdirectory.clone(), ext: DistExtension::Source(sdist.ext), } } Dist::Source(SourceDist::Git(sdist)) => Self::Git { git: (*sdist.git).clone(), url: sdist.url.clone(), subdirectory: sdist.subdirectory.clone(), }, Dist::Source(SourceDist::Path(sdist)) => Self::Path { install_path: sdist.install_path.clone(), url: sdist.url.clone(), ext: DistExtension::Source(sdist.ext), }, Dist::Source(SourceDist::Directory(sdist)) => Self::Directory { install_path: sdist.install_path.clone(), url: sdist.url.clone(), editable: sdist.editable, r#virtual: sdist.r#virtual, }, }, ResolvedDist::Installed { dist } => Self::Registry { specifier: uv_pep440::VersionSpecifiers::from( uv_pep440::VersionSpecifier::equals_version(dist.version().clone()), ), index: None, conflict: None, }, } } } uv-0.9.17+ds1/crates/uv-distribution-types/src/resolved.rs000066400000000000000000000204671520155276700235220ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use std::path::Path; use std::sync::Arc; use uv_normalize::PackageName; use uv_pep440::Version; use uv_pypi_types::Yanked; use crate::{ BuiltDist, Dist, DistributionId, DistributionMetadata, Identifier, IndexUrl, InstalledDist, Name, PrioritizedDist, RegistryBuiltWheel, RegistrySourceDist, ResourceId, SourceDist, VersionOrUrlRef, }; /// A distribution that can be used for resolution and installation. /// /// Either an already-installed distribution or a distribution that can be installed. #[derive(Debug, Clone, Hash)] #[allow(clippy::large_enum_variant)] pub enum ResolvedDist { Installed { dist: Arc, }, Installable { dist: Arc, version: Option, }, } /// A variant of [`ResolvedDist`] with borrowed inner distributions. #[derive(Debug, Clone)] pub enum ResolvedDistRef<'a> { Installed { dist: &'a InstalledDist, }, InstallableRegistrySourceDist { /// The source distribution that should be used. sdist: &'a RegistrySourceDist, /// The prioritized distribution that the wheel came from. prioritized: &'a PrioritizedDist, }, InstallableRegistryBuiltDist { /// The wheel that should be used. wheel: &'a RegistryBuiltWheel, /// The prioritized distribution that the wheel came from. prioritized: &'a PrioritizedDist, }, } impl ResolvedDist { /// Return true if the distribution is editable. pub fn is_editable(&self) -> bool { match self { Self::Installable { dist, .. } => dist.is_editable(), Self::Installed { dist } => dist.is_editable(), } } /// Return true if the distribution refers to a local file or directory. pub fn is_local(&self) -> bool { match self { Self::Installable { dist, .. } => dist.is_local(), Self::Installed { dist } => dist.is_local(), } } /// Returns the [`IndexUrl`], if the distribution is from a registry. pub fn index(&self) -> Option<&IndexUrl> { match self { Self::Installable { dist, .. } => dist.index(), Self::Installed { .. } => None, } } /// Returns the [`Yanked`] status of the distribution, if available. pub fn yanked(&self) -> Option<&Yanked> { match self { Self::Installable { dist, .. } => match dist.as_ref() { Dist::Source(SourceDist::Registry(sdist)) => sdist.file.yanked.as_deref(), Dist::Built(BuiltDist::Registry(wheel)) => { wheel.best_wheel().file.yanked.as_deref() } _ => None, }, Self::Installed { .. } => None, } } /// Returns the version of the distribution, if available. pub fn version(&self) -> Option<&Version> { match self { Self::Installable { version, dist } => dist.version().or(version.as_ref()), Self::Installed { dist } => Some(dist.version()), } } /// Return the source tree of the distribution, if available. pub fn source_tree(&self) -> Option<&Path> { match self { Self::Installable { dist, .. } => dist.source_tree(), Self::Installed { .. } => None, } } } impl ResolvedDistRef<'_> { pub fn to_owned(&self) -> ResolvedDist { match self { Self::InstallableRegistrySourceDist { sdist, prioritized } => { // This is okay because we're only here if the prioritized dist // has an sdist, so this always succeeds. let source = prioritized.source_dist().expect("a source distribution"); assert_eq!( (&sdist.name, &sdist.version), (&source.name, &source.version), "expected chosen sdist to match prioritized sdist" ); ResolvedDist::Installable { dist: Arc::new(Dist::Source(SourceDist::Registry(source))), version: Some(sdist.version.clone()), } } Self::InstallableRegistryBuiltDist { wheel, prioritized, .. } => { assert_eq!( Some(&wheel.filename), prioritized.best_wheel().map(|(wheel, _)| &wheel.filename), "expected chosen wheel to match best wheel" ); // This is okay because we're only here if the prioritized dist // has at least one wheel, so this always succeeds. let built = prioritized.built_dist().expect("at least one wheel"); ResolvedDist::Installable { dist: Arc::new(Dist::Built(BuiltDist::Registry(built))), version: Some(wheel.filename.version.clone()), } } Self::Installed { dist } => ResolvedDist::Installed { dist: Arc::new((*dist).clone()), }, } } /// Returns the [`IndexUrl`], if the distribution is from a registry. pub fn index(&self) -> Option<&IndexUrl> { match self { Self::InstallableRegistrySourceDist { sdist, .. } => Some(&sdist.index), Self::InstallableRegistryBuiltDist { wheel, .. } => Some(&wheel.index), Self::Installed { .. } => None, } } } impl Display for ResolvedDistRef<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::InstallableRegistrySourceDist { sdist, .. } => Display::fmt(sdist, f), Self::InstallableRegistryBuiltDist { wheel, .. } => Display::fmt(wheel, f), Self::Installed { dist } => Display::fmt(dist, f), } } } impl Name for ResolvedDistRef<'_> { fn name(&self) -> &PackageName { match self { Self::InstallableRegistrySourceDist { sdist, .. } => sdist.name(), Self::InstallableRegistryBuiltDist { wheel, .. } => wheel.name(), Self::Installed { dist } => dist.name(), } } } impl DistributionMetadata for ResolvedDistRef<'_> { fn version_or_url(&self) -> VersionOrUrlRef<'_> { match self { Self::Installed { dist } => VersionOrUrlRef::Version(dist.version()), Self::InstallableRegistrySourceDist { sdist, .. } => sdist.version_or_url(), Self::InstallableRegistryBuiltDist { wheel, .. } => wheel.version_or_url(), } } } impl Identifier for ResolvedDistRef<'_> { fn distribution_id(&self) -> DistributionId { match self { Self::Installed { dist } => dist.distribution_id(), Self::InstallableRegistrySourceDist { sdist, .. } => sdist.distribution_id(), Self::InstallableRegistryBuiltDist { wheel, .. } => wheel.distribution_id(), } } fn resource_id(&self) -> ResourceId { match self { Self::Installed { dist } => dist.resource_id(), Self::InstallableRegistrySourceDist { sdist, .. } => sdist.resource_id(), Self::InstallableRegistryBuiltDist { wheel, .. } => wheel.resource_id(), } } } impl Name for ResolvedDist { fn name(&self) -> &PackageName { match self { Self::Installable { dist, .. } => dist.name(), Self::Installed { dist } => dist.name(), } } } impl DistributionMetadata for ResolvedDist { fn version_or_url(&self) -> VersionOrUrlRef<'_> { match self { Self::Installed { dist } => dist.version_or_url(), Self::Installable { dist, .. } => dist.version_or_url(), } } } impl Identifier for ResolvedDist { fn distribution_id(&self) -> DistributionId { match self { Self::Installed { dist } => dist.distribution_id(), Self::Installable { dist, .. } => dist.distribution_id(), } } fn resource_id(&self) -> ResourceId { match self { Self::Installed { dist } => dist.resource_id(), Self::Installable { dist, .. } => dist.resource_id(), } } } impl Display for ResolvedDist { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Installed { dist } => dist.fmt(f), Self::Installable { dist, .. } => dist.fmt(f), } } } uv-0.9.17+ds1/crates/uv-distribution-types/src/specified_requirement.rs000066400000000000000000000177521520155276700262550ustar00rootroot00000000000000use std::borrow::Cow; use std::fmt::{Display, Formatter}; use uv_git_types::{GitLfs, GitReference}; use uv_normalize::ExtraName; use uv_pep508::{MarkerEnvironment, MarkerTree, UnnamedRequirement}; use uv_pypi_types::{Hashes, ParsedUrl}; use crate::{Requirement, RequirementSource, VerbatimParsedUrl}; /// An [`UnresolvedRequirement`] with additional metadata from `requirements.txt`, currently only /// hashes but in the future also editable and similar information. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct NameRequirementSpecification { /// The actual requirement. pub requirement: Requirement, /// Hashes of the downloadable packages. pub hashes: Vec, } /// An [`UnresolvedRequirement`] with additional metadata from `requirements.txt`, currently only /// hashes but in the future also editable and similar information. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct UnresolvedRequirementSpecification { /// The actual requirement. pub requirement: UnresolvedRequirement, /// Hashes of the downloadable packages. pub hashes: Vec, } /// A requirement read from a `requirements.txt` or `pyproject.toml` file. /// /// It is considered unresolved as we still need to query the URL for the `Unnamed` variant to /// resolve the requirement name. /// /// Analog to `RequirementsTxtRequirement` but with `distribution_types::Requirement` instead of /// `uv_pep508::Requirement`. #[derive(Hash, Debug, Clone, Eq, PartialEq)] pub enum UnresolvedRequirement { /// The uv-specific superset over PEP 508 requirements specifier incorporating /// `tool.uv.sources`. Named(Requirement), /// A PEP 508-like, direct URL dependency specifier. Unnamed(UnnamedRequirement), } impl Display for UnresolvedRequirement { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Named(requirement) => write!(f, "{requirement}"), Self::Unnamed(requirement) => write!(f, "{requirement}"), } } } impl UnresolvedRequirement { /// Returns whether the markers apply for the given environment. /// /// When the environment is not given, this treats all marker expressions /// that reference the environment as true. In other words, it does /// environment independent expression evaluation. (Which in turn devolves /// to "only evaluate marker expressions that reference an extra name.") pub fn evaluate_markers(&self, env: Option<&MarkerEnvironment>, extras: &[ExtraName]) -> bool { match self { Self::Named(requirement) => requirement.evaluate_markers(env, extras), Self::Unnamed(requirement) => requirement.evaluate_optional_environment(env, extras), } } /// Augment a user-provided requirement by attaching any specification data that was provided /// separately from the requirement itself (e.g., `--branch main`). #[must_use] pub fn augment_requirement( self, rev: Option<&str>, tag: Option<&str>, branch: Option<&str>, lfs: Option, marker: Option, ) -> Self { #[allow(clippy::manual_map)] let git_reference = if let Some(rev) = rev { Some(GitReference::from_rev(rev.to_string())) } else if let Some(tag) = tag { Some(GitReference::Tag(tag.to_string())) } else if let Some(branch) = branch { Some(GitReference::Branch(branch.to_string())) } else { None }; match self { Self::Named(mut requirement) => Self::Named(Requirement { marker: marker .map(|marker| { requirement.marker.and(marker); requirement.marker }) .unwrap_or(requirement.marker), source: match requirement.source { RequirementSource::Git { git, subdirectory, url, } => { let git = if let Some(git_reference) = git_reference { git.with_reference(git_reference) } else { git }; let git = if let Some(lfs) = lfs { git.with_lfs(GitLfs::from(lfs)) } else { git }; RequirementSource::Git { git, subdirectory, url, } } _ => requirement.source, }, ..requirement }), Self::Unnamed(mut requirement) => Self::Unnamed(UnnamedRequirement { marker: marker .map(|marker| { requirement.marker.and(marker); requirement.marker }) .unwrap_or(requirement.marker), url: match requirement.url.parsed_url { ParsedUrl::Git(mut git) => { if let Some(git_reference) = git_reference { git.url = git.url.with_reference(git_reference); } if let Some(lfs) = lfs { git.url = git.url.with_lfs(GitLfs::from(lfs)); } VerbatimParsedUrl { parsed_url: ParsedUrl::Git(git), verbatim: requirement.url.verbatim, } } _ => requirement.url, }, ..requirement }), } } /// Returns the extras for the requirement. pub fn extras(&self) -> &[ExtraName] { match self { Self::Named(requirement) => &requirement.extras, Self::Unnamed(requirement) => &requirement.extras, } } /// Return the version specifier or URL for the requirement. pub fn source(&self) -> Cow<'_, RequirementSource> { match self { Self::Named(requirement) => Cow::Borrowed(&requirement.source), Self::Unnamed(requirement) => Cow::Owned(RequirementSource::from_parsed_url( requirement.url.parsed_url.clone(), requirement.url.verbatim.clone(), )), } } /// Returns `true` if the requirement is editable. pub fn is_editable(&self) -> bool { match self { Self::Named(requirement) => requirement.is_editable(), Self::Unnamed(requirement) => requirement.url.is_editable(), } } /// Return the hashes of the requirement, as specified in the URL fragment. pub fn hashes(&self) -> Option { match self { Self::Named(requirement) => requirement.hashes(), Self::Unnamed(requirement) => { let fragment = requirement.url.verbatim.fragment()?; Hashes::parse_fragment(fragment).ok() } } } } impl NameRequirementSpecification { /// Return the hashes of the requirement, as specified in the URL fragment. pub fn hashes(&self) -> Option { let RequirementSource::Url { ref url, .. } = self.requirement.source else { return None; }; let fragment = url.fragment()?; Hashes::parse_fragment(fragment).ok() } } impl From for UnresolvedRequirementSpecification { fn from(requirement: Requirement) -> Self { Self { requirement: UnresolvedRequirement::Named(requirement), hashes: Vec::new(), } } } impl From for NameRequirementSpecification { fn from(requirement: Requirement) -> Self { Self { requirement, hashes: Vec::new(), } } } uv-0.9.17+ds1/crates/uv-distribution-types/src/status_code_strategy.rs000066400000000000000000000242311520155276700261270ustar00rootroot00000000000000#[cfg(feature = "schemars")] use std::borrow::Cow; use std::ops::Deref; use http::StatusCode; use rustc_hash::FxHashSet; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use url::Url; use crate::{IndexCapabilities, IndexUrl}; #[derive(Debug, Clone, Default, Eq, PartialEq)] pub enum IndexStatusCodeStrategy { #[default] Default, IgnoreErrorCodes { status_codes: FxHashSet, }, } impl IndexStatusCodeStrategy { /// Derive a strategy from an index URL. We special-case PyTorch. Otherwise, /// we follow the default strategy. pub fn from_index_url(url: &Url) -> Self { if url .host_str() .is_some_and(|host| host.ends_with("pytorch.org")) { // The PyTorch registry returns a 403 when a package is not found, so // we ignore them when deciding whether to search other indexes. Self::IgnoreErrorCodes { status_codes: FxHashSet::from_iter([StatusCode::FORBIDDEN]), } } else { Self::Default } } /// Derive a strategy from a list of status codes to ignore. pub fn from_ignored_error_codes(status_codes: &[SerializableStatusCode]) -> Self { Self::IgnoreErrorCodes { status_codes: status_codes .iter() .map(SerializableStatusCode::deref) .copied() .collect::>(), } } /// Derive a strategy for ignoring authentication error codes. pub fn ignore_authentication_error_codes() -> Self { Self::IgnoreErrorCodes { status_codes: FxHashSet::from_iter([ StatusCode::UNAUTHORIZED, StatusCode::FORBIDDEN, StatusCode::NETWORK_AUTHENTICATION_REQUIRED, StatusCode::PROXY_AUTHENTICATION_REQUIRED, ]), } } /// Based on the strategy, decide whether to continue searching the next index /// based on the status code returned by this one. pub fn handle_status_code( &self, status_code: StatusCode, index_url: &IndexUrl, capabilities: &IndexCapabilities, ) -> IndexStatusCodeDecision { match self { Self::Default => match status_code { StatusCode::NOT_FOUND => IndexStatusCodeDecision::Ignore, StatusCode::UNAUTHORIZED => { capabilities.set_unauthorized(index_url.clone()); IndexStatusCodeDecision::Fail(status_code) } StatusCode::FORBIDDEN => { capabilities.set_forbidden(index_url.clone()); IndexStatusCodeDecision::Fail(status_code) } _ => IndexStatusCodeDecision::Fail(status_code), }, Self::IgnoreErrorCodes { status_codes } => { if status_codes.contains(&status_code) { IndexStatusCodeDecision::Ignore } else { Self::Default.handle_status_code(status_code, index_url, capabilities) } } } } } /// Decision on whether to continue searching the next index. #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] pub enum IndexStatusCodeDecision { Ignore, Fail(StatusCode), } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SerializableStatusCode(StatusCode); impl Deref for SerializableStatusCode { type Target = StatusCode; fn deref(&self) -> &Self::Target { &self.0 } } impl Serialize for SerializableStatusCode { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_u16(self.0.as_u16()) } } impl<'de> Deserialize<'de> for SerializableStatusCode { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let code = u16::deserialize(deserializer)?; StatusCode::from_u16(code) .map(SerializableStatusCode) .map_err(|_| { serde::de::Error::custom(format!("{code} is not a valid HTTP status code")) }) } } #[cfg(feature = "schemars")] impl schemars::JsonSchema for SerializableStatusCode { fn schema_name() -> Cow<'static, str> { Cow::Borrowed("StatusCode") } fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { schemars::json_schema!({ "type": "number", "minimum": 100, "maximum": 599, "description": "HTTP status code (100-599)" }) } } #[cfg(test)] mod tests { use std::str::FromStr; use url::Url; use super::*; #[test] fn test_strategy_normal_registry() { let url = Url::from_str("https://internal-registry.com/simple").unwrap(); assert_eq!( IndexStatusCodeStrategy::from_index_url(&url), IndexStatusCodeStrategy::Default ); } #[test] fn test_strategy_pytorch_registry() { let status_codes = std::iter::once(StatusCode::FORBIDDEN).collect::>(); let url = Url::from_str("https://download.pytorch.org/whl/cu118").unwrap(); assert_eq!( IndexStatusCodeStrategy::from_index_url(&url), IndexStatusCodeStrategy::IgnoreErrorCodes { status_codes } ); } #[test] fn test_strategy_custom_error_codes() { let status_codes = FxHashSet::from_iter([StatusCode::UNAUTHORIZED, StatusCode::FORBIDDEN]); let serializable_status_codes = status_codes .iter() .map(|code| SerializableStatusCode(*code)) .collect::>(); assert_eq!( IndexStatusCodeStrategy::from_ignored_error_codes(&serializable_status_codes), IndexStatusCodeStrategy::IgnoreErrorCodes { status_codes } ); } #[test] fn test_decision_default_400() { let strategy = IndexStatusCodeStrategy::Default; let status_code = StatusCode::BAD_REQUEST; let index_url = IndexUrl::parse("https://internal-registry.com/simple", None).unwrap(); let capabilities = IndexCapabilities::default(); let decision = strategy.handle_status_code(status_code, &index_url, &capabilities); assert_eq!( decision, IndexStatusCodeDecision::Fail(StatusCode::BAD_REQUEST) ); } #[test] fn test_decision_default_401() { let strategy = IndexStatusCodeStrategy::Default; let status_code = StatusCode::UNAUTHORIZED; let index_url = IndexUrl::parse("https://internal-registry.com/simple", None).unwrap(); let capabilities = IndexCapabilities::default(); let decision = strategy.handle_status_code(status_code, &index_url, &capabilities); assert_eq!( decision, IndexStatusCodeDecision::Fail(StatusCode::UNAUTHORIZED) ); assert!(capabilities.unauthorized(&index_url)); assert!(!capabilities.forbidden(&index_url)); } #[test] fn test_decision_default_403() { let strategy = IndexStatusCodeStrategy::Default; let status_code = StatusCode::FORBIDDEN; let index_url = IndexUrl::parse("https://internal-registry.com/simple", None).unwrap(); let capabilities = IndexCapabilities::default(); let decision = strategy.handle_status_code(status_code, &index_url, &capabilities); assert_eq!( decision, IndexStatusCodeDecision::Fail(StatusCode::FORBIDDEN) ); assert!(capabilities.forbidden(&index_url)); assert!(!capabilities.unauthorized(&index_url)); } #[test] fn test_decision_default_404() { let strategy = IndexStatusCodeStrategy::Default; let status_code = StatusCode::NOT_FOUND; let index_url = IndexUrl::parse("https://internal-registry.com/simple", None).unwrap(); let capabilities = IndexCapabilities::default(); let decision = strategy.handle_status_code(status_code, &index_url, &capabilities); assert_eq!(decision, IndexStatusCodeDecision::Ignore); assert!(!capabilities.forbidden(&index_url)); assert!(!capabilities.unauthorized(&index_url)); } #[test] fn test_decision_pytorch() { let index_url = IndexUrl::parse("https://download.pytorch.org/whl/cu118", None).unwrap(); let strategy = IndexStatusCodeStrategy::from_index_url(&index_url); let capabilities = IndexCapabilities::default(); // Test we continue on 403 for PyTorch registry. let status_code = StatusCode::FORBIDDEN; let decision = strategy.handle_status_code(status_code, &index_url, &capabilities); assert_eq!(decision, IndexStatusCodeDecision::Ignore); // Test we stop on 401 for PyTorch registry. let status_code = StatusCode::UNAUTHORIZED; let decision = strategy.handle_status_code(status_code, &index_url, &capabilities); assert_eq!( decision, IndexStatusCodeDecision::Fail(StatusCode::UNAUTHORIZED) ); } #[test] fn test_decision_multiple_ignored_status_codes() { let status_codes = vec![ StatusCode::UNAUTHORIZED, StatusCode::BAD_GATEWAY, StatusCode::SERVICE_UNAVAILABLE, ]; let strategy = IndexStatusCodeStrategy::IgnoreErrorCodes { status_codes: status_codes.iter().copied().collect::>(), }; let index_url = IndexUrl::parse("https://internal-registry.com/simple", None).unwrap(); let capabilities = IndexCapabilities::default(); // Test each ignored status code for status_code in status_codes { let decision = strategy.handle_status_code(status_code, &index_url, &capabilities); assert_eq!(decision, IndexStatusCodeDecision::Ignore); } // Test a status code that's not ignored let other_status_code = StatusCode::FORBIDDEN; let decision = strategy.handle_status_code(other_status_code, &index_url, &capabilities); assert_eq!( decision, IndexStatusCodeDecision::Fail(StatusCode::FORBIDDEN) ); } } uv-0.9.17+ds1/crates/uv-distribution-types/src/traits.rs000066400000000000000000000215571520155276700232060ustar00rootroot00000000000000use std::borrow::Cow; use uv_normalize::PackageName; use uv_pep508::VerbatimUrl; use crate::error::Error; use crate::{ BuiltDist, CachedDirectUrlDist, CachedDist, CachedRegistryDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist, Dist, DistributionId, GitSourceDist, InstalledDirectUrlDist, InstalledDist, InstalledEggInfoDirectory, InstalledEggInfoFile, InstalledLegacyEditable, InstalledRegistryDist, InstalledVersion, LocalDist, PackageId, PathBuiltDist, PathSourceDist, RegistryBuiltWheel, RegistrySourceDist, ResourceId, SourceDist, VersionId, VersionOrUrlRef, }; pub trait Name { /// Return the normalized [`PackageName`] of the distribution. fn name(&self) -> &PackageName; } /// Metadata that can be resolved from a requirements specification alone (i.e., prior to building /// or installing the distribution). pub trait DistributionMetadata: Name { /// Return a [`uv_pep440::Version`], for registry-based distributions, or a [`url::Url`], /// for URL-based distributions. fn version_or_url(&self) -> VersionOrUrlRef<'_>; /// Returns a unique identifier for the package at the given version (e.g., `black==23.10.0`). /// /// Note that this is not equivalent to a unique identifier for the _distribution_, as multiple /// registry-based distributions (e.g., different wheels for the same package and version) /// will return the same version ID, but different distribution IDs. fn version_id(&self) -> VersionId { match self.version_or_url() { VersionOrUrlRef::Version(version) => { VersionId::from_registry(self.name().clone(), version.clone()) } VersionOrUrlRef::Url(url) => VersionId::from_url(url), } } /// Returns a unique identifier for a package. A package can either be identified by a name /// (e.g., `black`) or a URL (e.g., `git+https://github.com/psf/black`). /// /// Note that this is not equivalent to a unique identifier for the _distribution_, as multiple /// registry-based distributions (e.g., different wheels for the same package and version) /// will return the same version ID, but different distribution IDs. fn package_id(&self) -> PackageId { match self.version_or_url() { VersionOrUrlRef::Version(_) => PackageId::from_registry(self.name().clone()), VersionOrUrlRef::Url(url) => PackageId::from_url(url), } } } /// Metadata that can be resolved from a built distribution. pub trait InstalledMetadata: Name { /// Return the resolved version of the installed distribution. fn installed_version(&self) -> InstalledVersion<'_>; } pub trait RemoteSource { /// Return an appropriate filename for the distribution. fn filename(&self) -> Result, Error>; /// Return the size of the distribution, if known. fn size(&self) -> Option; } pub trait Identifier { /// Return a unique resource identifier for the distribution, like a SHA-256 hash of the /// distribution's contents. /// /// A distribution is a specific archive of a package at a specific version. For a given package /// version, there may be multiple distributions, e.g., source distribution, along with /// multiple binary distributions (wheels) for different platforms. As a concrete example, /// `black-23.10.0-py3-none-any.whl` would represent a (binary) distribution of the `black` package /// at version `23.10.0`. /// /// The distribution ID is used to uniquely identify a distribution. Ideally, the distribution /// ID should be a hash of the distribution's contents, though in practice, it's only required /// that the ID is unique within a single invocation of the resolver (and so, e.g., a hash of /// the URL would also be sufficient). fn distribution_id(&self) -> DistributionId; /// Return a unique resource identifier for the underlying resource backing the distribution. /// /// This is often equivalent to the distribution ID, but may differ in some cases. For example, /// if the same Git repository is used for two different distributions, at two different /// subdirectories or two different commits, then those distributions would share a resource ID, /// but have different distribution IDs. fn resource_id(&self) -> ResourceId; } pub trait Verbatim { /// Return the verbatim representation of the distribution. fn verbatim(&self) -> Cow<'_, str>; } impl Verbatim for VerbatimUrl { fn verbatim(&self) -> Cow<'_, str> { if let Some(given) = self.given() { Cow::Borrowed(given) } else { Cow::Owned(self.to_string()) } } } impl Verbatim for T { fn verbatim(&self) -> Cow<'_, str> { Cow::Owned(format!( "{}{}", self.name(), self.version_or_url().verbatim() )) } } // Implement `Display` for all known types that implement `Metadata`. impl std::fmt::Display for LocalDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.installed_version()) } } impl std::fmt::Display for BuiltDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.version_or_url()) } } impl std::fmt::Display for CachedDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.installed_version()) } } impl std::fmt::Display for CachedDirectUrlDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.installed_version()) } } impl std::fmt::Display for CachedRegistryDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.installed_version()) } } impl std::fmt::Display for DirectUrlBuiltDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.version_or_url()) } } impl std::fmt::Display for DirectUrlSourceDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.version_or_url()) } } impl std::fmt::Display for Dist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.version_or_url()) } } impl std::fmt::Display for GitSourceDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.version_or_url()) } } impl std::fmt::Display for InstalledDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.installed_version()) } } impl std::fmt::Display for InstalledDirectUrlDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.installed_version()) } } impl std::fmt::Display for InstalledRegistryDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.installed_version()) } } impl std::fmt::Display for InstalledEggInfoFile { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.installed_version()) } } impl std::fmt::Display for InstalledEggInfoDirectory { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.installed_version()) } } impl std::fmt::Display for InstalledLegacyEditable { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.installed_version()) } } impl std::fmt::Display for PathBuiltDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.version_or_url()) } } impl std::fmt::Display for PathSourceDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.version_or_url()) } } impl std::fmt::Display for DirectorySourceDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.version_or_url()) } } impl std::fmt::Display for RegistryBuiltWheel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.version_or_url()) } } impl std::fmt::Display for RegistrySourceDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.version_or_url()) } } impl std::fmt::Display for SourceDist { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.version_or_url()) } } uv-0.9.17+ds1/crates/uv-distribution/000077500000000000000000000000001520155276700173675ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-distribution/Cargo.toml000066400000000000000000000036301520155276700213210ustar00rootroot00000000000000[package] name = "uv-distribution" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-auth = { workspace = true } uv-cache = { workspace = true } uv-cache-info = { workspace = true } uv-client = { workspace = true } uv-configuration = { workspace = true } uv-distribution-filename = { workspace = true } uv-distribution-types = { workspace = true } uv-extract = { workspace = true } uv-flags = { workspace = true } uv-fs = { workspace = true, features = ["tokio"] } uv-git = { workspace = true } uv-git-types = { workspace = true } uv-metadata = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } uv-redacted = { workspace = true } uv-types = { workspace = true } uv-workspace = { workspace = true } anyhow = { workspace = true } either = { workspace = true } fs-err = { workspace = true } futures = { workspace = true } nanoid = { workspace = true } owo-colors = { workspace = true } reqwest = { workspace = true } reqwest-middleware = { workspace = true } rmp-serde = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"] } tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true, features = ["compat"] } toml = { workspace = true } tracing = { workspace = true } url = { workspace = true } walkdir = { workspace = true } zip = { workspace = true } [dev-dependencies] indoc = { workspace = true } insta = { workspace = true } [features] default = [] static = ["uv-extract/static"] uv-0.9.17+ds1/crates/uv-distribution/README.md000066400000000000000000000010411520155276700206420ustar00rootroot00000000000000 # uv-distribution This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-distribution). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-distribution/src/000077500000000000000000000000001520155276700201565ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-distribution/src/archive.rs000066400000000000000000000023131520155276700221440ustar00rootroot00000000000000use uv_cache::{ARCHIVE_VERSION, ArchiveId, Cache}; use uv_distribution_filename::WheelFilename; use uv_distribution_types::Hashed; use uv_pypi_types::{HashDigest, HashDigests}; /// An archive (unzipped wheel) that exists in the local cache. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Archive { /// The unique ID of the entry in the wheel's archive bucket. pub id: ArchiveId, /// The computed hashes of the archive. pub hashes: HashDigests, /// The filename of the wheel. pub filename: WheelFilename, /// The version of the archive bucket. pub version: u8, } impl Archive { /// Create a new [`Archive`] with the given ID and hashes. pub(crate) fn new(id: ArchiveId, hashes: HashDigests, filename: WheelFilename) -> Self { Self { id, hashes, filename, version: ARCHIVE_VERSION, } } /// Returns `true` if the archive exists in the cache. pub(crate) fn exists(&self, cache: &Cache) -> bool { self.version == ARCHIVE_VERSION && cache.archive(&self.id).exists() } } impl Hashed for Archive { fn hashes(&self) -> &[HashDigest] { self.hashes.as_slice() } } uv-0.9.17+ds1/crates/uv-distribution/src/distribution_database.rs000066400000000000000000001507371520155276700251040ustar00rootroot00000000000000use std::future::Future; use std::io; use std::path::Path; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; use futures::{FutureExt, TryStreamExt}; use tempfile::TempDir; use tokio::io::{AsyncRead, AsyncSeekExt, ReadBuf}; use tokio::sync::Semaphore; use tokio_util::compat::FuturesAsyncReadCompatExt; use tracing::{Instrument, info_span, instrument, warn}; use url::Url; use uv_cache::{ArchiveId, CacheBucket, CacheEntry, WheelCache}; use uv_cache_info::{CacheInfo, Timestamp}; use uv_client::{ CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient, }; use uv_distribution_filename::WheelFilename; use uv_distribution_types::{ BuildInfo, BuildableSource, BuiltDist, Dist, File, HashPolicy, Hashed, IndexUrl, InstalledDist, Name, SourceDist, ToUrlError, }; use uv_extract::hash::Hasher; use uv_fs::write_atomic; use uv_platform_tags::Tags; use uv_pypi_types::{HashDigest, HashDigests, PyProjectToml}; use uv_redacted::DisplaySafeUrl; use uv_types::{BuildContext, BuildStack}; use crate::archive::Archive; use crate::metadata::{ArchiveMetadata, Metadata}; use crate::source::SourceDistributionBuilder; use crate::{Error, LocalWheel, Reporter, RequiresDist}; /// A cached high-level interface to convert distributions (a requirement resolved to a location) /// to a wheel or wheel metadata. /// /// For wheel metadata, this happens by either fetching the metadata from the remote wheel or by /// building the source distribution. For wheel files, either the wheel is downloaded or a source /// distribution is downloaded, built and the new wheel gets returned. /// /// All kinds of wheel sources (index, URL, path) and source distribution source (index, URL, path, /// Git) are supported. /// /// This struct also has the task of acquiring locks around source dist builds in general and git /// operation especially, as well as respecting concurrency limits. pub struct DistributionDatabase<'a, Context: BuildContext> { build_context: &'a Context, builder: SourceDistributionBuilder<'a, Context>, client: ManagedClient<'a>, reporter: Option>, } impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { pub fn new( client: &'a RegistryClient, build_context: &'a Context, concurrent_downloads: usize, ) -> Self { Self { build_context, builder: SourceDistributionBuilder::new(build_context), client: ManagedClient::new(client, concurrent_downloads), reporter: None, } } /// Set the build stack to use for the [`DistributionDatabase`]. #[must_use] pub fn with_build_stack(self, build_stack: &'a BuildStack) -> Self { Self { builder: self.builder.with_build_stack(build_stack), ..self } } /// Set the [`Reporter`] to use for the [`DistributionDatabase`]. #[must_use] pub fn with_reporter(self, reporter: Arc) -> Self { Self { builder: self.builder.with_reporter(reporter.clone()), reporter: Some(reporter), ..self } } /// Handle a specific `reqwest` error, and convert it to [`io::Error`]. fn handle_response_errors(&self, err: reqwest::Error) -> io::Error { if err.is_timeout() { io::Error::new( io::ErrorKind::TimedOut, format!( "Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: {}s).", self.client.unmanaged.timeout().as_secs() ), ) } else { io::Error::other(err) } } /// Either fetch the wheel or fetch and build the source distribution /// /// Returns a wheel that's compliant with the given platform tags. /// /// While hashes will be generated in some cases, hash-checking is only enforced for source /// distributions, and should be enforced by the caller for wheels. #[instrument(skip_all, fields(%dist))] pub async fn get_or_build_wheel( &self, dist: &Dist, tags: &Tags, hashes: HashPolicy<'_>, ) -> Result { match dist { Dist::Built(built) => self.get_wheel(built, hashes).await, Dist::Source(source) => self.build_wheel(source, tags, hashes).await, } } /// Either fetch the only wheel metadata (directly from the index or with range requests) or /// fetch and build the source distribution. /// /// While hashes will be generated in some cases, hash-checking is only enforced for source /// distributions, and should be enforced by the caller for wheels. #[instrument(skip_all, fields(%dist))] pub async fn get_installed_metadata( &self, dist: &InstalledDist, ) -> Result { // If the metadata was provided by the user directly, prefer it. if let Some(metadata) = self .build_context .dependency_metadata() .get(dist.name(), Some(dist.version())) { return Ok(ArchiveMetadata::from_metadata23(metadata.clone())); } let metadata = dist .read_metadata() .map_err(|err| Error::ReadInstalled(Box::new(dist.clone()), err))?; Ok(ArchiveMetadata::from_metadata23(metadata.clone())) } /// Either fetch the only wheel metadata (directly from the index or with range requests) or /// fetch and build the source distribution. /// /// While hashes will be generated in some cases, hash-checking is only enforced for source /// distributions, and should be enforced by the caller for wheels. #[instrument(skip_all, fields(%dist))] pub async fn get_or_build_wheel_metadata( &self, dist: &Dist, hashes: HashPolicy<'_>, ) -> Result { match dist { Dist::Built(built) => self.get_wheel_metadata(built, hashes).await, Dist::Source(source) => { self.build_wheel_metadata(&BuildableSource::Dist(source), hashes) .await } } } /// Fetch a wheel from the cache or download it from the index. /// /// While hashes will be generated in all cases, hash-checking is _not_ enforced and should /// instead be enforced by the caller. async fn get_wheel( &self, dist: &BuiltDist, hashes: HashPolicy<'_>, ) -> Result { match dist { BuiltDist::Registry(wheels) => { let wheel = wheels.best_wheel(); let WheelTarget { url, extension, size, } = WheelTarget::try_from(&*wheel.file)?; // Create a cache entry for the wheel. let wheel_entry = self.build_context.cache().entry( CacheBucket::Wheels, WheelCache::Index(&wheel.index).wheel_dir(wheel.name().as_ref()), wheel.filename.cache_key(), ); // If the URL is a file URL, load the wheel directly. if url.scheme() == "file" { let path = url .to_file_path() .map_err(|()| Error::NonFileUrl(url.clone()))?; return self .load_wheel( &path, &wheel.filename, WheelExtension::Whl, wheel_entry, dist, hashes, ) .await; } // Download and unzip. match self .stream_wheel( url.clone(), dist.index(), &wheel.filename, extension, size, &wheel_entry, dist, hashes, ) .await { Ok(archive) => Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive: self .build_context .cache() .archive(&archive.id) .into_boxed_path(), hashes: archive.hashes, filename: wheel.filename.clone(), cache: CacheInfo::default(), build: None, }), Err(Error::Extract(name, err)) => { if err.is_http_streaming_unsupported() { warn!( "Streaming unsupported for {dist}; downloading wheel to disk ({err})" ); } else if err.is_http_streaming_failed() { warn!("Streaming failed for {dist}; downloading wheel to disk ({err})"); } else { return Err(Error::Extract(name, err)); } // If the request failed because streaming is unsupported, download the // wheel directly. let archive = self .download_wheel( url, dist.index(), &wheel.filename, extension, size, &wheel_entry, dist, hashes, ) .await?; Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive: self .build_context .cache() .archive(&archive.id) .into_boxed_path(), hashes: archive.hashes, filename: wheel.filename.clone(), cache: CacheInfo::default(), build: None, }) } Err(err) => Err(err), } } BuiltDist::DirectUrl(wheel) => { // Create a cache entry for the wheel. let wheel_entry = self.build_context.cache().entry( CacheBucket::Wheels, WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()), wheel.filename.cache_key(), ); // Download and unzip. match self .stream_wheel( wheel.url.raw().clone(), None, &wheel.filename, WheelExtension::Whl, None, &wheel_entry, dist, hashes, ) .await { Ok(archive) => Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive: self .build_context .cache() .archive(&archive.id) .into_boxed_path(), hashes: archive.hashes, filename: wheel.filename.clone(), cache: CacheInfo::default(), build: None, }), Err(Error::Client(err)) if err.is_http_streaming_unsupported() => { warn!( "Streaming unsupported for {dist}; downloading wheel to disk ({err})" ); // If the request failed because streaming is unsupported, download the // wheel directly. let archive = self .download_wheel( wheel.url.raw().clone(), None, &wheel.filename, WheelExtension::Whl, None, &wheel_entry, dist, hashes, ) .await?; Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive: self .build_context .cache() .archive(&archive.id) .into_boxed_path(), hashes: archive.hashes, filename: wheel.filename.clone(), cache: CacheInfo::default(), build: None, }) } Err(err) => Err(err), } } BuiltDist::Path(wheel) => { let cache_entry = self.build_context.cache().entry( CacheBucket::Wheels, WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()), wheel.filename.cache_key(), ); self.load_wheel( &wheel.install_path, &wheel.filename, WheelExtension::Whl, cache_entry, dist, hashes, ) .await } } } /// Convert a source distribution into a wheel, fetching it from the cache or building it if /// necessary. /// /// The returned wheel is guaranteed to come from a distribution with a matching hash, and /// no build processes will be executed for distributions with mismatched hashes. async fn build_wheel( &self, dist: &SourceDist, tags: &Tags, hashes: HashPolicy<'_>, ) -> Result { let built_wheel = self .builder .download_and_build(&BuildableSource::Dist(dist), tags, hashes, &self.client) .boxed_local() .await?; // Check that the wheel is compatible with its install target. // // When building a build dependency for a cross-install, the build dependency needs // to install and run on the host instead of the target. In this case the `tags` are already // for the host instead of the target, so this check passes. if !built_wheel.filename.is_compatible(tags) { return if tags.is_cross() { Err(Error::BuiltWheelIncompatibleTargetPlatform { filename: built_wheel.filename, python_platform: tags.python_platform().clone(), python_version: tags.python_version(), }) } else { Err(Error::BuiltWheelIncompatibleHostPlatform { filename: built_wheel.filename, python_platform: tags.python_platform().clone(), python_version: tags.python_version(), }) }; } // Acquire the advisory lock. #[cfg(windows)] let _lock = { let lock_entry = CacheEntry::new( built_wheel.target.parent().unwrap(), format!( "{}.lock", built_wheel.target.file_name().unwrap().to_str().unwrap() ), ); lock_entry.lock().await.map_err(Error::CacheLock)? }; // If the wheel was unzipped previously, respect it. Source distributions are // cached under a unique revision ID, so unzipped directories are never stale. match self.build_context.cache().resolve_link(&built_wheel.target) { Ok(archive) => { return Ok(LocalWheel { dist: Dist::Source(dist.clone()), archive: archive.into_boxed_path(), filename: built_wheel.filename, hashes: built_wheel.hashes, cache: built_wheel.cache_info, build: Some(built_wheel.build_info), }); } Err(err) if err.kind() == io::ErrorKind::NotFound => {} Err(err) => return Err(Error::CacheRead(err)), } // Otherwise, unzip the wheel. let id = self .unzip_wheel(&built_wheel.path, &built_wheel.target) .await?; Ok(LocalWheel { dist: Dist::Source(dist.clone()), archive: self.build_context.cache().archive(&id).into_boxed_path(), hashes: built_wheel.hashes, filename: built_wheel.filename, cache: built_wheel.cache_info, build: Some(built_wheel.build_info), }) } /// Fetch the wheel metadata from the index, or from the cache if possible. /// /// While hashes will be generated in some cases, hash-checking is _not_ enforced and should /// instead be enforced by the caller. async fn get_wheel_metadata( &self, dist: &BuiltDist, hashes: HashPolicy<'_>, ) -> Result { // If hash generation is enabled, and the distribution isn't hosted on a registry, get the // entire wheel to ensure that the hashes are included in the response. If the distribution // is hosted on an index, the hashes will be included in the simple metadata response. // For hash _validation_, callers are expected to enforce the policy when retrieving the // wheel. // // Historically, for `uv pip compile --universal`, we also generate hashes for // registry-based distributions when the relevant registry doesn't provide them. This was // motivated by `--find-links`. We continue that behavior (under `HashGeneration::All`) for // backwards compatibility, but it's a little dubious, since we're only hashing _one_ // distribution here (as opposed to hashing all distributions for the version), and it may // not even be a compatible distribution! // // TODO(charlie): Request the hashes via a separate method, to reduce the coupling in this API. if hashes.is_generate(dist) { let wheel = self.get_wheel(dist, hashes).await?; // If the metadata was provided by the user directly, prefer it. let metadata = if let Some(metadata) = self .build_context .dependency_metadata() .get(dist.name(), Some(dist.version())) { metadata.clone() } else { wheel.metadata()? }; let hashes = wheel.hashes; return Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata), hashes, }); } // If the metadata was provided by the user directly, prefer it. if let Some(metadata) = self .build_context .dependency_metadata() .get(dist.name(), Some(dist.version())) { return Ok(ArchiveMetadata::from_metadata23(metadata.clone())); } let result = self .client .managed(|client| { client .wheel_metadata(dist, self.build_context.capabilities()) .boxed_local() }) .await; match result { Ok(metadata) => { // Validate that the metadata is consistent with the distribution. Ok(ArchiveMetadata::from_metadata23(metadata)) } Err(err) if err.is_http_streaming_unsupported() => { warn!( "Streaming unsupported when fetching metadata for {dist}; downloading wheel directly ({err})" ); // If the request failed due to an error that could be resolved by // downloading the wheel directly, try that. let wheel = self.get_wheel(dist, hashes).await?; let metadata = wheel.metadata()?; let hashes = wheel.hashes; Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata), hashes, }) } Err(err) => Err(err.into()), } } /// Build the wheel metadata for a source distribution, or fetch it from the cache if possible. /// /// The returned metadata is guaranteed to come from a distribution with a matching hash, and /// no build processes will be executed for distributions with mismatched hashes. pub async fn build_wheel_metadata( &self, source: &BuildableSource<'_>, hashes: HashPolicy<'_>, ) -> Result { // If the metadata was provided by the user directly, prefer it. if let Some(dist) = source.as_dist() { if let Some(metadata) = self .build_context .dependency_metadata() .get(dist.name(), dist.version()) { // If we skipped the build, we should still resolve any Git dependencies to precise // commits. self.builder.resolve_revision(source, &self.client).await?; return Ok(ArchiveMetadata::from_metadata23(metadata.clone())); } } let metadata = self .builder .download_and_build_metadata(source, hashes, &self.client) .boxed_local() .await?; Ok(metadata) } /// Return the [`RequiresDist`] from a `pyproject.toml`, if it can be statically extracted. pub async fn requires_dist( &self, path: &Path, pyproject_toml: &PyProjectToml, ) -> Result, Error> { self.builder .source_tree_requires_dist( path, pyproject_toml, self.client.unmanaged.credentials_cache(), ) .await } /// Stream a wheel from a URL, unzipping it into the cache as it's downloaded. async fn stream_wheel( &self, url: DisplaySafeUrl, index: Option<&IndexUrl>, filename: &WheelFilename, extension: WheelExtension, size: Option, wheel_entry: &CacheEntry, dist: &BuiltDist, hashes: HashPolicy<'_>, ) -> Result { // Acquire an advisory lock, to guard against concurrent writes. #[cfg(windows)] let _lock = { let lock_entry = wheel_entry.with_file(format!("{}.lock", filename.stem())); lock_entry.lock().await.map_err(Error::CacheLock)? }; // Create an entry for the HTTP cache. let http_entry = wheel_entry.with_file(format!("{}.http", filename.cache_key())); let download = |response: reqwest::Response| { async { let size = size.or_else(|| content_length(&response)); let progress = self .reporter .as_ref() .map(|reporter| (reporter, reporter.on_download_start(dist.name(), size))); let reader = response .bytes_stream() .map_err(|err| self.handle_response_errors(err)) .into_async_read(); // Create a hasher for each hash algorithm. let algorithms = hashes.algorithms(); let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers); // Download and unzip the wheel to a temporary directory. let temp_dir = tempfile::tempdir_in(self.build_context.cache().root()) .map_err(Error::CacheWrite)?; match progress { Some((reporter, progress)) => { let mut reader = ProgressReader::new(&mut hasher, progress, &**reporter); match extension { WheelExtension::Whl => { uv_extract::stream::unzip(&mut reader, temp_dir.path()) .await .map_err(|err| Error::Extract(filename.to_string(), err))?; } WheelExtension::WhlZst => { uv_extract::stream::untar_zst(&mut reader, temp_dir.path()) .await .map_err(|err| Error::Extract(filename.to_string(), err))?; } } } None => match extension { WheelExtension::Whl => { uv_extract::stream::unzip(&mut hasher, temp_dir.path()) .await .map_err(|err| Error::Extract(filename.to_string(), err))?; } WheelExtension::WhlZst => { uv_extract::stream::untar_zst(&mut hasher, temp_dir.path()) .await .map_err(|err| Error::Extract(filename.to_string(), err))?; } }, } // If necessary, exhaust the reader to compute the hash. if !hashes.is_none() { hasher.finish().await.map_err(Error::HashExhaustion)?; } // Persist the temporary directory to the directory store. let id = self .build_context .cache() .persist(temp_dir.keep(), wheel_entry.path()) .await .map_err(Error::CacheRead)?; if let Some((reporter, progress)) = progress { reporter.on_download_complete(dist.name(), progress); } Ok(Archive::new( id, hashers.into_iter().map(HashDigest::from).collect(), filename.clone(), )) } .instrument(info_span!("wheel", wheel = %dist)) }; // Fetch the archive from the cache, or download it if necessary. let req = self.request(url.clone())?; // Determine the cache control policy for the URL. let cache_control = match self.client.unmanaged.connectivity() { Connectivity::Online => { if let Some(header) = index.and_then(|index| { self.build_context .locations() .artifact_cache_control_for(index) }) { CacheControl::Override(header) } else { CacheControl::from( self.build_context .cache() .freshness(&http_entry, Some(&filename.name), None) .map_err(Error::CacheRead)?, ) } } Connectivity::Offline => CacheControl::AllowStale, }; let archive = self .client .managed(|client| { client.cached_client().get_serde_with_retry( req, &http_entry, cache_control, download, ) }) .await .map_err(|err| match err { CachedClientError::Callback { err, .. } => err, CachedClientError::Client { err, .. } => Error::Client(err), })?; // If the archive is missing the required hashes, or has since been removed, force a refresh. let archive = Some(archive) .filter(|archive| archive.has_digests(hashes)) .filter(|archive| archive.exists(self.build_context.cache())); let archive = if let Some(archive) = archive { archive } else { self.client .managed(async |client| { client .cached_client() .skip_cache_with_retry( self.request(url)?, &http_entry, cache_control, download, ) .await .map_err(|err| match err { CachedClientError::Callback { err, .. } => err, CachedClientError::Client { err, .. } => Error::Client(err), }) }) .await? }; Ok(archive) } /// Download a wheel from a URL, then unzip it into the cache. async fn download_wheel( &self, url: DisplaySafeUrl, index: Option<&IndexUrl>, filename: &WheelFilename, extension: WheelExtension, size: Option, wheel_entry: &CacheEntry, dist: &BuiltDist, hashes: HashPolicy<'_>, ) -> Result { // Acquire an advisory lock, to guard against concurrent writes. #[cfg(windows)] let _lock = { let lock_entry = wheel_entry.with_file(format!("{}.lock", filename.stem())); lock_entry.lock().await.map_err(Error::CacheLock)? }; // Create an entry for the HTTP cache. let http_entry = wheel_entry.with_file(format!("{}.http", filename.cache_key())); let download = |response: reqwest::Response| { async { let size = size.or_else(|| content_length(&response)); let progress = self .reporter .as_ref() .map(|reporter| (reporter, reporter.on_download_start(dist.name(), size))); let reader = response .bytes_stream() .map_err(|err| self.handle_response_errors(err)) .into_async_read(); // Download the wheel to a temporary file. let temp_file = tempfile::tempfile_in(self.build_context.cache().root()) .map_err(Error::CacheWrite)?; let mut writer = tokio::io::BufWriter::new(fs_err::tokio::File::from_std( // It's an unnamed file on Linux so that's the best approximation. fs_err::File::from_parts(temp_file, self.build_context.cache().root()), )); match progress { Some((reporter, progress)) => { // Wrap the reader in a progress reporter. This will report 100% progress // after the download is complete, even if we still have to unzip and hash // part of the file. let mut reader = ProgressReader::new(reader.compat(), progress, &**reporter); tokio::io::copy(&mut reader, &mut writer) .await .map_err(Error::CacheWrite)?; } None => { tokio::io::copy(&mut reader.compat(), &mut writer) .await .map_err(Error::CacheWrite)?; } } // Unzip the wheel to a temporary directory. let temp_dir = tempfile::tempdir_in(self.build_context.cache().root()) .map_err(Error::CacheWrite)?; let mut file = writer.into_inner(); file.seek(io::SeekFrom::Start(0)) .await .map_err(Error::CacheWrite)?; // If no hashes are required, parallelize the unzip operation. let hashes = if hashes.is_none() { let file = file.into_std().await; tokio::task::spawn_blocking({ let target = temp_dir.path().to_owned(); move || -> Result<(), uv_extract::Error> { // Unzip the wheel into a temporary directory. match extension { WheelExtension::Whl => { uv_extract::unzip(file, &target)?; } WheelExtension::WhlZst => { uv_extract::stream::untar_zst_file(file, &target)?; } } Ok(()) } }) .await? .map_err(|err| Error::Extract(filename.to_string(), err))?; HashDigests::empty() } else { // Create a hasher for each hash algorithm. let algorithms = hashes.algorithms(); let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); let mut hasher = uv_extract::hash::HashReader::new(file, &mut hashers); match extension { WheelExtension::Whl => { uv_extract::stream::unzip(&mut hasher, temp_dir.path()) .await .map_err(|err| Error::Extract(filename.to_string(), err))?; } WheelExtension::WhlZst => { uv_extract::stream::untar_zst(&mut hasher, temp_dir.path()) .await .map_err(|err| Error::Extract(filename.to_string(), err))?; } } // If necessary, exhaust the reader to compute the hash. hasher.finish().await.map_err(Error::HashExhaustion)?; hashers.into_iter().map(HashDigest::from).collect() }; // Persist the temporary directory to the directory store. let id = self .build_context .cache() .persist(temp_dir.keep(), wheel_entry.path()) .await .map_err(Error::CacheRead)?; if let Some((reporter, progress)) = progress { reporter.on_download_complete(dist.name(), progress); } Ok(Archive::new(id, hashes, filename.clone())) } .instrument(info_span!("wheel", wheel = %dist)) }; // Fetch the archive from the cache, or download it if necessary. let req = self.request(url.clone())?; // Determine the cache control policy for the URL. let cache_control = match self.client.unmanaged.connectivity() { Connectivity::Online => { if let Some(header) = index.and_then(|index| { self.build_context .locations() .artifact_cache_control_for(index) }) { CacheControl::Override(header) } else { CacheControl::from( self.build_context .cache() .freshness(&http_entry, Some(&filename.name), None) .map_err(Error::CacheRead)?, ) } } Connectivity::Offline => CacheControl::AllowStale, }; let archive = self .client .managed(|client| { client.cached_client().get_serde_with_retry( req, &http_entry, cache_control, download, ) }) .await .map_err(|err| match err { CachedClientError::Callback { err, .. } => err, CachedClientError::Client { err, .. } => Error::Client(err), })?; // If the archive is missing the required hashes, or has since been removed, force a refresh. let archive = Some(archive) .filter(|archive| archive.has_digests(hashes)) .filter(|archive| archive.exists(self.build_context.cache())); let archive = if let Some(archive) = archive { archive } else { self.client .managed(async |client| { client .cached_client() .skip_cache_with_retry( self.request(url)?, &http_entry, cache_control, download, ) .await .map_err(|err| match err { CachedClientError::Callback { err, .. } => err, CachedClientError::Client { err, .. } => Error::Client(err), }) }) .await? }; Ok(archive) } /// Load a wheel from a local path. async fn load_wheel( &self, path: &Path, filename: &WheelFilename, extension: WheelExtension, wheel_entry: CacheEntry, dist: &BuiltDist, hashes: HashPolicy<'_>, ) -> Result { #[cfg(windows)] let _lock = { let lock_entry = wheel_entry.with_file(format!("{}.lock", filename.stem())); lock_entry.lock().await.map_err(Error::CacheLock)? }; // Determine the last-modified time of the wheel. let modified = Timestamp::from_path(path).map_err(Error::CacheRead)?; // Attempt to read the archive pointer from the cache. let pointer_entry = wheel_entry.with_file(format!("{}.rev", filename.cache_key())); let pointer = LocalArchivePointer::read_from(&pointer_entry)?; // Extract the archive from the pointer. let archive = pointer .filter(|pointer| pointer.is_up_to_date(modified)) .map(LocalArchivePointer::into_archive) .filter(|archive| archive.has_digests(hashes)); // If the file is already unzipped, and the cache is up-to-date, return it. if let Some(archive) = archive { Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive: self .build_context .cache() .archive(&archive.id) .into_boxed_path(), hashes: archive.hashes, filename: filename.clone(), cache: CacheInfo::from_timestamp(modified), build: None, }) } else if hashes.is_none() { // Otherwise, unzip the wheel. let archive = Archive::new( self.unzip_wheel(path, wheel_entry.path()).await?, HashDigests::empty(), filename.clone(), ); // Write the archive pointer to the cache. let pointer = LocalArchivePointer { timestamp: modified, archive: archive.clone(), }; pointer.write_to(&pointer_entry).await?; Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive: self .build_context .cache() .archive(&archive.id) .into_boxed_path(), hashes: archive.hashes, filename: filename.clone(), cache: CacheInfo::from_timestamp(modified), build: None, }) } else { // If necessary, compute the hashes of the wheel. let file = fs_err::tokio::File::open(path) .await .map_err(Error::CacheRead)?; let temp_dir = tempfile::tempdir_in(self.build_context.cache().root()) .map_err(Error::CacheWrite)?; // Create a hasher for each hash algorithm. let algorithms = hashes.algorithms(); let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); let mut hasher = uv_extract::hash::HashReader::new(file, &mut hashers); // Unzip the wheel to a temporary directory. match extension { WheelExtension::Whl => { uv_extract::stream::unzip(&mut hasher, temp_dir.path()) .await .map_err(|err| Error::Extract(filename.to_string(), err))?; } WheelExtension::WhlZst => { uv_extract::stream::untar_zst(&mut hasher, temp_dir.path()) .await .map_err(|err| Error::Extract(filename.to_string(), err))?; } } // Exhaust the reader to compute the hash. hasher.finish().await.map_err(Error::HashExhaustion)?; let hashes = hashers.into_iter().map(HashDigest::from).collect(); // Persist the temporary directory to the directory store. let id = self .build_context .cache() .persist(temp_dir.keep(), wheel_entry.path()) .await .map_err(Error::CacheWrite)?; // Create an archive. let archive = Archive::new(id, hashes, filename.clone()); // Write the archive pointer to the cache. let pointer = LocalArchivePointer { timestamp: modified, archive: archive.clone(), }; pointer.write_to(&pointer_entry).await?; Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive: self .build_context .cache() .archive(&archive.id) .into_boxed_path(), hashes: archive.hashes, filename: filename.clone(), cache: CacheInfo::from_timestamp(modified), build: None, }) } } /// Unzip a wheel into the cache, returning the path to the unzipped directory. async fn unzip_wheel(&self, path: &Path, target: &Path) -> Result { let temp_dir = tokio::task::spawn_blocking({ let path = path.to_owned(); let root = self.build_context.cache().root().to_path_buf(); move || -> Result { // Unzip the wheel into a temporary directory. let temp_dir = tempfile::tempdir_in(root).map_err(Error::CacheWrite)?; let reader = fs_err::File::open(&path).map_err(Error::CacheWrite)?; uv_extract::unzip(reader, temp_dir.path()) .map_err(|err| Error::Extract(path.to_string_lossy().into_owned(), err))?; Ok(temp_dir) } }) .await??; // Persist the temporary directory to the directory store. let id = self .build_context .cache() .persist(temp_dir.keep(), target) .await .map_err(Error::CacheWrite)?; Ok(id) } /// Returns a GET [`reqwest::Request`] for the given URL. fn request(&self, url: DisplaySafeUrl) -> Result { self.client .unmanaged .uncached_client(&url) .get(Url::from(url)) .header( // `reqwest` defaults to accepting compressed responses. // Specify identity encoding to get consistent .whl downloading // behavior from servers. ref: https://github.com/pypa/pip/pull/1688 "accept-encoding", reqwest::header::HeaderValue::from_static("identity"), ) .build() } /// Return the [`ManagedClient`] used by this resolver. pub fn client(&self) -> &ManagedClient<'a> { &self.client } } /// A wrapper around `RegistryClient` that manages a concurrency limit. pub struct ManagedClient<'a> { pub unmanaged: &'a RegistryClient, control: Semaphore, } impl<'a> ManagedClient<'a> { /// Create a new `ManagedClient` using the given client and concurrency limit. fn new(client: &'a RegistryClient, concurrency: usize) -> Self { ManagedClient { unmanaged: client, control: Semaphore::new(concurrency), } } /// Perform a request using the client, respecting the concurrency limit. /// /// If the concurrency limit has been reached, this method will wait until a pending /// operation completes before executing the closure. pub async fn managed(&self, f: impl FnOnce(&'a RegistryClient) -> F) -> T where F: Future, { let _permit = self.control.acquire().await.unwrap(); f(self.unmanaged).await } /// Perform a request using a client that internally manages the concurrency limit. /// /// The callback is passed the client and a semaphore. It must acquire the semaphore before /// any request through the client and drop it after. /// /// This method serves as an escape hatch for functions that may want to send multiple requests /// in parallel. pub async fn manual(&'a self, f: impl FnOnce(&'a RegistryClient, &'a Semaphore) -> F) -> T where F: Future, { f(self.unmanaged, &self.control).await } } /// Returns the value of the `Content-Length` header from the [`reqwest::Response`], if present. fn content_length(response: &reqwest::Response) -> Option { response .headers() .get(reqwest::header::CONTENT_LENGTH) .and_then(|val| val.to_str().ok()) .and_then(|val| val.parse::().ok()) } /// An asynchronous reader that reports progress as bytes are read. struct ProgressReader<'a, R> { reader: R, index: usize, reporter: &'a dyn Reporter, } impl<'a, R> ProgressReader<'a, R> { /// Create a new [`ProgressReader`] that wraps another reader. fn new(reader: R, index: usize, reporter: &'a dyn Reporter) -> Self { Self { reader, index, reporter, } } } impl AsyncRead for ProgressReader<'_, R> where R: AsyncRead + Unpin, { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { Pin::new(&mut self.as_mut().reader) .poll_read(cx, buf) .map_ok(|()| { self.reporter .on_download_progress(self.index, buf.filled().len() as u64); }) } } /// A pointer to an archive in the cache, fetched from an HTTP archive. /// /// Encoded with `MsgPack`, and represented on disk by a `.http` file. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct HttpArchivePointer { archive: Archive, } impl HttpArchivePointer { /// Read an [`HttpArchivePointer`] from the cache. pub fn read_from(path: impl AsRef) -> Result, Error> { match fs_err::File::open(path.as_ref()) { Ok(file) => { let data = DataWithCachePolicy::from_reader(file)?.data; let archive = rmp_serde::from_slice::(&data)?; Ok(Some(Self { archive })) } Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None), Err(err) => Err(Error::CacheRead(err)), } } /// Return the [`Archive`] from the pointer. pub fn into_archive(self) -> Archive { self.archive } /// Return the [`CacheInfo`] from the pointer. pub fn to_cache_info(&self) -> CacheInfo { CacheInfo::default() } /// Return the [`BuildInfo`] from the pointer. pub fn to_build_info(&self) -> Option { None } } /// A pointer to an archive in the cache, fetched from a local path. /// /// Encoded with `MsgPack`, and represented on disk by a `.rev` file. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct LocalArchivePointer { timestamp: Timestamp, archive: Archive, } impl LocalArchivePointer { /// Read an [`LocalArchivePointer`] from the cache. pub fn read_from(path: impl AsRef) -> Result, Error> { match fs_err::read(path) { Ok(cached) => Ok(Some(rmp_serde::from_slice::(&cached)?)), Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None), Err(err) => Err(Error::CacheRead(err)), } } /// Write an [`LocalArchivePointer`] to the cache. pub async fn write_to(&self, entry: &CacheEntry) -> Result<(), Error> { write_atomic(entry.path(), rmp_serde::to_vec(&self)?) .await .map_err(Error::CacheWrite) } /// Returns `true` if the archive is up-to-date with the given modified timestamp. pub fn is_up_to_date(&self, modified: Timestamp) -> bool { self.timestamp == modified } /// Return the [`Archive`] from the pointer. pub fn into_archive(self) -> Archive { self.archive } /// Return the [`CacheInfo`] from the pointer. pub fn to_cache_info(&self) -> CacheInfo { CacheInfo::from_timestamp(self.timestamp) } /// Return the [`BuildInfo`] from the pointer. pub fn to_build_info(&self) -> Option { None } } #[derive(Debug, Clone)] struct WheelTarget { /// The URL from which the wheel can be downloaded. url: DisplaySafeUrl, /// The expected extension of the wheel file. extension: WheelExtension, /// The expected size of the wheel file, if known. size: Option, } impl TryFrom<&File> for WheelTarget { type Error = ToUrlError; /// Determine the [`WheelTarget`] from a [`File`]. fn try_from(file: &File) -> Result { let url = file.url.to_url()?; if let Some(zstd) = file.zstd.as_ref() { Ok(Self { url: add_tar_zst_extension(url), extension: WheelExtension::WhlZst, size: zstd.size, }) } else { Ok(Self { url, extension: WheelExtension::Whl, size: file.size, }) } } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum WheelExtension { /// A `.whl` file. Whl, /// A `.whl.tar.zst` file. WhlZst, } /// Add `.tar.zst` to the end of the URL path, if it doesn't already exist. #[must_use] fn add_tar_zst_extension(mut url: DisplaySafeUrl) -> DisplaySafeUrl { let mut path = url.path().to_string(); if !path.ends_with(".tar.zst") { path.push_str(".tar.zst"); } url.set_path(&path); url } #[cfg(test)] mod tests { use super::*; #[test] fn test_add_tar_zst_extension() { let url = DisplaySafeUrl::parse("https://files.pythonhosted.org/flask-3.1.0-py3-none-any.whl") .unwrap(); assert_eq!( add_tar_zst_extension(url).as_str(), "https://files.pythonhosted.org/flask-3.1.0-py3-none-any.whl.tar.zst" ); let url = DisplaySafeUrl::parse( "https://files.pythonhosted.org/flask-3.1.0-py3-none-any.whl.tar.zst", ) .unwrap(); assert_eq!( add_tar_zst_extension(url).as_str(), "https://files.pythonhosted.org/flask-3.1.0-py3-none-any.whl.tar.zst" ); let url = DisplaySafeUrl::parse( "https://files.pythonhosted.org/flask-3.1.0%2Bcu124-py3-none-any.whl", ) .unwrap(); assert_eq!( add_tar_zst_extension(url).as_str(), "https://files.pythonhosted.org/flask-3.1.0%2Bcu124-py3-none-any.whl.tar.zst" ); } } uv-0.9.17+ds1/crates/uv-distribution/src/download.rs000066400000000000000000000043061520155276700223360ustar00rootroot00000000000000use std::path::Path; use uv_cache_info::CacheInfo; use uv_distribution_filename::WheelFilename; use uv_distribution_types::{BuildInfo, CachedDist, Dist, Hashed}; use uv_metadata::read_flat_wheel_metadata; use uv_pypi_types::{HashDigest, HashDigests, ResolutionMetadata}; use crate::Error; /// A locally available wheel. #[derive(Debug, Clone)] pub struct LocalWheel { /// The remote distribution from which this wheel was downloaded. pub(crate) dist: Dist, /// The parsed filename. pub(crate) filename: WheelFilename, /// The canonicalized path in the cache directory to which the wheel was downloaded. /// Typically, a directory within the archive bucket. pub(crate) archive: Box, /// The cache info of the wheel. pub(crate) cache: CacheInfo, /// The build info, if available. pub(crate) build: Option, /// The computed hashes of the wheel. pub(crate) hashes: HashDigests, } impl LocalWheel { /// Return the path to the downloaded wheel's entry in the cache. pub fn target(&self) -> &Path { &self.archive } /// Return the [`Dist`] from which this wheel was downloaded. pub fn remote(&self) -> &Dist { &self.dist } /// Return the [`WheelFilename`] of this wheel. pub fn filename(&self) -> &WheelFilename { &self.filename } /// Read the [`ResolutionMetadata`] from a wheel. pub fn metadata(&self) -> Result { read_flat_wheel_metadata(&self.filename, &self.archive) .map_err(|err| Error::WheelMetadata(self.archive.to_path_buf(), Box::new(err))) } } impl Hashed for LocalWheel { fn hashes(&self) -> &[HashDigest] { self.hashes.as_slice() } } /// Convert a [`LocalWheel`] into a [`CachedDist`]. impl From for CachedDist { fn from(wheel: LocalWheel) -> Self { Self::from_remote( wheel.dist, wheel.filename, wheel.hashes, wheel.cache, wheel.build, wheel.archive, ) } } impl std::fmt::Display for LocalWheel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.remote()) } } uv-0.9.17+ds1/crates/uv-distribution/src/error.rs000066400000000000000000000234241520155276700216620ustar00rootroot00000000000000use std::path::PathBuf; use owo_colors::OwoColorize; use tokio::task::JoinError; use zip::result::ZipError; use crate::metadata::MetadataError; use uv_client::WrappedReqwestError; use uv_distribution_filename::{WheelFilename, WheelFilenameError}; use uv_distribution_types::{InstalledDist, InstalledDistError, IsBuildBackendError}; use uv_fs::{LockedFileError, Simplified}; use uv_git::GitError; use uv_normalize::PackageName; use uv_pep440::{Version, VersionSpecifiers}; use uv_platform_tags::Platform; use uv_pypi_types::{HashAlgorithm, HashDigest}; use uv_redacted::DisplaySafeUrl; use uv_types::AnyErrorBuild; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Building source distributions is disabled")] NoBuild, // Network error #[error("Expected an absolute path, but received: {}", _0.user_display())] RelativePath(PathBuf), #[error(transparent)] InvalidUrl(#[from] uv_distribution_types::ToUrlError), #[error("Expected a file URL, but received: {0}")] NonFileUrl(DisplaySafeUrl), #[error(transparent)] Git(#[from] uv_git::GitResolverError), #[error(transparent)] Reqwest(#[from] WrappedReqwestError), #[error(transparent)] Client(#[from] uv_client::Error), // Cache writing error #[error("Failed to read from the distribution cache")] CacheRead(#[source] std::io::Error), #[error("Failed to write to the distribution cache")] CacheWrite(#[source] std::io::Error), #[error("Failed to acquire lock on the distribution cache")] CacheLock(#[source] LockedFileError), #[error("Failed to deserialize cache entry")] CacheDecode(#[from] rmp_serde::decode::Error), #[error("Failed to serialize cache entry")] CacheEncode(#[from] rmp_serde::encode::Error), #[error("Failed to walk the distribution cache")] CacheWalk(#[source] walkdir::Error), #[error(transparent)] CacheInfo(#[from] uv_cache_info::CacheInfoError), // Build error #[error(transparent)] Build(AnyErrorBuild), #[error("Built wheel has an invalid filename")] WheelFilename(#[from] WheelFilenameError), #[error("Package metadata name `{metadata}` does not match given name `{given}`")] WheelMetadataNameMismatch { given: PackageName, metadata: PackageName, }, #[error("Package metadata version `{metadata}` does not match given version `{given}`")] WheelMetadataVersionMismatch { given: Version, metadata: Version }, #[error( "Package metadata name `{metadata}` does not match `{filename}` from the wheel filename" )] WheelFilenameNameMismatch { filename: PackageName, metadata: PackageName, }, #[error( "Package metadata version `{metadata}` does not match `{filename}` from the wheel filename" )] WheelFilenameVersionMismatch { filename: Version, metadata: Version, }, /// This shouldn't happen, it's a bug in the build backend. #[error( "The built wheel `{}` is not compatible with the current Python {}.{} on {} {}", filename, python_version.0, python_version.1, python_platform.os(), python_platform.arch(), )] BuiltWheelIncompatibleHostPlatform { filename: WheelFilename, python_platform: Platform, python_version: (u8, u8), }, /// This may happen when trying to cross-install native dependencies without their build backend /// being aware that the target is a cross-install. #[error( "The built wheel `{}` is not compatible with the target Python {}.{} on {} {}. Consider using `--no-build` to disable building wheels.", filename, python_version.0, python_version.1, python_platform.os(), python_platform.arch(), )] BuiltWheelIncompatibleTargetPlatform { filename: WheelFilename, python_platform: Platform, python_version: (u8, u8), }, #[error("Failed to parse metadata from built wheel")] Metadata(#[from] uv_pypi_types::MetadataError), #[error("Failed to read metadata: `{}`", _0.user_display())] WheelMetadata(PathBuf, #[source] Box), #[error("Failed to read metadata from installed package `{0}`")] ReadInstalled(Box, #[source] InstalledDistError), #[error("Failed to read zip archive from built wheel")] Zip(#[from] ZipError), #[error("Failed to extract archive: {0}")] Extract(String, #[source] uv_extract::Error), #[error("The source distribution is missing a `PKG-INFO` file")] MissingPkgInfo, #[error("The source distribution `{}` has no subdirectory `{}`", _0, _1.display())] MissingSubdirectory(DisplaySafeUrl, PathBuf), #[error("The source distribution `{0}` is missing Git LFS artifacts.")] MissingGitLfsArtifacts(DisplaySafeUrl, #[source] GitError), #[error("Failed to extract static metadata from `PKG-INFO`")] PkgInfo(#[source] uv_pypi_types::MetadataError), #[error("Failed to extract metadata from `requires.txt`")] RequiresTxt(#[source] uv_pypi_types::MetadataError), #[error("The source distribution is missing a `pyproject.toml` file")] MissingPyprojectToml, #[error("Failed to extract static metadata from `pyproject.toml`")] PyprojectToml(#[source] uv_pypi_types::MetadataError), #[error("Unsupported scheme in URL: {0}")] UnsupportedScheme(String), #[error(transparent)] MetadataLowering(#[from] MetadataError), #[error("Distribution not found at: {0}")] NotFound(DisplaySafeUrl), #[error("Attempted to re-extract the source distribution for `{}`, but the {} hash didn't match. Run `{}` to clear the cache.", _0, _1, "uv cache clean".green())] CacheHeal(String, HashAlgorithm), #[error("The source distribution requires Python {0}, but {1} is installed")] RequiresPython(VersionSpecifiers, Version), #[error("Failed to identify base Python interpreter")] BaseInterpreter(#[source] std::io::Error), /// A generic request middleware error happened while making a request. /// Refer to the error message for more details. #[error(transparent)] ReqwestMiddlewareError(#[from] anyhow::Error), /// Should not occur; only seen when another task panicked. #[error("The task executor is broken, did some other task panic?")] Join(#[from] JoinError), /// An I/O error that occurs while exhausting a reader to compute a hash. #[error("Failed to hash distribution")] HashExhaustion(#[source] std::io::Error), #[error("Hash mismatch for `{distribution}`\n\nExpected:\n{expected}\n\nComputed:\n{actual}")] MismatchedHashes { distribution: String, expected: String, actual: String, }, #[error( "Hash-checking is enabled, but no hashes were provided or computed for: `{distribution}`" )] MissingHashes { distribution: String }, #[error( "Hash-checking is enabled, but no hashes were computed for: `{distribution}`\n\nExpected:\n{expected}" )] MissingActualHashes { distribution: String, expected: String, }, #[error( "Hash-checking is enabled, but no hashes were provided for: `{distribution}`\n\nComputed:\n{actual}" )] MissingExpectedHashes { distribution: String, actual: String, }, #[error("Hash-checking is not supported for local directories: `{0}`")] HashesNotSupportedSourceTree(String), #[error("Hash-checking is not supported for Git repositories: `{0}`")] HashesNotSupportedGit(String), } impl From for Error { fn from(error: reqwest::Error) -> Self { Self::Reqwest(WrappedReqwestError::from(error)) } } impl From for Error { fn from(error: reqwest_middleware::Error) -> Self { match error { reqwest_middleware::Error::Middleware(error) => Self::ReqwestMiddlewareError(error), reqwest_middleware::Error::Reqwest(error) => { Self::Reqwest(WrappedReqwestError::from(error)) } } } } impl IsBuildBackendError for Error { fn is_build_backend_error(&self) -> bool { match self { Self::Build(err) => err.is_build_backend_error(), _ => false, } } } impl Error { /// Construct a hash mismatch error. pub fn hash_mismatch( distribution: String, expected: &[HashDigest], actual: &[HashDigest], ) -> Self { match (expected.is_empty(), actual.is_empty()) { (true, true) => Self::MissingHashes { distribution }, (true, false) => { let actual = actual .iter() .map(|hash| format!(" {hash}")) .collect::>() .join("\n"); Self::MissingExpectedHashes { distribution, actual, } } (false, true) => { let expected = expected .iter() .map(|hash| format!(" {hash}")) .collect::>() .join("\n"); Self::MissingActualHashes { distribution, expected, } } (false, false) => { let expected = expected .iter() .map(|hash| format!(" {hash}")) .collect::>() .join("\n"); let actual = actual .iter() .map(|hash| format!(" {hash}")) .collect::>() .join("\n"); Self::MismatchedHashes { distribution, expected, actual, } } } } } uv-0.9.17+ds1/crates/uv-distribution/src/index/000077500000000000000000000000001520155276700212655ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-distribution/src/index/built_wheel_index.rs000066400000000000000000000275701520155276700253400ustar00rootroot00000000000000use std::borrow::Cow; use uv_cache::{Cache, CacheBucket, CacheShard, WheelCache}; use uv_cache_info::CacheInfo; use uv_distribution_types::{ BuildInfo, BuildVariables, ConfigSettings, DirectUrlSourceDist, DirectorySourceDist, ExtraBuildRequirement, ExtraBuildRequires, ExtraBuildVariables, GitSourceDist, Hashed, PackageConfigSettings, PathSourceDist, }; use uv_normalize::PackageName; use uv_platform_tags::Tags; use uv_pypi_types::HashDigests; use uv_types::HashStrategy; use crate::Error; use crate::index::cached_wheel::{CachedWheel, ResolvedWheel}; use crate::source::{HTTP_REVISION, HttpRevisionPointer, LOCAL_REVISION, LocalRevisionPointer}; /// A local index of built distributions for a specific source distribution. #[derive(Debug)] pub struct BuiltWheelIndex<'a> { cache: &'a Cache, tags: &'a Tags, hasher: &'a HashStrategy, config_settings: &'a ConfigSettings, config_settings_package: &'a PackageConfigSettings, extra_build_requires: &'a ExtraBuildRequires, extra_build_variables: &'a ExtraBuildVariables, } impl<'a> BuiltWheelIndex<'a> { /// Initialize an index of built distributions. pub fn new( cache: &'a Cache, tags: &'a Tags, hasher: &'a HashStrategy, config_settings: &'a ConfigSettings, config_settings_package: &'a PackageConfigSettings, extra_build_requires: &'a ExtraBuildRequires, extra_build_variables: &'a ExtraBuildVariables, ) -> Self { Self { cache, tags, hasher, config_settings, config_settings_package, extra_build_requires, extra_build_variables, } } /// Return the most compatible [`CachedWheel`] for a given source distribution at a direct URL. /// /// This method does not perform any freshness checks and assumes that the source distribution /// is already up-to-date. pub fn url(&self, source_dist: &DirectUrlSourceDist) -> Result, Error> { // For direct URLs, cache directly under the hash of the URL itself. let cache_shard = self.cache.shard( CacheBucket::SourceDistributions, WheelCache::Url(source_dist.url.raw()).root(), ); // Read the revision from the cache. let Some(pointer) = HttpRevisionPointer::read_from(cache_shard.entry(HTTP_REVISION))? else { return Ok(None); }; // Enforce hash-checking by omitting any wheels that don't satisfy the required hashes. let revision = pointer.into_revision(); if !revision.satisfies(self.hasher.get(source_dist)) { return Ok(None); } let cache_shard = cache_shard.shard(revision.id()); // If there are build settings, we need to scope to a cache shard. let config_settings = self.config_settings_for(&source_dist.name); let extra_build_deps = self.extra_build_requires_for(&source_dist.name); let extra_build_vars = self.extra_build_variables_for(&source_dist.name); let build_info = BuildInfo::from_settings(&config_settings, extra_build_deps, extra_build_vars); let cache_shard = build_info .cache_shard() .map(|digest| cache_shard.shard(digest)) .unwrap_or(cache_shard); Ok(self.find(&cache_shard).map(|wheel| { CachedWheel::from_entry( wheel, revision.into_hashes(), CacheInfo::default(), build_info, ) })) } /// Return the most compatible [`CachedWheel`] for a given source distribution at a local path. pub fn path(&self, source_dist: &PathSourceDist) -> Result, Error> { let cache_shard = self.cache.shard( CacheBucket::SourceDistributions, WheelCache::Path(&source_dist.url).root(), ); // Read the revision from the cache. let Some(pointer) = LocalRevisionPointer::read_from(cache_shard.entry(LOCAL_REVISION))? else { return Ok(None); }; // If the distribution is stale, omit it from the index. let cache_info = CacheInfo::from_file(&source_dist.install_path).map_err(Error::CacheRead)?; if cache_info != *pointer.cache_info() { return Ok(None); } // Enforce hash-checking by omitting any wheels that don't satisfy the required hashes. let revision = pointer.into_revision(); if !revision.satisfies(self.hasher.get(source_dist)) { return Ok(None); } let cache_shard = cache_shard.shard(revision.id()); // If there are build settings, we need to scope to a cache shard. let config_settings = self.config_settings_for(&source_dist.name); let extra_build_deps = self.extra_build_requires_for(&source_dist.name); let extra_build_vars = self.extra_build_variables_for(&source_dist.name); let build_info = BuildInfo::from_settings(&config_settings, extra_build_deps, extra_build_vars); let cache_shard = build_info .cache_shard() .map(|digest| cache_shard.shard(digest)) .unwrap_or(cache_shard); Ok(self.find(&cache_shard).map(|wheel| { CachedWheel::from_entry(wheel, revision.into_hashes(), cache_info, build_info) })) } /// Return the most compatible [`CachedWheel`] for a given source distribution built from a /// local directory (source tree). pub fn directory( &self, source_dist: &DirectorySourceDist, ) -> Result, Error> { let cache_shard = self.cache.shard( CacheBucket::SourceDistributions, if source_dist.editable.unwrap_or(false) { WheelCache::Editable(&source_dist.url).root() } else { WheelCache::Path(&source_dist.url).root() }, ); // Read the revision from the cache. let Some(pointer) = LocalRevisionPointer::read_from(cache_shard.entry(LOCAL_REVISION))? else { return Ok(None); }; // If the distribution is stale, omit it from the index. let cache_info = CacheInfo::from_directory(&source_dist.install_path)?; if cache_info != *pointer.cache_info() { return Ok(None); } // Enforce hash-checking by omitting any wheels that don't satisfy the required hashes. let revision = pointer.into_revision(); if !revision.satisfies(self.hasher.get(source_dist)) { return Ok(None); } let cache_shard = cache_shard.shard(revision.id()); // If there are build settings, we need to scope to a cache shard. let config_settings = self.config_settings_for(&source_dist.name); let extra_build_deps = self.extra_build_requires_for(&source_dist.name); let extra_build_vars = self.extra_build_variables_for(&source_dist.name); let build_info = BuildInfo::from_settings(&config_settings, extra_build_deps, extra_build_vars); let cache_shard = build_info .cache_shard() .map(|digest| cache_shard.shard(digest)) .unwrap_or(cache_shard); Ok(self.find(&cache_shard).map(|wheel| { CachedWheel::from_entry(wheel, revision.into_hashes(), cache_info, build_info) })) } /// Return the most compatible [`CachedWheel`] for a given source distribution at a git URL. pub fn git(&self, source_dist: &GitSourceDist) -> Option { // Enforce hash-checking, which isn't supported for Git distributions. if self.hasher.get(source_dist).is_validate() { return None; } let git_sha = source_dist.git.precise()?; let cache_shard = self.cache.shard( CacheBucket::SourceDistributions, WheelCache::Git(&source_dist.url, git_sha.as_short_str()).root(), ); // If there are build settings, we need to scope to a cache shard. let config_settings = self.config_settings_for(&source_dist.name); let extra_build_deps = self.extra_build_requires_for(&source_dist.name); let extra_build_vars = self.extra_build_variables_for(&source_dist.name); let build_info = BuildInfo::from_settings(&config_settings, extra_build_deps, extra_build_vars); let cache_shard = build_info .cache_shard() .map(|digest| cache_shard.shard(digest)) .unwrap_or(cache_shard); self.find(&cache_shard).map(|wheel| { CachedWheel::from_entry( wheel, HashDigests::empty(), CacheInfo::default(), build_info, ) }) } /// Find the "best" distribution in the index for a given source distribution. /// /// This lookup prefers newer versions over older versions, and aims to maximize compatibility /// with the target platform. /// /// The `shard` should point to a directory containing the built distributions for a specific /// source distribution. For example, given the built wheel cache structure: /// ```text /// built-wheels-v0/ /// └── pypi /// └── django-allauth-0.51.0.tar.gz /// ├── django_allauth-0.51.0-py3-none-any.whl /// └── metadata.json /// ``` /// /// The `shard` should be `built-wheels-v0/pypi/django-allauth-0.51.0.tar.gz`. fn find(&self, shard: &CacheShard) -> Option { let mut candidate: Option = None; // Unzipped wheels are stored as symlinks into the archive directory. for wheel_dir in uv_fs::entries(shard).ok().into_iter().flatten() { // Ignore any `.lock` files. if wheel_dir .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("lock")) { continue; } match ResolvedWheel::from_built_source(&wheel_dir, self.cache) { None => {} Some(dist_info) => { // Pick the wheel with the highest priority let compatibility = dist_info.filename.compatibility(self.tags); // Only consider wheels that are compatible with our tags. if !compatibility.is_compatible() { continue; } if let Some(existing) = candidate.as_ref() { // Override if the wheel is newer, or "more" compatible. if dist_info.filename.version > existing.filename.version || compatibility > existing.filename.compatibility(self.tags) { candidate = Some(dist_info); } } else { candidate = Some(dist_info); } } } } candidate } /// Determine the [`ConfigSettings`] for the given package name. fn config_settings_for(&self, name: &PackageName) -> Cow<'_, ConfigSettings> { if let Some(package_settings) = self.config_settings_package.get(name) { Cow::Owned(package_settings.clone().merge(self.config_settings.clone())) } else { Cow::Borrowed(self.config_settings) } } /// Determine the extra build requirements for the given package name. fn extra_build_requires_for(&self, name: &PackageName) -> &[ExtraBuildRequirement] { self.extra_build_requires .get(name) .map(Vec::as_slice) .unwrap_or(&[]) } /// Determine the extra build variables for the given package name. fn extra_build_variables_for(&self, name: &PackageName) -> Option<&BuildVariables> { self.extra_build_variables.get(name) } } uv-0.9.17+ds1/crates/uv-distribution/src/index/cached_wheel.rs000066400000000000000000000151061520155276700242310ustar00rootroot00000000000000use std::path::Path; use uv_cache::{Cache, CacheBucket, CacheEntry}; use uv_cache_info::CacheInfo; use uv_distribution_filename::WheelFilename; use uv_distribution_types::{ BuildInfo, CachedDirectUrlDist, CachedRegistryDist, DirectUrlSourceDist, DirectorySourceDist, GitSourceDist, Hashed, PathSourceDist, }; use uv_pypi_types::{HashDigest, HashDigests, VerbatimParsedUrl}; use crate::archive::Archive; use crate::{HttpArchivePointer, LocalArchivePointer}; #[derive(Debug, Clone)] pub struct ResolvedWheel { /// The filename of the wheel. pub filename: WheelFilename, /// The [`CacheEntry`] for the wheel. pub entry: CacheEntry, } impl ResolvedWheel { /// Try to parse a distribution from a cached directory name (like `typing-extensions-4.8.0-py3-none-any`). pub fn from_built_source(path: impl AsRef, cache: &Cache) -> Option { let path = path.as_ref(); // Determine the wheel filename. let filename = path.file_name()?.to_str()?; let filename = WheelFilename::from_stem(filename).ok()?; // Convert to a cached wheel. let archive = cache.resolve_link(path).ok()?; let entry = CacheEntry::from_path(archive); Some(Self { filename, entry }) } } #[derive(Debug, Clone)] pub struct CachedWheel { /// The filename of the wheel. pub filename: WheelFilename, /// The [`CacheEntry`] for the wheel. pub entry: CacheEntry, /// The [`HashDigest`]s for the wheel. pub hashes: HashDigests, /// The [`CacheInfo`] for the wheel. pub cache_info: CacheInfo, /// The [`BuildInfo`] for the wheel, if it was built. pub build_info: Option, } impl CachedWheel { /// Create a [`CachedWheel`] from a [`ResolvedWheel`]. pub fn from_entry( wheel: ResolvedWheel, hashes: HashDigests, cache_info: CacheInfo, build_info: BuildInfo, ) -> Self { Self { filename: wheel.filename, entry: wheel.entry, hashes, cache_info, build_info: Some(build_info), } } /// Read a cached wheel from a `.http` pointer pub fn from_http_pointer(path: impl AsRef, cache: &Cache) -> Option { let path = path.as_ref(); // Read the pointer. let pointer = HttpArchivePointer::read_from(path).ok()??; let cache_info = pointer.to_cache_info(); let build_info = pointer.to_build_info(); let archive = pointer.into_archive(); // Ignore stale pointers. if !archive.exists(cache) { return None; } let Archive { id, hashes, .. } = archive; let entry = cache.entry(CacheBucket::Archive, "", id); // Convert to a cached wheel. Some(Self { filename: archive.filename, entry, hashes, cache_info, build_info, }) } /// Read a cached wheel from a `.rev` pointer pub fn from_local_pointer(path: impl AsRef, cache: &Cache) -> Option { let path = path.as_ref(); // Read the pointer. let pointer = LocalArchivePointer::read_from(path).ok()??; let cache_info = pointer.to_cache_info(); let build_info = pointer.to_build_info(); let archive = pointer.into_archive(); // Ignore stale pointers. if !archive.exists(cache) { return None; } let Archive { id, hashes, .. } = archive; let entry = cache.entry(CacheBucket::Archive, "", id); // Convert to a cached wheel. Some(Self { filename: archive.filename, entry, hashes, cache_info, build_info, }) } /// Convert a [`CachedWheel`] into a [`CachedRegistryDist`]. pub fn into_registry_dist(self) -> CachedRegistryDist { CachedRegistryDist { filename: self.filename, path: self.entry.into_path_buf().into_boxed_path(), hashes: self.hashes, cache_info: self.cache_info, build_info: self.build_info, } } /// Convert a [`CachedWheel`] into a [`CachedDirectUrlDist`] by merging in the given /// [`DirectUrlSourceDist`]. pub fn into_url_dist(self, dist: &DirectUrlSourceDist) -> CachedDirectUrlDist { CachedDirectUrlDist { filename: self.filename, url: VerbatimParsedUrl { parsed_url: dist.parsed_url(), verbatim: dist.url.clone(), }, path: self.entry.into_path_buf().into_boxed_path(), hashes: self.hashes, cache_info: self.cache_info, build_info: self.build_info, } } /// Convert a [`CachedWheel`] into a [`CachedDirectUrlDist`] by merging in the given /// [`PathSourceDist`]. pub fn into_path_dist(self, dist: &PathSourceDist) -> CachedDirectUrlDist { CachedDirectUrlDist { filename: self.filename, url: VerbatimParsedUrl { parsed_url: dist.parsed_url(), verbatim: dist.url.clone(), }, path: self.entry.into_path_buf().into_boxed_path(), hashes: self.hashes, cache_info: self.cache_info, build_info: self.build_info, } } /// Convert a [`CachedWheel`] into a [`CachedDirectUrlDist`] by merging in the given /// [`DirectorySourceDist`]. pub fn into_directory_dist(self, dist: &DirectorySourceDist) -> CachedDirectUrlDist { CachedDirectUrlDist { filename: self.filename, url: VerbatimParsedUrl { parsed_url: dist.parsed_url(), verbatim: dist.url.clone(), }, path: self.entry.into_path_buf().into_boxed_path(), hashes: self.hashes, cache_info: self.cache_info, build_info: self.build_info, } } /// Convert a [`CachedWheel`] into a [`CachedDirectUrlDist`] by merging in the given /// [`GitSourceDist`]. pub fn into_git_dist(self, dist: &GitSourceDist) -> CachedDirectUrlDist { CachedDirectUrlDist { filename: self.filename, url: VerbatimParsedUrl { parsed_url: dist.parsed_url(), verbatim: dist.url.clone(), }, path: self.entry.into_path_buf().into_boxed_path(), hashes: self.hashes, cache_info: self.cache_info, build_info: self.build_info, } } } impl Hashed for CachedWheel { fn hashes(&self) -> &[HashDigest] { self.hashes.as_slice() } } uv-0.9.17+ds1/crates/uv-distribution/src/index/mod.rs000066400000000000000000000002421520155276700224100ustar00rootroot00000000000000pub use built_wheel_index::BuiltWheelIndex; pub use registry_wheel_index::RegistryWheelIndex; mod built_wheel_index; mod cached_wheel; mod registry_wheel_index; uv-0.9.17+ds1/crates/uv-distribution/src/index/registry_wheel_index.rs000066400000000000000000000327031520155276700260630ustar00rootroot00000000000000use std::borrow::Cow; use std::collections::hash_map::Entry; use rustc_hash::{FxHashMap, FxHashSet}; use uv_cache::{Cache, CacheBucket, WheelCache}; use uv_cache_info::CacheInfo; use uv_distribution_types::{ BuildInfo, BuildVariables, CachedRegistryDist, ConfigSettings, ExtraBuildRequirement, ExtraBuildRequires, ExtraBuildVariables, Hashed, Index, IndexLocations, IndexUrl, PackageConfigSettings, }; use uv_fs::{directories, files}; use uv_normalize::PackageName; use uv_platform_tags::Tags; use uv_types::HashStrategy; use crate::index::cached_wheel::{CachedWheel, ResolvedWheel}; use crate::source::{HTTP_REVISION, HttpRevisionPointer, LOCAL_REVISION, LocalRevisionPointer}; /// An entry in the [`RegistryWheelIndex`]. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct IndexEntry<'index> { /// The cached distribution. pub dist: CachedRegistryDist, /// Whether the wheel was built from source (true), or downloaded from the registry directly (false). pub built: bool, /// The index from which the wheel was downloaded. pub index: &'index Index, } /// A local index of distributions that originate from a registry, like `PyPI`. #[derive(Debug)] pub struct RegistryWheelIndex<'a> { cache: &'a Cache, tags: &'a Tags, index_locations: &'a IndexLocations, hasher: &'a HashStrategy, index: FxHashMap<&'a PackageName, Vec>>, config_settings: &'a ConfigSettings, config_settings_package: &'a PackageConfigSettings, extra_build_requires: &'a ExtraBuildRequires, extra_build_variables: &'a ExtraBuildVariables, } impl<'a> RegistryWheelIndex<'a> { /// Initialize an index of registry distributions. pub fn new( cache: &'a Cache, tags: &'a Tags, index_locations: &'a IndexLocations, hasher: &'a HashStrategy, config_settings: &'a ConfigSettings, config_settings_package: &'a PackageConfigSettings, extra_build_requires: &'a ExtraBuildRequires, extra_build_variables: &'a ExtraBuildVariables, ) -> Self { Self { cache, tags, index_locations, hasher, config_settings, config_settings_package, extra_build_requires, extra_build_variables, index: FxHashMap::default(), } } /// Return an iterator over available wheels for a given package. /// /// If the package is not yet indexed, this will index the package by reading from the cache. pub fn get(&mut self, name: &'a PackageName) -> impl Iterator> { self.get_impl(name).iter().rev() } /// Get an entry in the index. fn get_impl(&mut self, name: &'a PackageName) -> &[IndexEntry<'_>] { (match self.index.entry(name) { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => entry.insert(Self::index( name, self.cache, self.tags, self.index_locations, self.hasher, self.config_settings, self.config_settings_package, self.extra_build_requires, self.extra_build_variables, )), }) as _ } /// Add a package to the index by reading from the cache. fn index<'index>( package: &PackageName, cache: &Cache, tags: &Tags, index_locations: &'index IndexLocations, hasher: &HashStrategy, config_settings: &ConfigSettings, config_settings_package: &PackageConfigSettings, extra_build_requires: &ExtraBuildRequires, extra_build_variables: &ExtraBuildVariables, ) -> Vec> { let mut entries = vec![]; let mut seen = FxHashSet::default(); for index in index_locations.allowed_indexes() { if !seen.insert(index.url()) { continue; } // Index all the wheels that were downloaded directly from the registry. let wheel_dir = cache.shard( CacheBucket::Wheels, WheelCache::Index(index.url()).wheel_dir(package.as_ref()), ); // For registry wheels, the cache structure is: `//.http` // or `///.rev`. for file in files(&wheel_dir).ok().into_iter().flatten() { match index.url() { // Add files from remote registries. IndexUrl::Pypi(_) | IndexUrl::Url(_) => { if file .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("http")) { if let Some(wheel) = CachedWheel::from_http_pointer(wheel_dir.join(file), cache) { if wheel.filename.compatibility(tags).is_compatible() { // Enforce hash-checking based on the built distribution. if wheel.satisfies( hasher.get_package( &wheel.filename.name, &wheel.filename.version, ), ) { entries.push(IndexEntry { dist: wheel.into_registry_dist(), index, built: false, }); } } } } } // Add files from local registries (e.g., `--find-links`). IndexUrl::Path(_) => { if file .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("rev")) { if let Some(wheel) = CachedWheel::from_local_pointer(wheel_dir.join(file), cache) { if wheel.filename.compatibility(tags).is_compatible() { // Enforce hash-checking based on the built distribution. if wheel.satisfies( hasher.get_package( &wheel.filename.name, &wheel.filename.version, ), ) { entries.push(IndexEntry { dist: wheel.into_registry_dist(), index, built: false, }); } } } } } } } // Index all the built wheels, created by downloading and building source distributions // from the registry. let cache_shard = cache.shard( CacheBucket::SourceDistributions, WheelCache::Index(index.url()).wheel_dir(package.as_ref()), ); // For registry source distributions, the cache structure is: `///`. for shard in directories(&cache_shard).ok().into_iter().flatten() { let cache_shard = cache_shard.shard(shard); // Read the revision from the cache. let revision = match index.url() { // Add files from remote registries. IndexUrl::Pypi(_) | IndexUrl::Url(_) => { let revision_entry = cache_shard.entry(HTTP_REVISION); if let Ok(Some(pointer)) = HttpRevisionPointer::read_from(revision_entry) { Some(pointer.into_revision()) } else { None } } // Add files from local registries (e.g., `--find-links`). IndexUrl::Path(_) => { let revision_entry = cache_shard.entry(LOCAL_REVISION); if let Ok(Some(pointer)) = LocalRevisionPointer::read_from(revision_entry) { Some(pointer.into_revision()) } else { None } } }; if let Some(revision) = revision { let cache_shard = cache_shard.shard(revision.id()); // If there are build settings, we need to scope to a cache shard. let extra_build_deps = Self::extra_build_requires_for(package, extra_build_requires); let extra_build_vars = Self::extra_build_variables_for(package, extra_build_variables); let config_settings = Self::config_settings_for( package, config_settings, config_settings_package, ); let build_info = BuildInfo::from_settings( &config_settings, extra_build_deps, extra_build_vars, ); let cache_shard = build_info .cache_shard() .map(|digest| cache_shard.shard(digest)) .unwrap_or(cache_shard); for wheel_dir in uv_fs::entries(cache_shard).ok().into_iter().flatten() { // Ignore any `.lock` files. if wheel_dir .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("lock")) { continue; } if let Some(wheel) = ResolvedWheel::from_built_source(wheel_dir, cache) { if wheel.filename.compatibility(tags).is_compatible() { // Enforce hash-checking based on the source distribution. if revision.satisfies( hasher .get_package(&wheel.filename.name, &wheel.filename.version), ) { let wheel = CachedWheel::from_entry( wheel, revision.hashes().into(), CacheInfo::default(), build_info.clone(), ); entries.push(IndexEntry { dist: wheel.into_registry_dist(), index, built: true, }); } } } } } } } // Sort the cached distributions by (1) version, (2) compatibility, and (3) build status. // We want the highest versions, with the greatest compatibility, that were built from source. // at the end of the list. entries.sort_unstable_by(|a, b| { a.dist .filename .version .cmp(&b.dist.filename.version) .then_with(|| { a.dist .filename .compatibility(tags) .cmp(&b.dist.filename.compatibility(tags)) .then_with(|| a.built.cmp(&b.built)) }) }); entries } /// Determine the [`ConfigSettings`] for the given package name. fn config_settings_for<'settings>( name: &PackageName, config_settings: &'settings ConfigSettings, config_settings_package: &PackageConfigSettings, ) -> Cow<'settings, ConfigSettings> { if let Some(package_settings) = config_settings_package.get(name) { Cow::Owned(package_settings.clone().merge(config_settings.clone())) } else { Cow::Borrowed(config_settings) } } /// Determine the extra build requirements for the given package name. fn extra_build_requires_for<'settings>( name: &PackageName, extra_build_requires: &'settings ExtraBuildRequires, ) -> &'settings [ExtraBuildRequirement] { extra_build_requires .get(name) .map(Vec::as_slice) .unwrap_or(&[]) } /// Determine the extra build variables for the given package name. fn extra_build_variables_for<'settings>( name: &PackageName, extra_build_variables: &'settings ExtraBuildVariables, ) -> Option<&'settings BuildVariables> { extra_build_variables.get(name) } } uv-0.9.17+ds1/crates/uv-distribution/src/lib.rs000066400000000000000000000011111520155276700212640ustar00rootroot00000000000000pub use distribution_database::{DistributionDatabase, HttpArchivePointer, LocalArchivePointer}; pub use download::LocalWheel; pub use error::Error; pub use index::{BuiltWheelIndex, RegistryWheelIndex}; pub use metadata::{ ArchiveMetadata, BuildRequires, FlatRequiresDist, LoweredExtraBuildDependencies, LoweredRequirement, LoweringError, Metadata, MetadataError, RequiresDist, SourcedDependencyGroups, }; pub use reporter::Reporter; pub use source::prune; mod archive; mod distribution_database; mod download; mod error; mod index; mod metadata; mod reporter; mod source; uv-0.9.17+ds1/crates/uv-distribution/src/metadata/000077500000000000000000000000001520155276700217365ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-distribution/src/metadata/build_requires.rs000066400000000000000000000327141520155276700253310ustar00rootroot00000000000000use std::collections::BTreeMap; use std::path::Path; use uv_auth::CredentialsCache; use uv_configuration::SourceStrategy; use uv_distribution_types::{ ExtraBuildRequirement, ExtraBuildRequires, IndexLocations, Requirement, }; use uv_normalize::PackageName; use uv_workspace::pyproject::{ExtraBuildDependencies, ExtraBuildDependency, ToolUvSources}; use uv_workspace::{ DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace, WorkspaceCache, }; use crate::metadata::{LoweredRequirement, MetadataError}; /// Lowered requirements from a `[build-system.requires]` field in a `pyproject.toml` file. #[derive(Debug, Clone)] pub struct BuildRequires { pub name: Option, pub requires_dist: Vec, } impl BuildRequires { /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive /// dependencies. pub fn from_metadata23(metadata: uv_pypi_types::BuildRequires) -> Self { Self { name: metadata.name, requires_dist: metadata .requires_dist .into_iter() .map(Requirement::from) .collect(), } } /// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory /// dependencies. pub async fn from_project_maybe_workspace( metadata: uv_pypi_types::BuildRequires, install_path: &Path, locations: &IndexLocations, sources: SourceStrategy, cache: &WorkspaceCache, credentials_cache: &CredentialsCache, ) -> Result { let discovery = match sources { SourceStrategy::Enabled => DiscoveryOptions::default(), SourceStrategy::Disabled => DiscoveryOptions { members: MemberDiscovery::None, ..Default::default() }, }; let Some(project_workspace) = ProjectWorkspace::from_maybe_project_root(install_path, &discovery, cache).await? else { return Ok(Self::from_metadata23(metadata)); }; Self::from_project_workspace( metadata, &project_workspace, locations, sources, credentials_cache, ) } /// Lower the `build-system.requires` field from a `pyproject.toml` file. pub fn from_project_workspace( metadata: uv_pypi_types::BuildRequires, project_workspace: &ProjectWorkspace, locations: &IndexLocations, source_strategy: SourceStrategy, credentials_cache: &CredentialsCache, ) -> Result { // Collect any `tool.uv.index` entries. let empty = vec![]; let project_indexes = match source_strategy { SourceStrategy::Enabled => project_workspace .current_project() .pyproject_toml() .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) .and_then(|uv| uv.index.as_deref()) .unwrap_or(&empty), SourceStrategy::Disabled => &empty, }; // Collect any `tool.uv.sources` and `tool.uv.dev_dependencies` from `pyproject.toml`. let empty = BTreeMap::default(); let project_sources = match source_strategy { SourceStrategy::Enabled => project_workspace .current_project() .pyproject_toml() .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) .and_then(|uv| uv.sources.as_ref()) .map(ToolUvSources::inner) .unwrap_or(&empty), SourceStrategy::Disabled => &empty, }; // Lower the requirements. let requires_dist = metadata.requires_dist.into_iter(); let requires_dist = match source_strategy { SourceStrategy::Enabled => requires_dist .flat_map(|requirement| { let requirement_name = requirement.name.clone(); let extra = requirement.marker.top_level_extra_name(); let group = None; LoweredRequirement::from_requirement( requirement, metadata.name.as_ref(), project_workspace.project_root(), project_sources, project_indexes, extra.as_deref(), group, locations, project_workspace.workspace(), None, credentials_cache, ) .map(move |requirement| match requirement { Ok(requirement) => Ok(requirement.into_inner()), Err(err) => Err(MetadataError::LoweringError( requirement_name.clone(), Box::new(err), )), }) }) .collect::, _>>()?, SourceStrategy::Disabled => requires_dist.into_iter().map(Requirement::from).collect(), }; Ok(Self { name: metadata.name, requires_dist, }) } /// Lower the `build-system.requires` field from a `pyproject.toml` file. pub fn from_workspace( metadata: uv_pypi_types::BuildRequires, workspace: &Workspace, locations: &IndexLocations, source_strategy: SourceStrategy, credentials_cache: &CredentialsCache, ) -> Result { // Collect any `tool.uv.index` entries. let empty = vec![]; let project_indexes = match source_strategy { SourceStrategy::Enabled => workspace .pyproject_toml() .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) .and_then(|uv| uv.index.as_deref()) .unwrap_or(&empty), SourceStrategy::Disabled => &empty, }; // Collect any `tool.uv.sources` and `tool.uv.dev_dependencies` from `pyproject.toml`. let empty = BTreeMap::default(); let project_sources = match source_strategy { SourceStrategy::Enabled => workspace .pyproject_toml() .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) .and_then(|uv| uv.sources.as_ref()) .map(ToolUvSources::inner) .unwrap_or(&empty), SourceStrategy::Disabled => &empty, }; // Lower the requirements. let requires_dist = metadata.requires_dist.into_iter(); let requires_dist = match source_strategy { SourceStrategy::Enabled => requires_dist .flat_map(|requirement| { let requirement_name = requirement.name.clone(); let extra = requirement.marker.top_level_extra_name(); let group = None; LoweredRequirement::from_requirement( requirement, None, workspace.install_path(), project_sources, project_indexes, extra.as_deref(), group, locations, workspace, None, credentials_cache, ) .map(move |requirement| match requirement { Ok(requirement) => Ok(requirement.into_inner()), Err(err) => Err(MetadataError::LoweringError( requirement_name.clone(), Box::new(err), )), }) }) .collect::, _>>()?, SourceStrategy::Disabled => requires_dist.into_iter().map(Requirement::from).collect(), }; Ok(Self { name: metadata.name, requires_dist, }) } } /// Lowered extra build dependencies. /// /// This is a wrapper around [`ExtraBuildRequires`] that provides methods to lower /// [`ExtraBuildDependencies`] from a workspace context or from already lowered dependencies. #[derive(Debug, Clone, Default)] pub struct LoweredExtraBuildDependencies(ExtraBuildRequires); impl LoweredExtraBuildDependencies { /// Return the [`ExtraBuildRequires`] that this was lowered into. pub fn into_inner(self) -> ExtraBuildRequires { self.0 } /// Create from a workspace, lowering the extra build dependencies. pub fn from_workspace( extra_build_dependencies: ExtraBuildDependencies, workspace: &Workspace, index_locations: &IndexLocations, source_strategy: SourceStrategy, credentials_cache: &CredentialsCache, ) -> Result { match source_strategy { SourceStrategy::Enabled => { // Collect project sources and indexes let project_indexes = workspace .pyproject_toml() .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) .and_then(|uv| uv.index.as_deref()) .unwrap_or(&[]); let empty_sources = BTreeMap::default(); let project_sources = workspace .pyproject_toml() .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) .and_then(|uv| uv.sources.as_ref()) .map(ToolUvSources::inner) .unwrap_or(&empty_sources); // Lower each package's extra build dependencies let mut build_requires = ExtraBuildRequires::default(); for (package_name, requirements) in extra_build_dependencies { let lowered: Vec = requirements .into_iter() .flat_map( |ExtraBuildDependency { requirement, match_runtime, }| { let requirement_name = requirement.name.clone(); let extra = requirement.marker.top_level_extra_name(); let group = None; LoweredRequirement::from_requirement( requirement, None, workspace.install_path(), project_sources, project_indexes, extra.as_deref(), group, index_locations, workspace, None, credentials_cache, ) .map(move |requirement| { match requirement { Ok(requirement) => Ok(ExtraBuildRequirement { requirement: requirement.into_inner(), match_runtime, }), Err(err) => Err(MetadataError::LoweringError( requirement_name.clone(), Box::new(err), )), } }) }, ) .collect::, _>>()?; build_requires.insert(package_name, lowered); } Ok(Self(build_requires)) } SourceStrategy::Disabled => Ok(Self::from_non_lowered(extra_build_dependencies)), } } /// Create from lowered dependencies (for non-workspace contexts, like scripts). pub fn from_lowered(extra_build_dependencies: ExtraBuildRequires) -> Self { Self(extra_build_dependencies) } /// Create from unlowered dependencies (e.g., for contexts in the pip CLI). pub fn from_non_lowered(extra_build_dependencies: ExtraBuildDependencies) -> Self { Self( extra_build_dependencies .into_iter() .map(|(name, requirements)| { ( name, requirements .into_iter() .map( |ExtraBuildDependency { requirement, match_runtime, }| { ExtraBuildRequirement { requirement: requirement.into(), match_runtime, } }, ) .collect::>(), ) }) .collect(), ) } } uv-0.9.17+ds1/crates/uv-distribution/src/metadata/dependency_groups.rs000066400000000000000000000214351520155276700260260ustar00rootroot00000000000000use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use uv_auth::CredentialsCache; use uv_configuration::SourceStrategy; use uv_distribution_types::{IndexLocations, Requirement}; use uv_normalize::{GroupName, PackageName}; use uv_workspace::dependency_groups::FlatDependencyGroups; use uv_workspace::pyproject::{Sources, ToolUvSources}; use uv_workspace::{ DiscoveryOptions, MemberDiscovery, VirtualProject, WorkspaceCache, WorkspaceError, }; use crate::metadata::{GitWorkspaceMember, LoweredRequirement, MetadataError}; /// Like [`crate::RequiresDist`] but only supporting dependency-groups. /// /// PEP 735 says: /// /// > A pyproject.toml file with only `[dependency-groups]` and no other tables is valid. /// /// This is a special carveout to enable users to adopt dependency-groups without having /// to learn about projects. It is supported by `pip install --group`, and thus interfaces /// like `uv pip install --group` must also support it for interop and conformance. /// /// On paper this is trivial to support because dependency-groups are so self-contained /// that they're basically a `requirements.txt` embedded within a pyproject.toml, so it's /// fine to just grab that section and handle it independently. /// /// However several uv extensions make this complicated, notably, as of this writing: /// /// * tool.uv.sources /// * tool.uv.index /// /// These fields may also be present in the pyproject.toml, and, critically, /// may be defined and inherited in a parent workspace pyproject.toml. /// /// Therefore, we need to gracefully degrade from a full workspacey situation all /// the way down to one of these stub pyproject.tomls the PEP defines. This is why /// we avoid going through `RequiresDist` -- we don't want to muddy up the "compile a package" /// logic with support for non-project/workspace pyproject.tomls, and we don't want to /// muddy this logic up with setuptools fallback modes that `RequiresDist` wants. /// /// (We used to shove this feature into that path, and then we would see there's no metadata /// and try to run setuptools to try to desperately find any metadata, and then error out.) #[derive(Debug, Clone)] pub struct SourcedDependencyGroups { pub name: Option, pub dependency_groups: BTreeMap>, } impl SourcedDependencyGroups { /// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory /// dependencies. pub async fn from_virtual_project( pyproject_path: &Path, git_member: Option<&GitWorkspaceMember<'_>>, locations: &IndexLocations, source_strategy: SourceStrategy, cache: &WorkspaceCache, credentials_cache: &CredentialsCache, ) -> Result { // If the `pyproject.toml` doesn't exist, fail early. if !pyproject_path.is_file() { return Err(MetadataError::MissingPyprojectToml( pyproject_path.to_path_buf(), )); } let discovery = DiscoveryOptions { stop_discovery_at: git_member.map(|git_member| { git_member .fetch_root .parent() .expect("git checkout has a parent") .to_path_buf() }), members: match source_strategy { SourceStrategy::Enabled => MemberDiscovery::default(), SourceStrategy::Disabled => MemberDiscovery::None, }, ..DiscoveryOptions::default() }; // The subsequent API takes an absolute path to the dir the pyproject is in let empty = PathBuf::new(); let absolute_pyproject_path = std::path::absolute(pyproject_path).map_err(WorkspaceError::Normalize)?; let project_dir = absolute_pyproject_path.parent().unwrap_or(&empty); let project = VirtualProject::discover(project_dir, &discovery, cache).await?; // Collect the dependency groups. let dependency_groups = FlatDependencyGroups::from_pyproject_toml(project.root(), project.pyproject_toml())?; // If sources/indexes are disabled we can just stop here let SourceStrategy::Enabled = source_strategy else { return Ok(Self { name: project.project_name().cloned(), dependency_groups: dependency_groups .into_iter() .map(|(name, group)| { let requirements = group .requirements .into_iter() .map(Requirement::from) .collect(); (name, requirements) }) .collect(), }); }; // Collect any `tool.uv.index` entries. let empty = vec![]; let project_indexes = project .pyproject_toml() .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) .and_then(|uv| uv.index.as_deref()) .unwrap_or(&empty); // Collect any `tool.uv.sources` and `tool.uv.dev_dependencies` from `pyproject.toml`. let empty = BTreeMap::default(); let project_sources = project .pyproject_toml() .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) .and_then(|uv| uv.sources.as_ref()) .map(ToolUvSources::inner) .unwrap_or(&empty); // Now that we've resolved the dependency groups, we can validate that each source references // a valid extra or group, if present. Self::validate_sources(project_sources, &dependency_groups)?; // Lower the dependency groups. let dependency_groups = dependency_groups .into_iter() .map(|(name, group)| { let requirements = group .requirements .into_iter() .flat_map(|requirement| { let requirement_name = requirement.name.clone(); let group = name.clone(); let extra = None; LoweredRequirement::from_requirement( requirement, project.project_name(), project.root(), project_sources, project_indexes, extra, Some(&group), locations, project.workspace(), git_member, credentials_cache, ) .map(move |requirement| match requirement { Ok(requirement) => Ok(requirement.into_inner()), Err(err) => Err(MetadataError::GroupLoweringError( group.clone(), requirement_name.clone(), Box::new(err), )), }) }) .collect::, _>>()?; Ok::<(GroupName, Box<_>), MetadataError>((name, requirements)) }) .collect::, _>>()?; Ok(Self { name: project.project_name().cloned(), dependency_groups, }) } /// Validate the sources. /// /// If a source is requested with `group`, ensure that the relevant dependency is /// present in the relevant `dependency-groups` section. fn validate_sources( sources: &BTreeMap, dependency_groups: &FlatDependencyGroups, ) -> Result<(), MetadataError> { for (name, sources) in sources { for source in sources.iter() { if let Some(group) = source.group() { // If the group doesn't exist at all, error. let Some(flat_group) = dependency_groups.get(group) else { return Err(MetadataError::MissingSourceGroup( name.clone(), group.clone(), )); }; // If there is no such requirement with the group, error. if !flat_group .requirements .iter() .any(|requirement| requirement.name == *name) { return Err(MetadataError::IncompleteSourceGroup( name.clone(), group.clone(), )); } } } } Ok(()) } } uv-0.9.17+ds1/crates/uv-distribution/src/metadata/lowering.rs000066400000000000000000001006451520155276700241400ustar00rootroot00000000000000use std::collections::BTreeMap; use std::io; use std::path::{Path, PathBuf}; use either::Either; use thiserror::Error; use uv_auth::CredentialsCache; use uv_distribution_filename::DistExtension; use uv_distribution_types::{ Index, IndexLocations, IndexMetadata, IndexName, Origin, Requirement, RequirementSource, }; use uv_git_types::{GitLfs, GitReference, GitUrl, GitUrlParseError}; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep440::VersionSpecifiers; use uv_pep508::{MarkerTree, VerbatimUrl, VersionOrUrl, looks_like_git_repository}; use uv_pypi_types::{ConflictItem, ParsedGitUrl, ParsedUrlError, VerbatimParsedUrl}; use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError}; use uv_workspace::Workspace; use uv_workspace::pyproject::{PyProjectToml, Source, Sources}; use crate::metadata::GitWorkspaceMember; #[derive(Debug, Clone)] pub struct LoweredRequirement(Requirement); #[derive(Debug, Clone, Copy)] enum RequirementOrigin { /// The `tool.uv.sources` were read from the project. Project, /// The `tool.uv.sources` were read from the workspace root. Workspace, } impl LoweredRequirement { /// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`. pub(crate) fn from_requirement<'data>( requirement: uv_pep508::Requirement, project_name: Option<&'data PackageName>, project_dir: &'data Path, project_sources: &'data BTreeMap, project_indexes: &'data [Index], extra: Option<&ExtraName>, group: Option<&GroupName>, locations: &'data IndexLocations, workspace: &'data Workspace, git_member: Option<&'data GitWorkspaceMember<'data>>, credentials_cache: &'data CredentialsCache, ) -> impl Iterator> + use<'data> + 'data { // Identify the source from the `tool.uv.sources` table. let (sources, origin) = if let Some(source) = project_sources.get(&requirement.name) { (Some(source), RequirementOrigin::Project) } else if let Some(source) = workspace.sources().get(&requirement.name) { (Some(source), RequirementOrigin::Workspace) } else { (None, RequirementOrigin::Project) }; // If the source only applies to a given extra or dependency group, filter it out. let sources = sources.map(|sources| { sources .iter() .filter(|source| { if let Some(target) = source.extra() { if extra != Some(target) { return false; } } if let Some(target) = source.group() { if group != Some(target) { return false; } } true }) .cloned() .collect::() }); // If you use a package that's part of the workspace... if workspace.packages().contains_key(&requirement.name) { // And it's not a recursive self-inclusion (extras that activate other extras), e.g. // `framework[machine_learning]` depends on `framework[cuda]`. if project_name.is_none_or(|project_name| *project_name != requirement.name) { // It must be declared as a workspace source. let Some(sources) = sources.as_ref() else { // No sources were declared for the workspace package. return Either::Left(std::iter::once(Err( LoweringError::MissingWorkspaceSource(requirement.name.clone()), ))); }; for source in sources.iter() { match source { Source::Git { .. } => { return Either::Left(std::iter::once(Err( LoweringError::NonWorkspaceSource( requirement.name.clone(), SourceKind::Git, ), ))); } Source::Url { .. } => { return Either::Left(std::iter::once(Err( LoweringError::NonWorkspaceSource( requirement.name.clone(), SourceKind::Url, ), ))); } Source::Path { .. } => { return Either::Left(std::iter::once(Err( LoweringError::NonWorkspaceSource( requirement.name.clone(), SourceKind::Path, ), ))); } Source::Registry { .. } => { return Either::Left(std::iter::once(Err( LoweringError::NonWorkspaceSource( requirement.name.clone(), SourceKind::Registry, ), ))); } Source::Workspace { .. } => { // OK } } } } } let Some(sources) = sources else { return Either::Left(std::iter::once(Ok(Self(Requirement::from(requirement))))); }; // Determine whether the markers cover the full space for the requirement. If not, fill the // remaining space with the negation of the sources. let remaining = { // Determine the space covered by the sources. let mut total = MarkerTree::FALSE; for source in sources.iter() { total.or(source.marker()); } // Determine the space covered by the requirement. let mut remaining = total.negate(); remaining.and(requirement.marker); Self(Requirement { marker: remaining, ..Requirement::from(requirement.clone()) }) }; Either::Right( sources .into_iter() .map(move |source| { let (source, mut marker) = match source { Source::Git { git, subdirectory, rev, tag, branch, lfs, marker, .. } => { let source = git_source( &git, subdirectory.map(Box::::from), rev, tag, branch, lfs, )?; (source, marker) } Source::Url { url, subdirectory, marker, .. } => { let source = url_source(&requirement, url, subdirectory.map(Box::::from))?; (source, marker) } Source::Path { path, editable, package, marker, .. } => { let source = path_source( path, git_member, origin, project_dir, workspace.install_path(), editable, package, )?; (source, marker) } Source::Registry { index, marker, extra, group, } => { // Identify the named index from either the project indexes or the workspace indexes, // in that order. let Some(index) = locations .indexes() .filter(|index| matches!(index.origin, Some(Origin::Cli))) .chain(project_indexes.iter()) .chain(workspace.indexes().iter()) .find(|Index { name, .. }| { name.as_ref().is_some_and(|name| *name == index) }) else { return Err(LoweringError::MissingIndex( requirement.name.clone(), index, )); }; if let Some(credentials) = index.credentials() { credentials_cache.store_credentials(index.raw_url(), credentials); } let index = IndexMetadata { url: index.url.clone(), format: index.format, }; let conflict = project_name.and_then(|project_name| { if let Some(extra) = extra { Some(ConflictItem::from((project_name.clone(), extra))) } else { group.map(|group| { ConflictItem::from((project_name.clone(), group)) }) } }); let source = registry_source(&requirement, index, conflict); (source, marker) } Source::Workspace { workspace: is_workspace, marker, .. } => { if !is_workspace { return Err(LoweringError::WorkspaceFalse); } let member = workspace .packages() .get(&requirement.name) .ok_or_else(|| { LoweringError::UndeclaredWorkspacePackage( requirement.name.clone(), ) })? .clone(); // Say we have: // ``` // root // ├── main_workspace <- We want to the path from here ... // │ ├── pyproject.toml // │ └── uv.lock // └──current_workspace // └── packages // └── current_package <- ... to here. // └── pyproject.toml // ``` // The path we need in the lockfile: `../current_workspace/packages/current_project` // member root: `/root/current_workspace/packages/current_project` // workspace install root: `/root/current_workspace` // relative to workspace: `packages/current_project` // workspace lock root: `../current_workspace` // relative to main workspace: `../current_workspace/packages/current_project` let url = VerbatimUrl::from_absolute_path(member.root())?; let install_path = url.to_file_path().map_err(|()| { LoweringError::RelativeTo(io::Error::other( "Invalid path in file URL", )) })?; let source = if let Some(git_member) = &git_member { // If the workspace comes from a Git dependency, all workspace // members need to be Git dependencies, too. let subdirectory = uv_fs::relative_to(member.root(), git_member.fetch_root) .expect("Workspace member must be relative"); let subdirectory = uv_fs::normalize_path_buf(subdirectory); RequirementSource::Git { git: git_member.git_source.git.clone(), subdirectory: if subdirectory == PathBuf::new() { None } else { Some(subdirectory.into_boxed_path()) }, url, } } else { let value = workspace.required_members().get(&requirement.name); let is_required_member = value.is_some(); let editability = value.copied().flatten(); if member.pyproject_toml().is_package(!is_required_member) { RequirementSource::Directory { install_path: install_path.into_boxed_path(), url, editable: Some(editability.unwrap_or(true)), r#virtual: Some(false), } } else { RequirementSource::Directory { install_path: install_path.into_boxed_path(), url, editable: Some(false), r#virtual: Some(true), } } }; (source, marker) } }; marker.and(requirement.marker); Ok(Self(Requirement { name: requirement.name.clone(), extras: requirement.extras.clone(), groups: Box::new([]), marker, source, origin: requirement.origin.clone(), })) }) .chain(std::iter::once(Ok(remaining))) .filter(|requirement| match requirement { Ok(requirement) => !requirement.0.marker.is_false(), Err(_) => true, }), ) } /// Lower a [`uv_pep508::Requirement`] in a non-workspace setting (for example, in a PEP 723 /// script, which runs in an isolated context). pub fn from_non_workspace_requirement<'data>( requirement: uv_pep508::Requirement, dir: &'data Path, sources: &'data BTreeMap, indexes: &'data [Index], locations: &'data IndexLocations, credentials_cache: &'data CredentialsCache, ) -> impl Iterator> + 'data { let source = sources.get(&requirement.name).cloned(); let Some(source) = source else { return Either::Left(std::iter::once(Ok(Self(Requirement::from(requirement))))); }; // If the source only applies to a given extra, filter it out. let source = source .iter() .filter(|source| { source.extra().is_none_or(|target| { requirement .marker .top_level_extra_name() .is_some_and(|extra| &*extra == target) }) }) .cloned() .collect::(); // Determine whether the markers cover the full space for the requirement. If not, fill the // remaining space with the negation of the sources. let remaining = { // Determine the space covered by the sources. let mut total = MarkerTree::FALSE; for source in source.iter() { total.or(source.marker()); } // Determine the space covered by the requirement. let mut remaining = total.negate(); remaining.and(requirement.marker); Self(Requirement { marker: remaining, ..Requirement::from(requirement.clone()) }) }; Either::Right( source .into_iter() .map(move |source| { let (source, mut marker) = match source { Source::Git { git, subdirectory, rev, tag, branch, lfs, marker, .. } => { let source = git_source( &git, subdirectory.map(Box::::from), rev, tag, branch, lfs, )?; (source, marker) } Source::Url { url, subdirectory, marker, .. } => { let source = url_source(&requirement, url, subdirectory.map(Box::::from))?; (source, marker) } Source::Path { path, editable, package, marker, .. } => { let source = path_source( path, None, RequirementOrigin::Project, dir, dir, editable, package, )?; (source, marker) } Source::Registry { index, marker, .. } => { let Some(index) = locations .indexes() .filter(|index| matches!(index.origin, Some(Origin::Cli))) .chain(indexes.iter()) .find(|Index { name, .. }| { name.as_ref().is_some_and(|name| *name == index) }) else { return Err(LoweringError::MissingIndex( requirement.name.clone(), index, )); }; if let Some(credentials) = index.credentials() { credentials_cache.store_credentials(index.raw_url(), credentials); } let index = IndexMetadata { url: index.url.clone(), format: index.format, }; let conflict = None; let source = registry_source(&requirement, index, conflict); (source, marker) } Source::Workspace { .. } => { return Err(LoweringError::WorkspaceMember); } }; marker.and(requirement.marker); Ok(Self(Requirement { name: requirement.name.clone(), extras: requirement.extras.clone(), groups: Box::new([]), marker, source, origin: requirement.origin.clone(), })) }) .chain(std::iter::once(Ok(remaining))) .filter(|requirement| match requirement { Ok(requirement) => !requirement.0.marker.is_false(), Err(_) => true, }), ) } /// Convert back into a [`Requirement`]. pub fn into_inner(self) -> Requirement { self.0 } } /// An error parsing and merging `tool.uv.sources` with /// `project.{dependencies,optional-dependencies}`. #[derive(Debug, Error)] pub enum LoweringError { #[error( "`{0}` is included as a workspace member, but is missing an entry in `tool.uv.sources` (e.g., `{0} = {{ workspace = true }}`)" )] MissingWorkspaceSource(PackageName), #[error( "`{0}` is included as a workspace member, but references a {1} in `tool.uv.sources`. Workspace members must be declared as workspace sources (e.g., `{0} = {{ workspace = true }}`)." )] NonWorkspaceSource(PackageName, SourceKind), #[error( "`{0}` references a workspace in `tool.uv.sources` (e.g., `{0} = {{ workspace = true }}`), but is not a workspace member" )] UndeclaredWorkspacePackage(PackageName), #[error("Can only specify one of: `rev`, `tag`, or `branch`")] MoreThanOneGitRef, #[error(transparent)] GitUrlParse(#[from] GitUrlParseError), #[error("Package `{0}` references an undeclared index: `{1}`")] MissingIndex(PackageName, IndexName), #[error("Workspace members are not allowed in non-workspace contexts")] WorkspaceMember, #[error(transparent)] InvalidUrl(#[from] DisplaySafeUrlError), #[error(transparent)] InvalidVerbatimUrl(#[from] uv_pep508::VerbatimUrlError), #[error("Fragments are not allowed in URLs: `{0}`")] ForbiddenFragment(DisplaySafeUrl), #[error( "`{0}` is associated with a URL source, but references a Git repository. Consider using a Git source instead (e.g., `{0} = {{ git = \"{1}\" }}`)" )] MissingGitSource(PackageName, DisplaySafeUrl), #[error("`workspace = false` is not yet supported")] WorkspaceFalse, #[error("Source with `editable = true` must refer to a local directory, not a file: `{0}`")] EditableFile(String), #[error("Source with `package = true` must refer to a local directory, not a file: `{0}`")] PackagedFile(String), #[error( "Git repository references local file source, but only directories are supported as transitive Git dependencies: `{0}`" )] GitFile(String), #[error(transparent)] ParsedUrl(#[from] ParsedUrlError), #[error("Path must be UTF-8: `{0}`")] NonUtf8Path(PathBuf), #[error(transparent)] // Function attaches the context RelativeTo(io::Error), } #[derive(Debug, Copy, Clone)] pub enum SourceKind { Path, Url, Git, Registry, } impl std::fmt::Display for SourceKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Path => write!(f, "path"), Self::Url => write!(f, "URL"), Self::Git => write!(f, "Git"), Self::Registry => write!(f, "registry"), } } } /// Convert a Git source into a [`RequirementSource`]. fn git_source( git: &DisplaySafeUrl, subdirectory: Option>, rev: Option, tag: Option, branch: Option, lfs: Option, ) -> Result { let reference = match (rev, tag, branch) { (None, None, None) => GitReference::DefaultBranch, (Some(rev), None, None) => GitReference::from_rev(rev), (None, Some(tag), None) => GitReference::Tag(tag), (None, None, Some(branch)) => GitReference::Branch(branch), _ => return Err(LoweringError::MoreThanOneGitRef), }; // Create a PEP 508-compatible URL. let mut url = DisplaySafeUrl::parse(&format!("git+{git}"))?; if let Some(rev) = reference.as_str() { let path = format!("{}@{}", url.path(), rev); url.set_path(&path); } let mut frags: Vec = Vec::new(); if let Some(subdirectory) = subdirectory.as_ref() { let subdirectory = subdirectory .to_str() .ok_or_else(|| LoweringError::NonUtf8Path(subdirectory.to_path_buf()))?; frags.push(format!("subdirectory={subdirectory}")); } // Loads Git LFS Enablement according to priority. // First: lfs = true, lfs = false from pyproject.toml // Second: UV_GIT_LFS from environment let lfs = GitLfs::from(lfs); // Preserve that we're using Git LFS in the Verbatim Url representations if lfs.enabled() { frags.push("lfs=true".to_string()); } if !frags.is_empty() { url.set_fragment(Some(&frags.join("&"))); } let url = VerbatimUrl::from_url(url); let repository = git.clone(); Ok(RequirementSource::Git { url, git: GitUrl::from_fields(repository, reference, None, lfs)?, subdirectory, }) } /// Convert a URL source into a [`RequirementSource`]. fn url_source( requirement: &uv_pep508::Requirement, url: DisplaySafeUrl, subdirectory: Option>, ) -> Result { let mut verbatim_url = url.clone(); if verbatim_url.fragment().is_some() { return Err(LoweringError::ForbiddenFragment(url)); } if let Some(subdirectory) = subdirectory.as_ref() { let subdirectory = subdirectory .to_str() .ok_or_else(|| LoweringError::NonUtf8Path(subdirectory.to_path_buf()))?; verbatim_url.set_fragment(Some(&format!("subdirectory={subdirectory}"))); } let ext = match DistExtension::from_path(url.path()) { Ok(ext) => ext, Err(..) if looks_like_git_repository(&url) => { return Err(LoweringError::MissingGitSource( requirement.name.clone(), url.clone(), )); } Err(err) => { return Err(ParsedUrlError::MissingExtensionUrl(url.to_string(), err).into()); } }; let verbatim_url = VerbatimUrl::from_url(verbatim_url); Ok(RequirementSource::Url { location: url, subdirectory, ext, url: verbatim_url, }) } /// Convert a registry source into a [`RequirementSource`]. fn registry_source( requirement: &uv_pep508::Requirement, index: IndexMetadata, conflict: Option, ) -> RequirementSource { match &requirement.version_or_url { None => RequirementSource::Registry { specifier: VersionSpecifiers::empty(), index: Some(index), conflict, }, Some(VersionOrUrl::VersionSpecifier(version)) => RequirementSource::Registry { specifier: version.clone(), index: Some(index), conflict, }, Some(VersionOrUrl::Url(_)) => RequirementSource::Registry { specifier: VersionSpecifiers::empty(), index: Some(index), conflict, }, } } /// Convert a path string to a file or directory source. fn path_source( path: impl AsRef, git_member: Option<&GitWorkspaceMember>, origin: RequirementOrigin, project_dir: &Path, workspace_root: &Path, editable: Option, package: Option, ) -> Result { let path = path.as_ref(); let base = match origin { RequirementOrigin::Project => project_dir, RequirementOrigin::Workspace => workspace_root, }; let url = VerbatimUrl::from_path(path, base)?.with_given(path.to_string_lossy()); let install_path = url .to_file_path() .map_err(|()| LoweringError::RelativeTo(io::Error::other("Invalid path in file URL")))?; let is_dir = if let Ok(metadata) = install_path.metadata() { metadata.is_dir() } else { install_path.extension().is_none() }; if is_dir { if let Some(git_member) = git_member { let git = git_member.git_source.git.clone(); let subdirectory = uv_fs::relative_to(install_path, git_member.fetch_root) .expect("Workspace member must be relative"); let subdirectory = uv_fs::normalize_path_buf(subdirectory); let subdirectory = if subdirectory == PathBuf::new() { None } else { Some(subdirectory.into_boxed_path()) }; let url = DisplaySafeUrl::from(ParsedGitUrl { url: git.clone(), subdirectory: subdirectory.clone(), }); return Ok(RequirementSource::Git { git, subdirectory, url: VerbatimUrl::from_url(url), }); } if editable == Some(true) { Ok(RequirementSource::Directory { install_path: install_path.into_boxed_path(), url, editable, r#virtual: Some(false), }) } else { // Determine whether the project is a package or virtual. // If the `package` option is unset, check if `tool.uv.package` is set // on the path source (otherwise, default to `true`). let is_package = package.unwrap_or_else(|| { let pyproject_path = install_path.join("pyproject.toml"); fs_err::read_to_string(&pyproject_path) .ok() .and_then(|contents| PyProjectToml::from_string(contents).ok()) // We don't require a build system for path dependencies .map(|pyproject_toml| pyproject_toml.is_package(false)) .unwrap_or(true) }); // If the project is not a package, treat it as a virtual dependency. let r#virtual = !is_package; Ok(RequirementSource::Directory { install_path: install_path.into_boxed_path(), url, editable: Some(false), r#virtual: Some(r#virtual), }) } } else { // TODO(charlie): If a Git repo contains a source that points to a file, what should we do? if git_member.is_some() { return Err(LoweringError::GitFile(url.to_string())); } if editable == Some(true) { return Err(LoweringError::EditableFile(url.to_string())); } if package == Some(true) { return Err(LoweringError::PackagedFile(url.to_string())); } Ok(RequirementSource::Path { ext: DistExtension::from_path(&install_path) .map_err(|err| ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err))?, install_path: install_path.into_boxed_path(), url, }) } } uv-0.9.17+ds1/crates/uv-distribution/src/metadata/mod.rs000066400000000000000000000150121520155276700230620ustar00rootroot00000000000000use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use thiserror::Error; use uv_auth::CredentialsCache; use uv_configuration::SourceStrategy; use uv_distribution_types::{GitSourceUrl, IndexLocations, Requirement}; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pypi_types::{HashDigests, ResolutionMetadata}; use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::{WorkspaceCache, WorkspaceError}; pub use crate::metadata::build_requires::{BuildRequires, LoweredExtraBuildDependencies}; pub use crate::metadata::dependency_groups::SourcedDependencyGroups; pub use crate::metadata::lowering::LoweredRequirement; pub use crate::metadata::lowering::LoweringError; pub use crate::metadata::requires_dist::{FlatRequiresDist, RequiresDist}; mod build_requires; mod dependency_groups; mod lowering; mod requires_dist; #[derive(Debug, Error)] pub enum MetadataError { #[error(transparent)] Workspace(#[from] WorkspaceError), #[error(transparent)] DependencyGroup(#[from] DependencyGroupError), #[error("No pyproject.toml found at: {0}")] MissingPyprojectToml(PathBuf), #[error("Failed to parse entry: `{0}`")] LoweringError(PackageName, #[source] Box), #[error("Failed to parse entry in group `{0}`: `{1}`")] GroupLoweringError(GroupName, PackageName, #[source] Box), #[error( "Source entry for `{0}` only applies to extra `{1}`, but the `{1}` extra does not exist. When an extra is present on a source (e.g., `extra = \"{1}\"`), the relevant package must be included in the `project.optional-dependencies` section for that extra (e.g., `project.optional-dependencies = {{ \"{1}\" = [\"{0}\"] }}`)." )] MissingSourceExtra(PackageName, ExtraName), #[error( "Source entry for `{0}` only applies to extra `{1}`, but `{0}` was not found under the `project.optional-dependencies` section for that extra. When an extra is present on a source (e.g., `extra = \"{1}\"`), the relevant package must be included in the `project.optional-dependencies` section for that extra (e.g., `project.optional-dependencies = {{ \"{1}\" = [\"{0}\"] }}`)." )] IncompleteSourceExtra(PackageName, ExtraName), #[error( "Source entry for `{0}` only applies to dependency group `{1}`, but the `{1}` group does not exist. When a group is present on a source (e.g., `group = \"{1}\"`), the relevant package must be included in the `dependency-groups` section for that extra (e.g., `dependency-groups = {{ \"{1}\" = [\"{0}\"] }}`)." )] MissingSourceGroup(PackageName, GroupName), #[error( "Source entry for `{0}` only applies to dependency group `{1}`, but `{0}` was not found under the `dependency-groups` section for that group. When a group is present on a source (e.g., `group = \"{1}\"`), the relevant package must be included in the `dependency-groups` section for that extra (e.g., `dependency-groups = {{ \"{1}\" = [\"{0}\"] }}`)." )] IncompleteSourceGroup(PackageName, GroupName), } #[derive(Debug, Clone)] pub struct Metadata { // Mandatory fields pub name: PackageName, pub version: Version, // Optional fields pub requires_dist: Box<[Requirement]>, pub requires_python: Option, pub provides_extra: Box<[ExtraName]>, pub dependency_groups: BTreeMap>, pub dynamic: bool, } impl Metadata { /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive /// dependencies. pub fn from_metadata23(metadata: ResolutionMetadata) -> Self { Self { name: metadata.name, version: metadata.version, requires_dist: Box::into_iter(metadata.requires_dist) .map(Requirement::from) .collect(), requires_python: metadata.requires_python, provides_extra: metadata.provides_extra, dependency_groups: BTreeMap::default(), dynamic: metadata.dynamic, } } /// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory /// dependencies. pub async fn from_workspace( metadata: ResolutionMetadata, install_path: &Path, git_source: Option<&GitWorkspaceMember<'_>>, locations: &IndexLocations, sources: SourceStrategy, cache: &WorkspaceCache, credentials_cache: &CredentialsCache, ) -> Result { // Lower the requirements. let requires_dist = uv_pypi_types::RequiresDist { name: metadata.name, requires_dist: metadata.requires_dist, provides_extra: metadata.provides_extra, dynamic: metadata.dynamic, }; let RequiresDist { name, requires_dist, provides_extra, dependency_groups, dynamic, } = RequiresDist::from_project_maybe_workspace( requires_dist, install_path, git_source, locations, sources, cache, credentials_cache, ) .await?; // Combine with the remaining metadata. Ok(Self { name, version: metadata.version, requires_dist, requires_python: metadata.requires_python, provides_extra, dependency_groups, dynamic, }) } } /// The metadata associated with an archive. #[derive(Debug, Clone)] pub struct ArchiveMetadata { /// The [`Metadata`] for the underlying distribution. pub metadata: Metadata, /// The hashes of the source or built archive. pub hashes: HashDigests, } impl ArchiveMetadata { /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive /// dependencies. pub fn from_metadata23(metadata: ResolutionMetadata) -> Self { Self { metadata: Metadata::from_metadata23(metadata), hashes: HashDigests::empty(), } } } impl From for ArchiveMetadata { fn from(metadata: Metadata) -> Self { Self { metadata, hashes: HashDigests::empty(), } } } /// A workspace member from a checked-out Git repo. #[derive(Debug, Clone)] pub struct GitWorkspaceMember<'a> { /// The root of the checkout, which may be the root of the workspace or may be above the /// workspace root. pub fetch_root: &'a Path, pub git_source: &'a GitSourceUrl<'a>, } uv-0.9.17+ds1/crates/uv-distribution/src/metadata/requires_dist.rs000066400000000000000000000735271520155276700252040ustar00rootroot00000000000000use std::collections::{BTreeMap, VecDeque}; use std::path::Path; use std::slice; use rustc_hash::FxHashSet; use uv_auth::CredentialsCache; use uv_configuration::SourceStrategy; use uv_distribution_types::{IndexLocations, Requirement}; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep508::MarkerTree; use uv_workspace::dependency_groups::FlatDependencyGroups; use uv_workspace::pyproject::{Sources, ToolUvSources}; use uv_workspace::{DiscoveryOptions, MemberDiscovery, ProjectWorkspace, WorkspaceCache}; use crate::Metadata; use crate::metadata::{GitWorkspaceMember, LoweredRequirement, MetadataError}; #[derive(Debug, Clone)] pub struct RequiresDist { pub name: PackageName, pub requires_dist: Box<[Requirement]>, pub provides_extra: Box<[ExtraName]>, pub dependency_groups: BTreeMap>, pub dynamic: bool, } impl RequiresDist { /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive /// dependencies. pub fn from_metadata23(metadata: uv_pypi_types::RequiresDist) -> Self { Self { name: metadata.name, requires_dist: Box::into_iter(metadata.requires_dist) .map(Requirement::from) .collect(), provides_extra: metadata.provides_extra, dependency_groups: BTreeMap::default(), dynamic: metadata.dynamic, } } /// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory /// dependencies. pub async fn from_project_maybe_workspace( metadata: uv_pypi_types::RequiresDist, install_path: &Path, git_member: Option<&GitWorkspaceMember<'_>>, locations: &IndexLocations, sources: SourceStrategy, cache: &WorkspaceCache, credentials_cache: &CredentialsCache, ) -> Result { let discovery = DiscoveryOptions { stop_discovery_at: git_member.map(|git_member| { git_member .fetch_root .parent() .expect("git checkout has a parent") .to_path_buf() }), members: match sources { SourceStrategy::Enabled => MemberDiscovery::default(), SourceStrategy::Disabled => MemberDiscovery::None, }, ..DiscoveryOptions::default() }; let Some(project_workspace) = ProjectWorkspace::from_maybe_project_root(install_path, &discovery, cache).await? else { return Ok(Self::from_metadata23(metadata)); }; Self::from_project_workspace( metadata, &project_workspace, git_member, locations, sources, credentials_cache, ) } fn from_project_workspace( metadata: uv_pypi_types::RequiresDist, project_workspace: &ProjectWorkspace, git_member: Option<&GitWorkspaceMember<'_>>, locations: &IndexLocations, source_strategy: SourceStrategy, credentials_cache: &CredentialsCache, ) -> Result { // Collect any `tool.uv.index` entries. let empty = vec![]; let project_indexes = match source_strategy { SourceStrategy::Enabled => project_workspace .current_project() .pyproject_toml() .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) .and_then(|uv| uv.index.as_deref()) .unwrap_or(&empty), SourceStrategy::Disabled => &empty, }; // Collect any `tool.uv.sources` and `tool.uv.dev_dependencies` from `pyproject.toml`. let empty = BTreeMap::default(); let project_sources = match source_strategy { SourceStrategy::Enabled => project_workspace .current_project() .pyproject_toml() .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) .and_then(|uv| uv.sources.as_ref()) .map(ToolUvSources::inner) .unwrap_or(&empty), SourceStrategy::Disabled => &empty, }; let dependency_groups = FlatDependencyGroups::from_pyproject_toml( project_workspace.current_project().root(), project_workspace.current_project().pyproject_toml(), )?; // Now that we've resolved the dependency groups, we can validate that each source references // a valid extra or group, if present. Self::validate_sources(project_sources, &metadata, &dependency_groups)?; // Lower the dependency groups. let dependency_groups = dependency_groups .into_iter() .map(|(name, flat_group)| { let requirements = match source_strategy { SourceStrategy::Enabled => flat_group .requirements .into_iter() .flat_map(|requirement| { let requirement_name = requirement.name.clone(); let group = name.clone(); let extra = None; LoweredRequirement::from_requirement( requirement, Some(&metadata.name), project_workspace.project_root(), project_sources, project_indexes, extra, Some(&group), locations, project_workspace.workspace(), git_member, credentials_cache, ) .map( move |requirement| match requirement { Ok(requirement) => Ok(requirement.into_inner()), Err(err) => Err(MetadataError::GroupLoweringError( group.clone(), requirement_name.clone(), Box::new(err), )), }, ) }) .collect::, _>>(), SourceStrategy::Disabled => Ok(flat_group .requirements .into_iter() .map(Requirement::from) .collect()), }?; Ok::<(GroupName, Box<_>), MetadataError>((name, requirements)) }) .collect::, _>>()?; // Lower the requirements. let requires_dist = Box::into_iter(metadata.requires_dist); let requires_dist = match source_strategy { SourceStrategy::Enabled => requires_dist .flat_map(|requirement| { let requirement_name = requirement.name.clone(); let extra = requirement.marker.top_level_extra_name(); let group = None; LoweredRequirement::from_requirement( requirement, Some(&metadata.name), project_workspace.project_root(), project_sources, project_indexes, extra.as_deref(), group, locations, project_workspace.workspace(), git_member, credentials_cache, ) .map(move |requirement| match requirement { Ok(requirement) => Ok(requirement.into_inner()), Err(err) => Err(MetadataError::LoweringError( requirement_name.clone(), Box::new(err), )), }) }) .collect::, _>>()?, SourceStrategy::Disabled => requires_dist.into_iter().map(Requirement::from).collect(), }; Ok(Self { name: metadata.name, requires_dist, dependency_groups, provides_extra: metadata.provides_extra, dynamic: metadata.dynamic, }) } /// Validate the sources for a given [`uv_pypi_types::RequiresDist`]. /// /// If a source is requested with an `extra` or `group`, ensure that the relevant dependency is /// present in the relevant `project.optional-dependencies` or `dependency-groups` section. fn validate_sources( sources: &BTreeMap, metadata: &uv_pypi_types::RequiresDist, dependency_groups: &FlatDependencyGroups, ) -> Result<(), MetadataError> { for (name, sources) in sources { for source in sources.iter() { if let Some(extra) = source.extra() { // If the extra doesn't exist at all, error. if !metadata.provides_extra.contains(extra) { return Err(MetadataError::MissingSourceExtra( name.clone(), extra.clone(), )); } // If there is no such requirement with the extra, error. if !metadata.requires_dist.iter().any(|requirement| { requirement.name == *name && requirement.marker.top_level_extra_name().as_deref() == Some(extra) }) { return Err(MetadataError::IncompleteSourceExtra( name.clone(), extra.clone(), )); } } if let Some(group) = source.group() { // If the group doesn't exist at all, error. let Some(flat_group) = dependency_groups.get(group) else { return Err(MetadataError::MissingSourceGroup( name.clone(), group.clone(), )); }; // If there is no such requirement with the group, error. if !flat_group .requirements .iter() .any(|requirement| requirement.name == *name) { return Err(MetadataError::IncompleteSourceGroup( name.clone(), group.clone(), )); } } } } Ok(()) } } impl From for RequiresDist { fn from(metadata: Metadata) -> Self { Self { name: metadata.name, requires_dist: metadata.requires_dist, provides_extra: metadata.provides_extra, dependency_groups: metadata.dependency_groups, dynamic: metadata.dynamic, } } } /// Like [`uv_pypi_types::RequiresDist`], but with any recursive (or self-referential) dependencies /// resolved. /// /// For example, given: /// ```toml /// [project] /// name = "example" /// version = "0.1.0" /// requires-python = ">=3.13.0" /// dependencies = [] /// /// [project.optional-dependencies] /// all = [ /// "example[async]", /// ] /// async = [ /// "fastapi", /// ] /// ``` /// /// A build backend could return: /// ```txt /// Metadata-Version: 2.2 /// Name: example /// Version: 0.1.0 /// Requires-Python: >=3.13.0 /// Provides-Extra: all /// Requires-Dist: example[async]; extra == "all" /// Provides-Extra: async /// Requires-Dist: fastapi; extra == "async" /// ``` /// /// Or: /// ```txt /// Metadata-Version: 2.4 /// Name: example /// Version: 0.1.0 /// Requires-Python: >=3.13.0 /// Provides-Extra: all /// Requires-Dist: fastapi; extra == 'all' /// Provides-Extra: async /// Requires-Dist: fastapi; extra == 'async' /// ``` /// /// The [`FlatRequiresDist`] struct is used to flatten out the recursive dependencies, i.e., convert /// from the former to the latter. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FlatRequiresDist(Box<[Requirement]>); impl FlatRequiresDist { /// Flatten a set of requirements, resolving any self-references. pub fn from_requirements(requirements: Box<[Requirement]>, name: &PackageName) -> Self { // If there are no self-references, we can return early. if requirements.iter().all(|req| req.name != *name) { return Self(requirements); } // Memoize the top level extras, in the same order as `requirements` let top_level_extras: Vec<_> = requirements .iter() .map(|req| req.marker.top_level_extra_name()) .collect(); // Transitively process all extras that are recursively included. let mut flattened = requirements.to_vec(); let mut seen = FxHashSet::<(ExtraName, MarkerTree)>::default(); let mut queue: VecDeque<_> = flattened .iter() .filter(|req| req.name == *name) .flat_map(|req| req.extras.iter().cloned().map(|extra| (extra, req.marker))) .collect(); while let Some((extra, marker)) = queue.pop_front() { if !seen.insert((extra.clone(), marker)) { continue; } // Find the requirements for the extra. for (requirement, top_level_extra) in requirements.iter().zip(top_level_extras.iter()) { if top_level_extra.as_deref() != Some(&extra) { continue; } let requirement = { let mut marker = marker; marker.and(requirement.marker); Requirement { name: requirement.name.clone(), extras: requirement.extras.clone(), groups: requirement.groups.clone(), source: requirement.source.clone(), origin: requirement.origin.clone(), marker: marker.simplify_extras(slice::from_ref(&extra)), } }; if requirement.name == *name { // Add each transitively included extra. queue.extend( requirement .extras .iter() .cloned() .map(|extra| (extra, requirement.marker)), ); } else { // Add the requirements for that extra. flattened.push(requirement); } } } // Drop all the self-references now that we've flattened them out. flattened.retain(|req| req.name != *name); // Retain any self-constraints for that extra, e.g., if `project[foo]` includes // `project[bar]>1.0`, as a dependency, we need to propagate `project>1.0`, in addition to // transitively expanding `project[bar]`. for req in &requirements { if req.name == *name { if !req.source.is_empty() { flattened.push(Requirement { name: req.name.clone(), extras: Box::new([]), groups: req.groups.clone(), source: req.source.clone(), origin: req.origin.clone(), marker: req.marker, }); } } } Self(flattened.into_boxed_slice()) } /// Consume the [`FlatRequiresDist`] and return the inner requirements. pub fn into_inner(self) -> Box<[Requirement]> { self.0 } } impl IntoIterator for FlatRequiresDist { type Item = Requirement; type IntoIter = as IntoIterator>::IntoIter; fn into_iter(self) -> Self::IntoIter { Box::into_iter(self.0) } } #[cfg(test)] mod test { use std::path::Path; use std::str::FromStr; use anyhow::Context; use indoc::indoc; use insta::assert_snapshot; use uv_auth::CredentialsCache; use uv_configuration::SourceStrategy; use uv_distribution_types::IndexLocations; use uv_normalize::PackageName; use uv_pep508::Requirement; use uv_workspace::pyproject::PyProjectToml; use uv_workspace::{DiscoveryOptions, ProjectWorkspace, WorkspaceCache}; use crate::RequiresDist; use crate::metadata::requires_dist::FlatRequiresDist; async fn requires_dist_from_pyproject_toml(contents: &str) -> anyhow::Result { let pyproject_toml = PyProjectToml::from_string(contents.to_string())?; let path = Path::new("pyproject.toml"); let project_workspace = ProjectWorkspace::from_project( path, pyproject_toml .project .as_ref() .context("metadata field project not found")?, &pyproject_toml, &DiscoveryOptions { stop_discovery_at: Some(path.to_path_buf()), ..DiscoveryOptions::default() }, &WorkspaceCache::default(), ) .await?; let pyproject_toml = uv_pypi_types::PyProjectToml::from_toml(contents)?; let requires_dist = uv_pypi_types::RequiresDist::from_pyproject_toml(pyproject_toml)?; Ok(RequiresDist::from_project_workspace( requires_dist, &project_workspace, None, &IndexLocations::default(), SourceStrategy::default(), &CredentialsCache::new(), )?) } async fn format_err(input: &str) -> String { use std::fmt::Write; let err = requires_dist_from_pyproject_toml(input).await.unwrap_err(); let mut causes = err.chain(); let mut message = String::new(); let _ = writeln!(message, "error: {}", causes.next().unwrap()); for err in causes { let _ = writeln!(message, " Caused by: {err}"); } message } #[tokio::test] async fn wrong_type() { let input = indoc! {r#" [project] name = "foo" version = "0.0.0" dependencies = [ "tqdm", ] [tool.uv.sources] tqdm = true "#}; assert_snapshot!(format_err(input).await, @r###" error: TOML parse error at line 8, column 8 | 8 | tqdm = true | ^^^^ invalid type: boolean `true`, expected a single source (as a map) or list of sources "###); } #[tokio::test] async fn too_many_git_specs() { let input = indoc! {r#" [project] name = "foo" version = "0.0.0" dependencies = [ "tqdm", ] [tool.uv.sources] tqdm = { git = "https://github.com/tqdm/tqdm", rev = "baaaaaab", tag = "v1.0.0" } "#}; assert_snapshot!(format_err(input).await, @r###" error: TOML parse error at line 8, column 8 | 8 | tqdm = { git = "https://github.com/tqdm/tqdm", rev = "baaaaaab", tag = "v1.0.0" } | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected at most one of `rev`, `tag`, or `branch` "###); } #[tokio::test] async fn too_many_git_typo() { let input = indoc! {r#" [project] name = "foo" version = "0.0.0" dependencies = [ "tqdm", ] [tool.uv.sources] tqdm = { git = "https://github.com/tqdm/tqdm", ref = "baaaaaab" } "#}; assert_snapshot!(format_err(input).await, @r#" error: TOML parse error at line 8, column 48 | 8 | tqdm = { git = "https://github.com/tqdm/tqdm", ref = "baaaaaab" } | ^^^ unknown field `ref`, expected one of `git`, `subdirectory`, `rev`, `tag`, `branch`, `lfs`, `url`, `path`, `editable`, `package`, `index`, `workspace`, `marker`, `extra`, `group` "#); } #[tokio::test] async fn extra_and_group() { let input = indoc! {r#" [project] name = "foo" version = "0.0.0" dependencies = [] [tool.uv.sources] tqdm = { git = "https://github.com/tqdm/tqdm", extra = "torch", group = "dev" } "#}; assert_snapshot!(format_err(input).await, @r###" error: TOML parse error at line 7, column 8 | 7 | tqdm = { git = "https://github.com/tqdm/tqdm", extra = "torch", group = "dev" } | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot specify both `extra` and `group` "###); } #[tokio::test] async fn you_cant_mix_those() { let input = indoc! {r#" [project] name = "foo" version = "0.0.0" dependencies = [ "tqdm", ] [tool.uv.sources] tqdm = { path = "tqdm", index = "torch" } "#}; assert_snapshot!(format_err(input).await, @r###" error: TOML parse error at line 8, column 8 | 8 | tqdm = { path = "tqdm", index = "torch" } | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot specify both `path` and `index` "###); } #[tokio::test] async fn missing_constraint() { let input = indoc! {r#" [project] name = "foo" version = "0.0.0" dependencies = [ "tqdm", ] "#}; assert!(requires_dist_from_pyproject_toml(input).await.is_ok()); } #[tokio::test] async fn invalid_syntax() { let input = indoc! {r#" [project] name = "foo" version = "0.0.0" dependencies = [ "tqdm ==4.66.0", ] [tool.uv.sources] tqdm = { url = invalid url to tqdm-4.66.0-py3-none-any.whl" } "#}; assert_snapshot!(format_err(input).await, @r#" error: TOML parse error at line 8, column 16 | 8 | tqdm = { url = invalid url to tqdm-4.66.0-py3-none-any.whl" } | ^ missing opening quote, expected `"` "#); } #[tokio::test] async fn invalid_url() { let input = indoc! {r#" [project] name = "foo" version = "0.0.0" dependencies = [ "tqdm ==4.66.0", ] [tool.uv.sources] tqdm = { url = "§invalid#+#*Ä" } "#}; assert_snapshot!(format_err(input).await, @r###" error: TOML parse error at line 8, column 16 | 8 | tqdm = { url = "§invalid#+#*Ä" } | ^^^^^^^^^^^^^^^^^ relative URL without a base: "§invalid#+#*Ä" "###); } #[tokio::test] async fn workspace_and_url_spec() { let input = indoc! {r#" [project] name = "foo" version = "0.0.0" dependencies = [ "tqdm @ git+https://github.com/tqdm/tqdm", ] [tool.uv.sources] tqdm = { workspace = true } "#}; assert_snapshot!(format_err(input).await, @r###" error: Failed to parse entry: `tqdm` Caused by: `tqdm` references a workspace in `tool.uv.sources` (e.g., `tqdm = { workspace = true }`), but is not a workspace member "###); } #[tokio::test] async fn missing_workspace_package() { let input = indoc! {r#" [project] name = "foo" version = "0.0.0" dependencies = [ "tqdm ==4.66.0", ] [tool.uv.sources] tqdm = { workspace = true } "#}; assert_snapshot!(format_err(input).await, @r###" error: Failed to parse entry: `tqdm` Caused by: `tqdm` references a workspace in `tool.uv.sources` (e.g., `tqdm = { workspace = true }`), but is not a workspace member "###); } #[tokio::test] async fn cant_be_dynamic() { let input = indoc! {r#" [project] name = "foo" version = "0.0.0" dynamic = [ "dependencies" ] [tool.uv.sources] tqdm = { workspace = true } "#}; assert_snapshot!(format_err(input).await, @r###" error: The following field was marked as dynamic: dependencies "###); } #[tokio::test] async fn missing_project_section() { let input = indoc! {" [tool.uv.sources] tqdm = { workspace = true } "}; assert_snapshot!(format_err(input).await, @r###" error: metadata field project not found "###); } #[test] fn test_flat_requires_dist_noop() { let name = PackageName::from_str("pkg").unwrap(); let requirements = [ Requirement::from_str("requests>=2.0.0").unwrap().into(), Requirement::from_str("pytest; extra == 'test'") .unwrap() .into(), Requirement::from_str("black; extra == 'dev'") .unwrap() .into(), ]; let expected = FlatRequiresDist( [ Requirement::from_str("requests>=2.0.0").unwrap().into(), Requirement::from_str("pytest; extra == 'test'") .unwrap() .into(), Requirement::from_str("black; extra == 'dev'") .unwrap() .into(), ] .into(), ); let actual = FlatRequiresDist::from_requirements(requirements.into(), &name); assert_eq!(actual, expected); } #[test] fn test_flat_requires_dist_basic() { let name = PackageName::from_str("pkg").unwrap(); let requirements = [ Requirement::from_str("requests>=2.0.0").unwrap().into(), Requirement::from_str("pytest; extra == 'test'") .unwrap() .into(), Requirement::from_str("pkg[dev]; extra == 'test'") .unwrap() .into(), Requirement::from_str("black; extra == 'dev'") .unwrap() .into(), ]; let expected = FlatRequiresDist( [ Requirement::from_str("requests>=2.0.0").unwrap().into(), Requirement::from_str("pytest; extra == 'test'") .unwrap() .into(), Requirement::from_str("black; extra == 'dev'") .unwrap() .into(), Requirement::from_str("black; extra == 'test'") .unwrap() .into(), ] .into(), ); let actual = FlatRequiresDist::from_requirements(requirements.into(), &name); assert_eq!(actual, expected); } #[test] fn test_flat_requires_dist_with_markers() { let name = PackageName::from_str("pkg").unwrap(); let requirements = vec![ Requirement::from_str("requests>=2.0.0").unwrap().into(), Requirement::from_str("pytest; extra == 'test'") .unwrap() .into(), Requirement::from_str("pkg[dev]; extra == 'test' and sys_platform == 'win32'") .unwrap() .into(), Requirement::from_str("black; extra == 'dev' and sys_platform == 'win32'") .unwrap() .into(), ]; let expected = FlatRequiresDist( [ Requirement::from_str("requests>=2.0.0").unwrap().into(), Requirement::from_str("pytest; extra == 'test'") .unwrap() .into(), Requirement::from_str("black; extra == 'dev' and sys_platform == 'win32'") .unwrap() .into(), Requirement::from_str("black; extra == 'test' and sys_platform == 'win32'") .unwrap() .into(), ] .into(), ); let actual = FlatRequiresDist::from_requirements(requirements.into(), &name); assert_eq!(actual, expected); } #[test] fn test_flat_requires_dist_self_constraint() { let name = PackageName::from_str("pkg").unwrap(); let requirements = [ Requirement::from_str("requests>=2.0.0").unwrap().into(), Requirement::from_str("pytest; extra == 'test'") .unwrap() .into(), Requirement::from_str("black; extra == 'dev'") .unwrap() .into(), Requirement::from_str("pkg[async]==1.0.0").unwrap().into(), ]; let expected = FlatRequiresDist( [ Requirement::from_str("requests>=2.0.0").unwrap().into(), Requirement::from_str("pytest; extra == 'test'") .unwrap() .into(), Requirement::from_str("black; extra == 'dev'") .unwrap() .into(), Requirement::from_str("pkg==1.0.0").unwrap().into(), ] .into(), ); let actual = FlatRequiresDist::from_requirements(requirements.into(), &name); assert_eq!(actual, expected); } } uv-0.9.17+ds1/crates/uv-distribution/src/reporter.rs000066400000000000000000000035011520155276700223650ustar00rootroot00000000000000use std::sync::Arc; use uv_distribution_types::BuildableSource; use uv_normalize::PackageName; use uv_redacted::DisplaySafeUrl; pub trait Reporter: Send + Sync { /// Callback to invoke when a source distribution build is kicked off. fn on_build_start(&self, source: &BuildableSource) -> usize; /// Callback to invoke when a source distribution build is complete. fn on_build_complete(&self, source: &BuildableSource, id: usize); /// Callback to invoke when a repository checkout begins. fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize; /// Callback to invoke when a repository checkout completes. fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize); /// Callback to invoke when a download is kicked off. fn on_download_start(&self, name: &PackageName, size: Option) -> usize; /// Callback to invoke when a download makes progress (i.e. some number of bytes are /// downloaded). fn on_download_progress(&self, id: usize, inc: u64); /// Callback to invoke when a download is complete. fn on_download_complete(&self, name: &PackageName, id: usize); } impl dyn Reporter { /// Converts this reporter to a [`uv_git::Reporter`]. pub(crate) fn into_git_reporter(self: Arc) -> Arc { Arc::new(Facade { reporter: self.clone(), }) } } /// A facade for converting from [`Reporter`] to [`uv_git::Reporter`]. struct Facade { reporter: Arc, } impl uv_git::Reporter for Facade { fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize { self.reporter.on_checkout_start(url, rev) } fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize) { self.reporter.on_checkout_complete(url, rev, id); } } uv-0.9.17+ds1/crates/uv-distribution/src/source/000077500000000000000000000000001520155276700214565ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-distribution/src/source/built_wheel_metadata.rs000066400000000000000000000064761520155276700262040ustar00rootroot00000000000000use std::path::{Path, PathBuf}; use std::str::FromStr; use uv_cache::CacheShard; use uv_cache_info::CacheInfo; use uv_distribution_filename::WheelFilename; use uv_distribution_types::{BuildInfo, Hashed}; use uv_fs::files; use uv_normalize::PackageName; use uv_pep440::Version; use uv_platform_tags::Tags; use uv_pypi_types::{HashDigest, HashDigests}; /// The information about the wheel we either just built or got from the cache. #[derive(Debug, Clone)] pub(crate) struct BuiltWheelMetadata { /// The path to the built wheel. pub(crate) path: Box, /// The expected path to the downloaded wheel's entry in the cache. pub(crate) target: Box, /// The parsed filename. pub(crate) filename: WheelFilename, /// The computed hashes of the source distribution from which the wheel was built. pub(crate) hashes: HashDigests, /// The cache information for the underlying source distribution. pub(crate) cache_info: CacheInfo, /// The build information for the wheel. pub(crate) build_info: BuildInfo, } impl BuiltWheelMetadata { /// Create a [`BuiltWheelMetadata`] from a [`BuiltWheelFile`]. pub(crate) fn from_file( file: BuiltWheelFile, hashes: HashDigests, cache_info: CacheInfo, build_info: BuildInfo, ) -> Self { Self { path: file.path, target: file.target, filename: file.filename, hashes, cache_info, build_info, } } } impl Hashed for BuiltWheelMetadata { fn hashes(&self) -> &[HashDigest] { self.hashes.as_slice() } } /// The path to a built wheel file, along with its parsed filename. #[derive(Debug, Clone)] pub(crate) struct BuiltWheelFile { /// The path to the built wheel. pub(crate) path: Box, /// The expected path to the downloaded wheel's entry in the cache. pub(crate) target: Box, /// The parsed filename. pub(crate) filename: WheelFilename, } impl BuiltWheelFile { /// Find a compatible wheel in the cache. pub(crate) fn find_in_cache( tags: &Tags, cache_shard: &CacheShard, ) -> Result, std::io::Error> { for file in files(cache_shard)? { if let Some(metadata) = Self::from_path(file, cache_shard) { // Validate that the wheel is compatible with the target platform. if metadata.filename.is_compatible(tags) { return Ok(Some(metadata)); } } } Ok(None) } /// Try to parse a distribution from a cached directory name (like `typing-extensions-4.8.0-py3-none-any.whl`). fn from_path(path: PathBuf, cache_shard: &CacheShard) -> Option { let filename = path.file_name()?.to_str()?; let filename = WheelFilename::from_str(filename).ok()?; Some(Self { target: cache_shard.join(filename.stem()).into_boxed_path(), path: path.into_boxed_path(), filename, }) } /// Returns `true` if the wheel matches the given package name and version. pub(crate) fn matches(&self, name: Option<&PackageName>, version: Option<&Version>) -> bool { name.is_none_or(|name| self.filename.name == *name) && version.is_none_or(|version| self.filename.version == *version) } } uv-0.9.17+ds1/crates/uv-distribution/src/source/mod.rs000066400000000000000000003433061520155276700226140ustar00rootroot00000000000000//! Fetch and build source distributions from remote sources. // This is to squash warnings about `|r| r.into_git_reporter()`. Clippy wants // me to eta-reduce that and write it as // `<(dyn reporter::Reporter + 'static)>::into_git_reporter` // instead. But that's a monster. On the other hand, applying this suppression // instruction more granularly is annoying. So we just slap it on the module // for now. ---AG #![allow(clippy::redundant_closure_for_method_calls)] use std::borrow::Cow; use std::ops::Bound; use std::path::Path; use std::str::FromStr; use std::sync::Arc; use fs_err::tokio as fs; use futures::{FutureExt, TryStreamExt}; use reqwest::{Response, StatusCode}; use tokio_util::compat::FuturesAsyncReadCompatExt; use tracing::{Instrument, debug, info_span, instrument, warn}; use url::Url; use zip::ZipArchive; use uv_auth::CredentialsCache; use uv_cache::{Cache, CacheBucket, CacheEntry, CacheShard, Removal, WheelCache}; use uv_cache_info::CacheInfo; use uv_client::{ CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient, }; use uv_configuration::{BuildKind, BuildOutput, SourceStrategy}; use uv_distribution_filename::{SourceDistExtension, WheelFilename}; use uv_distribution_types::{ BuildInfo, BuildVariables, BuildableSource, ConfigSettings, DirectorySourceUrl, ExtraBuildRequirement, GitSourceUrl, HashPolicy, Hashed, IndexUrl, PathSourceUrl, SourceDist, SourceUrl, }; use uv_extract::hash::Hasher; use uv_fs::{rename_with_retry, write_atomic}; use uv_git::{GIT_LFS, GitError}; use uv_git_types::{GitHubRepository, GitOid}; use uv_metadata::read_archive_metadata; use uv_normalize::PackageName; use uv_pep440::{Version, release_specifiers_to_ranges}; use uv_platform_tags::Tags; use uv_pypi_types::{HashAlgorithm, HashDigest, HashDigests, PyProjectToml, ResolutionMetadata}; use uv_redacted::DisplaySafeUrl; use uv_types::{BuildContext, BuildKey, BuildStack, SourceBuildTrait}; use uv_workspace::pyproject::ToolUvSources; use crate::distribution_database::ManagedClient; use crate::error::Error; use crate::metadata::{ArchiveMetadata, GitWorkspaceMember, Metadata}; use crate::source::built_wheel_metadata::{BuiltWheelFile, BuiltWheelMetadata}; use crate::source::revision::Revision; use crate::{Reporter, RequiresDist}; mod built_wheel_metadata; mod revision; /// Fetch and build a source distribution from a remote source, or from a local cache. pub(crate) struct SourceDistributionBuilder<'a, T: BuildContext> { build_context: &'a T, build_stack: Option<&'a BuildStack>, reporter: Option>, } /// The name of the file that contains the revision ID for a remote distribution, encoded via `MsgPack`. pub(crate) const HTTP_REVISION: &str = "revision.http"; /// The name of the file that contains the revision ID for a local distribution, encoded via `MsgPack`. pub(crate) const LOCAL_REVISION: &str = "revision.rev"; /// The name of the file that contains the cached distribution metadata, encoded via `MsgPack`. pub(crate) const METADATA: &str = "metadata.msgpack"; /// The directory within each entry under which to store the unpacked source distribution. pub(crate) const SOURCE: &str = "src"; impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { /// Initialize a [`SourceDistributionBuilder`] from a [`BuildContext`]. pub(crate) fn new(build_context: &'a T) -> Self { Self { build_context, build_stack: None, reporter: None, } } /// Set the [`BuildStack`] to use for the [`SourceDistributionBuilder`]. #[must_use] pub(crate) fn with_build_stack(self, build_stack: &'a BuildStack) -> Self { Self { build_stack: Some(build_stack), ..self } } /// Set the [`Reporter`] to use for the [`SourceDistributionBuilder`]. #[must_use] pub(crate) fn with_reporter(self, reporter: Arc) -> Self { Self { reporter: Some(reporter), ..self } } /// Download and build a [`SourceDist`]. pub(crate) async fn download_and_build( &self, source: &BuildableSource<'_>, tags: &Tags, hashes: HashPolicy<'_>, client: &ManagedClient<'_>, ) -> Result { let built_wheel_metadata = match &source { BuildableSource::Dist(SourceDist::Registry(dist)) => { // For registry source distributions, shard by package, then version, for // convenience in debugging. let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, WheelCache::Index(&dist.index) .wheel_dir(dist.name.as_ref()) .join(dist.version.to_string()), ); let url = dist.file.url.to_url()?; // If the URL is a file URL, use the local path directly. if url.scheme() == "file" { let path = url .to_file_path() .map_err(|()| Error::NonFileUrl(url.clone()))?; return self .archive( source, &PathSourceUrl { url: &url, path: Cow::Owned(path), ext: dist.ext, }, &cache_shard, tags, hashes, ) .boxed_local() .await; } self.url( source, &url, Some(&dist.index), &cache_shard, None, dist.ext, tags, hashes, client, ) .boxed_local() .await? } BuildableSource::Dist(SourceDist::DirectUrl(dist)) => { // For direct URLs, cache directly under the hash of the URL itself. let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, WheelCache::Url(&dist.url).root(), ); self.url( source, &dist.url, None, &cache_shard, dist.subdirectory.as_deref(), dist.ext, tags, hashes, client, ) .boxed_local() .await? } BuildableSource::Dist(SourceDist::Git(dist)) => { self.git(source, &GitSourceUrl::from(dist), tags, hashes, client) .boxed_local() .await? } BuildableSource::Dist(SourceDist::Directory(dist)) => { self.source_tree(source, &DirectorySourceUrl::from(dist), tags, hashes) .boxed_local() .await? } BuildableSource::Dist(SourceDist::Path(dist)) => { let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, WheelCache::Path(&dist.url).root(), ); self.archive( source, &PathSourceUrl::from(dist), &cache_shard, tags, hashes, ) .boxed_local() .await? } BuildableSource::Url(SourceUrl::Direct(resource)) => { // For direct URLs, cache directly under the hash of the URL itself. let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, WheelCache::Url(resource.url).root(), ); self.url( source, resource.url, None, &cache_shard, resource.subdirectory, resource.ext, tags, hashes, client, ) .boxed_local() .await? } BuildableSource::Url(SourceUrl::Git(resource)) => { self.git(source, resource, tags, hashes, client) .boxed_local() .await? } BuildableSource::Url(SourceUrl::Directory(resource)) => { self.source_tree(source, resource, tags, hashes) .boxed_local() .await? } BuildableSource::Url(SourceUrl::Path(resource)) => { let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, WheelCache::Path(resource.url).root(), ); self.archive(source, resource, &cache_shard, tags, hashes) .boxed_local() .await? } }; Ok(built_wheel_metadata) } /// Download a [`SourceDist`] and determine its metadata. This typically involves building the /// source distribution into a wheel; however, some build backends support determining the /// metadata without building the source distribution. pub(crate) async fn download_and_build_metadata( &self, source: &BuildableSource<'_>, hashes: HashPolicy<'_>, client: &ManagedClient<'_>, ) -> Result { let metadata = match &source { BuildableSource::Dist(SourceDist::Registry(dist)) => { // For registry source distributions, shard by package, then version. let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, WheelCache::Index(&dist.index) .wheel_dir(dist.name.as_ref()) .join(dist.version.to_string()), ); let url = dist.file.url.to_url()?; // If the URL is a file URL, use the local path directly. if url.scheme() == "file" { let path = url .to_file_path() .map_err(|()| Error::NonFileUrl(url.clone()))?; return self .archive_metadata( source, &PathSourceUrl { url: &url, path: Cow::Owned(path), ext: dist.ext, }, &cache_shard, hashes, ) .boxed_local() .await; } self.url_metadata( source, &url, Some(&dist.index), &cache_shard, None, dist.ext, hashes, client, ) .boxed_local() .await? } BuildableSource::Dist(SourceDist::DirectUrl(dist)) => { // For direct URLs, cache directly under the hash of the URL itself. let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, WheelCache::Url(&dist.url).root(), ); self.url_metadata( source, &dist.url, None, &cache_shard, dist.subdirectory.as_deref(), dist.ext, hashes, client, ) .boxed_local() .await? } BuildableSource::Dist(SourceDist::Git(dist)) => { self.git_metadata( source, &GitSourceUrl::from(dist), hashes, client, client.unmanaged.credentials_cache(), ) .boxed_local() .await? } BuildableSource::Dist(SourceDist::Directory(dist)) => { self.source_tree_metadata( source, &DirectorySourceUrl::from(dist), hashes, client.unmanaged.credentials_cache(), ) .boxed_local() .await? } BuildableSource::Dist(SourceDist::Path(dist)) => { let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, WheelCache::Path(&dist.url).root(), ); self.archive_metadata(source, &PathSourceUrl::from(dist), &cache_shard, hashes) .boxed_local() .await? } BuildableSource::Url(SourceUrl::Direct(resource)) => { // For direct URLs, cache directly under the hash of the URL itself. let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, WheelCache::Url(resource.url).root(), ); self.url_metadata( source, resource.url, None, &cache_shard, resource.subdirectory, resource.ext, hashes, client, ) .boxed_local() .await? } BuildableSource::Url(SourceUrl::Git(resource)) => { self.git_metadata( source, resource, hashes, client, client.unmanaged.credentials_cache(), ) .boxed_local() .await? } BuildableSource::Url(SourceUrl::Directory(resource)) => { self.source_tree_metadata( source, resource, hashes, client.unmanaged.credentials_cache(), ) .boxed_local() .await? } BuildableSource::Url(SourceUrl::Path(resource)) => { let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, WheelCache::Path(resource.url).root(), ); self.archive_metadata(source, resource, &cache_shard, hashes) .boxed_local() .await? } }; Ok(metadata) } /// Determine the [`ConfigSettings`] for the given package name. fn config_settings_for(&self, name: Option<&PackageName>) -> Cow<'_, ConfigSettings> { if let Some(name) = name { if let Some(package_settings) = self.build_context.config_settings_package().get(name) { Cow::Owned( package_settings .clone() .merge(self.build_context.config_settings().clone()), ) } else { Cow::Borrowed(self.build_context.config_settings()) } } else { Cow::Borrowed(self.build_context.config_settings()) } } /// Determine the extra build dependencies for the given package name. fn extra_build_dependencies_for(&self, name: Option<&PackageName>) -> &[ExtraBuildRequirement] { name.and_then(|name| { self.build_context .extra_build_requires() .get(name) .map(Vec::as_slice) }) .unwrap_or(&[]) } /// Determine the extra build variables for the given package name. fn extra_build_variables_for(&self, name: Option<&PackageName>) -> Option<&BuildVariables> { name.and_then(|name| self.build_context.extra_build_variables().get(name)) } /// Build a source distribution from a remote URL. async fn url<'data>( &self, source: &BuildableSource<'data>, url: &'data DisplaySafeUrl, index: Option<&'data IndexUrl>, cache_shard: &CacheShard, subdirectory: Option<&'data Path>, ext: SourceDistExtension, tags: &Tags, hashes: HashPolicy<'_>, client: &ManagedClient<'_>, ) -> Result { let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?; // Fetch the revision for the source distribution. let revision = self .url_revision(source, ext, url, index, cache_shard, hashes, client) .await?; // Before running the build, check that the hashes match. if !revision.satisfies(hashes) { return Err(Error::hash_mismatch( source.to_string(), hashes.digests(), revision.hashes(), )); } // Scope all operations to the revision. Within the revision, there's no need to check for // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); let source_dist_entry = cache_shard.entry(SOURCE); // We don't track any cache information for URL-based source distributions; they're assumed // to be immutable. let cache_info = CacheInfo::default(); // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); let extra_build_deps = self.extra_build_dependencies_for(source.name()); let extra_build_variables = self.extra_build_variables_for(source.name()); let build_info = BuildInfo::from_settings(&config_settings, extra_build_deps, extra_build_variables); let cache_shard = build_info .cache_shard() .map(|digest| cache_shard.shard(digest)) .unwrap_or(cache_shard); // If the cache contains a compatible wheel, return it. if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard) .ok() .flatten() .filter(|file| file.matches(source.name(), source.version())) { return Ok(BuiltWheelMetadata::from_file( file, revision.into_hashes(), cache_info, build_info, )); } // Otherwise, we need to build a wheel. Before building, ensure that the source is present. let revision = if source_dist_entry.path().is_dir() { revision } else { self.heal_url_revision( source, ext, url, index, &source_dist_entry, revision, hashes, client, ) .await? }; // Validate that the subdirectory exists. if let Some(subdirectory) = subdirectory { if !source_dist_entry.path().join(subdirectory).is_dir() { return Err(Error::MissingSubdirectory( url.clone(), subdirectory.to_path_buf(), )); } } let task = self .reporter .as_ref() .map(|reporter| reporter.on_build_start(source)); // Build the source distribution. let (disk_filename, wheel_filename, metadata) = self .build_distribution( source, source_dist_entry.path(), subdirectory, &cache_shard, SourceStrategy::Disabled, ) .await?; if let Some(task) = task { if let Some(reporter) = self.reporter.as_ref() { reporter.on_build_complete(source, task); } } // Store the metadata. let metadata_entry = cache_shard.entry(METADATA); write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) .await .map_err(Error::CacheWrite)?; Ok(BuiltWheelMetadata { path: cache_shard.join(&disk_filename).into_boxed_path(), target: cache_shard.join(wheel_filename.stem()).into_boxed_path(), filename: wheel_filename, hashes: revision.into_hashes(), cache_info, build_info, }) } /// Build the source distribution's metadata from a local path. /// /// If the build backend supports `prepare_metadata_for_build_wheel`, this method will avoid /// building the wheel. async fn url_metadata<'data>( &self, source: &BuildableSource<'data>, url: &'data DisplaySafeUrl, index: Option<&'data IndexUrl>, cache_shard: &CacheShard, subdirectory: Option<&'data Path>, ext: SourceDistExtension, hashes: HashPolicy<'_>, client: &ManagedClient<'_>, ) -> Result { let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?; // Fetch the revision for the source distribution. let revision = self .url_revision(source, ext, url, index, cache_shard, hashes, client) .await?; // Before running the build, check that the hashes match. if !revision.satisfies(hashes) { return Err(Error::hash_mismatch( source.to_string(), hashes.digests(), revision.hashes(), )); } // Scope all operations to the revision. Within the revision, there's no need to check for // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); let source_dist_entry = cache_shard.entry(SOURCE); // If the metadata is static, return it. let dynamic = match StaticMetadata::read(source, source_dist_entry.path(), subdirectory).await? { StaticMetadata::Some(metadata) => { return Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata), hashes: revision.into_hashes(), }); } StaticMetadata::Dynamic => true, StaticMetadata::None => false, }; // If the cache contains compatible metadata, return it. let metadata_entry = cache_shard.entry(METADATA); match CachedMetadata::read(&metadata_entry).await { Ok(Some(metadata)) => { if metadata.matches(source.name(), source.version()) { debug!("Using cached metadata for: {source}"); return Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata.into()), hashes: revision.into_hashes(), }); } debug!("Cached metadata does not match expected name and version for: {source}"); } Ok(None) => {} Err(err) => { debug!("Failed to deserialize cached metadata for: {source} ({err})"); } } // Otherwise, we need a wheel. let revision = if source_dist_entry.path().is_dir() { revision } else { self.heal_url_revision( source, ext, url, index, &source_dist_entry, revision, hashes, client, ) .await? }; // Validate that the subdirectory exists. if let Some(subdirectory) = subdirectory { if !source_dist_entry.path().join(subdirectory).is_dir() { return Err(Error::MissingSubdirectory( url.clone(), subdirectory.to_path_buf(), )); } } // Otherwise, we either need to build the metadata. // If the backend supports `prepare_metadata_for_build_wheel`, use it. if let Some(metadata) = self .build_metadata( source, source_dist_entry.path(), subdirectory, SourceStrategy::Disabled, ) .boxed_local() .await? { // If necessary, mark the metadata as dynamic. let metadata = if dynamic { ResolutionMetadata { dynamic: true, ..metadata } } else { metadata }; // Store the metadata. fs::create_dir_all(metadata_entry.dir()) .await .map_err(Error::CacheWrite)?; write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) .await .map_err(Error::CacheWrite)?; return Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata), hashes: revision.into_hashes(), }); } // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); let extra_build_deps = self.extra_build_dependencies_for(source.name()); let extra_build_variables = self.extra_build_variables_for(source.name()); let build_info = BuildInfo::from_settings(&config_settings, extra_build_deps, extra_build_variables); let cache_shard = build_info .cache_shard() .map(|digest| cache_shard.shard(digest)) .unwrap_or(cache_shard); let task = self .reporter .as_ref() .map(|reporter| reporter.on_build_start(source)); // Build the source distribution. let (_disk_filename, _wheel_filename, metadata) = self .build_distribution( source, source_dist_entry.path(), subdirectory, &cache_shard, SourceStrategy::Disabled, ) .await?; if let Some(task) = task { if let Some(reporter) = self.reporter.as_ref() { reporter.on_build_complete(source, task); } } // If necessary, mark the metadata as dynamic. let metadata = if dynamic { ResolutionMetadata { dynamic: true, ..metadata } } else { metadata }; // Store the metadata. write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) .await .map_err(Error::CacheWrite)?; Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata), hashes: revision.into_hashes(), }) } /// Return the [`Revision`] for a remote URL, refreshing it if necessary. async fn url_revision( &self, source: &BuildableSource<'_>, ext: SourceDistExtension, url: &DisplaySafeUrl, index: Option<&IndexUrl>, cache_shard: &CacheShard, hashes: HashPolicy<'_>, client: &ManagedClient<'_>, ) -> Result { let cache_entry = cache_shard.entry(HTTP_REVISION); // Determine the cache control policy for the request. let cache_control = match client.unmanaged.connectivity() { Connectivity::Online => { if let Some(header) = index.and_then(|index| { self.build_context .locations() .artifact_cache_control_for(index) }) { CacheControl::Override(header) } else { CacheControl::from( self.build_context .cache() .freshness(&cache_entry, source.name(), source.source_tree()) .map_err(Error::CacheRead)?, ) } } Connectivity::Offline => CacheControl::AllowStale, }; let download = |response| { async { // At this point, we're seeing a new or updated source distribution. Initialize a // new revision, to collect the source and built artifacts. let revision = Revision::new(); // Download the source distribution. debug!("Downloading source distribution: {source}"); let entry = cache_shard.shard(revision.id()).entry(SOURCE); let algorithms = hashes.algorithms(); let hashes = self .download_archive(response, source, ext, entry.path(), &algorithms) .await?; Ok(revision.with_hashes(HashDigests::from(hashes))) } .boxed_local() .instrument(info_span!("download", source_dist = %source)) }; let req = Self::request(url.clone(), client.unmanaged)?; let revision = client .managed(|client| { client.cached_client().get_serde_with_retry( req, &cache_entry, cache_control, download, ) }) .await .map_err(|err| match err { CachedClientError::Callback { err, .. } => err, CachedClientError::Client { err, .. } => Error::Client(err), })?; // If the archive is missing the required hashes, force a refresh. if revision.has_digests(hashes) { Ok(revision) } else { client .managed(async |client| { client .cached_client() .skip_cache_with_retry( Self::request(url.clone(), client)?, &cache_entry, cache_control, download, ) .await .map_err(|err| match err { CachedClientError::Callback { err, .. } => err, CachedClientError::Client { err, .. } => Error::Client(err), }) }) .await } } /// Build a source distribution from a local archive (e.g., `.tar.gz` or `.zip`). async fn archive( &self, source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, cache_shard: &CacheShard, tags: &Tags, hashes: HashPolicy<'_>, ) -> Result { let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?; // Fetch the revision for the source distribution. let LocalRevisionPointer { cache_info, revision, } = self .archive_revision(source, resource, cache_shard, hashes) .await?; // Before running the build, check that the hashes match. if !revision.satisfies(hashes) { return Err(Error::hash_mismatch( source.to_string(), hashes.digests(), revision.hashes(), )); } // Scope all operations to the revision. Within the revision, there's no need to check for // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); let source_entry = cache_shard.entry(SOURCE); // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); let extra_build_deps = self.extra_build_dependencies_for(source.name()); let extra_build_variables = self.extra_build_variables_for(source.name()); let build_info = BuildInfo::from_settings(&config_settings, extra_build_deps, extra_build_variables); let cache_shard = build_info .cache_shard() .map(|digest| cache_shard.shard(digest)) .unwrap_or(cache_shard); // If the cache contains a compatible wheel, return it. if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard) .ok() .flatten() .filter(|file| file.matches(source.name(), source.version())) { return Ok(BuiltWheelMetadata::from_file( file, revision.into_hashes(), cache_info, build_info, )); } // Otherwise, we need to build a wheel, which requires a source distribution. let revision = if source_entry.path().is_dir() { revision } else { self.heal_archive_revision(source, resource, &source_entry, revision, hashes) .await? }; let task = self .reporter .as_ref() .map(|reporter| reporter.on_build_start(source)); let (disk_filename, filename, metadata) = self .build_distribution( source, source_entry.path(), None, &cache_shard, SourceStrategy::Disabled, ) .await?; if let Some(task) = task { if let Some(reporter) = self.reporter.as_ref() { reporter.on_build_complete(source, task); } } // Store the metadata. let metadata_entry = cache_shard.entry(METADATA); write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) .await .map_err(Error::CacheWrite)?; Ok(BuiltWheelMetadata { path: cache_shard.join(&disk_filename).into_boxed_path(), target: cache_shard.join(filename.stem()).into_boxed_path(), filename, hashes: revision.into_hashes(), cache_info, build_info, }) } /// Build the source distribution's metadata from a local archive (e.g., `.tar.gz` or `.zip`). /// /// If the build backend supports `prepare_metadata_for_build_wheel`, this method will avoid /// building the wheel. async fn archive_metadata( &self, source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, cache_shard: &CacheShard, hashes: HashPolicy<'_>, ) -> Result { let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?; // Fetch the revision for the source distribution. let LocalRevisionPointer { revision, .. } = self .archive_revision(source, resource, cache_shard, hashes) .await?; // Before running the build, check that the hashes match. if !revision.satisfies(hashes) { return Err(Error::hash_mismatch( source.to_string(), hashes.digests(), revision.hashes(), )); } // Scope all operations to the revision. Within the revision, there's no need to check for // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); let source_entry = cache_shard.entry(SOURCE); // If the metadata is static, return it. let dynamic = match StaticMetadata::read(source, source_entry.path(), None).await? { StaticMetadata::Some(metadata) => { return Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata), hashes: revision.into_hashes(), }); } StaticMetadata::Dynamic => true, StaticMetadata::None => false, }; // If the cache contains compatible metadata, return it. let metadata_entry = cache_shard.entry(METADATA); match CachedMetadata::read(&metadata_entry).await { Ok(Some(metadata)) => { if metadata.matches(source.name(), source.version()) { debug!("Using cached metadata for: {source}"); return Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata.into()), hashes: revision.into_hashes(), }); } debug!("Cached metadata does not match expected name and version for: {source}"); } Ok(None) => {} Err(err) => { debug!("Failed to deserialize cached metadata for: {source} ({err})"); } } // Otherwise, we need a source distribution. let revision = if source_entry.path().is_dir() { revision } else { self.heal_archive_revision(source, resource, &source_entry, revision, hashes) .await? }; // If the backend supports `prepare_metadata_for_build_wheel`, use it. if let Some(metadata) = self .build_metadata(source, source_entry.path(), None, SourceStrategy::Disabled) .boxed_local() .await? { // If necessary, mark the metadata as dynamic. let metadata = if dynamic { ResolutionMetadata { dynamic: true, ..metadata } } else { metadata }; // Store the metadata. fs::create_dir_all(metadata_entry.dir()) .await .map_err(Error::CacheWrite)?; write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) .await .map_err(Error::CacheWrite)?; return Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata), hashes: revision.into_hashes(), }); } // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); let extra_build_deps = self.extra_build_dependencies_for(source.name()); let extra_build_variables = self.extra_build_variables_for(source.name()); let build_info = BuildInfo::from_settings(&config_settings, extra_build_deps, extra_build_variables); let cache_shard = build_info .cache_shard() .map(|digest| cache_shard.shard(digest)) .unwrap_or(cache_shard); // Otherwise, we need to build a wheel. let task = self .reporter .as_ref() .map(|reporter| reporter.on_build_start(source)); let (_disk_filename, _filename, metadata) = self .build_distribution( source, source_entry.path(), None, &cache_shard, SourceStrategy::Disabled, ) .await?; if let Some(task) = task { if let Some(reporter) = self.reporter.as_ref() { reporter.on_build_complete(source, task); } } // If necessary, mark the metadata as dynamic. let metadata = if dynamic { ResolutionMetadata { dynamic: true, ..metadata } } else { metadata }; // Store the metadata. write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) .await .map_err(Error::CacheWrite)?; Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata), hashes: revision.into_hashes(), }) } /// Return the [`Revision`] for a local archive, refreshing it if necessary. async fn archive_revision( &self, source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, cache_shard: &CacheShard, hashes: HashPolicy<'_>, ) -> Result { // Verify that the archive exists. if !resource.path.is_file() { return Err(Error::NotFound(resource.url.clone())); } // Determine the last-modified time of the source distribution. let cache_info = CacheInfo::from_file(&resource.path).map_err(Error::CacheRead)?; // Read the existing metadata from the cache. let revision_entry = cache_shard.entry(LOCAL_REVISION); // If the revision already exists, return it. There's no need to check for freshness, since // we use an exact timestamp. if let Some(pointer) = LocalRevisionPointer::read_from(&revision_entry)? { if *pointer.cache_info() == cache_info { if pointer.revision().has_digests(hashes) { return Ok(pointer); } } } // Otherwise, we need to create a new revision. let revision = Revision::new(); // Unzip the archive to a temporary directory. debug!("Unpacking source distribution: {source}"); let entry = cache_shard.shard(revision.id()).entry(SOURCE); let algorithms = hashes.algorithms(); let hashes = self .persist_archive(&resource.path, resource.ext, entry.path(), &algorithms) .await?; // Include the hashes and cache info in the revision. let revision = revision.with_hashes(HashDigests::from(hashes)); // Persist the revision. let pointer = LocalRevisionPointer { cache_info, revision, }; pointer.write_to(&revision_entry).await?; Ok(pointer) } /// Build a source distribution from a local source tree (i.e., directory), either editable or /// non-editable. async fn source_tree( &self, source: &BuildableSource<'_>, resource: &DirectorySourceUrl<'_>, tags: &Tags, hashes: HashPolicy<'_>, ) -> Result { // Before running the build, check that the hashes match. if hashes.is_validate() { return Err(Error::HashesNotSupportedSourceTree(source.to_string())); } let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, if resource.editable.unwrap_or(false) { WheelCache::Editable(resource.url).root() } else { WheelCache::Path(resource.url).root() }, ); // Acquire the advisory lock. let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?; // Fetch the revision for the source distribution. let LocalRevisionPointer { cache_info, revision, } = self .source_tree_revision(source, resource, &cache_shard) .await?; // Scope all operations to the revision. Within the revision, there's no need to check for // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); let extra_build_deps = self.extra_build_dependencies_for(source.name()); let extra_build_variables = self.extra_build_variables_for(source.name()); let build_info = BuildInfo::from_settings(&config_settings, extra_build_deps, extra_build_variables); let cache_shard = build_info .cache_shard() .map(|digest| cache_shard.shard(digest)) .unwrap_or(cache_shard); // If the cache contains a compatible wheel, return it. if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard) .ok() .flatten() .filter(|file| file.matches(source.name(), source.version())) { return Ok(BuiltWheelMetadata::from_file( file, revision.into_hashes(), cache_info, build_info, )); } // Otherwise, we need to build a wheel. let task = self .reporter .as_ref() .map(|reporter| reporter.on_build_start(source)); let (disk_filename, filename, metadata) = self .build_distribution( source, &resource.install_path, None, &cache_shard, self.build_context.sources(), ) .await?; if let Some(task) = task { if let Some(reporter) = self.reporter.as_ref() { reporter.on_build_complete(source, task); } } // Store the metadata. let metadata_entry = cache_shard.entry(METADATA); write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) .await .map_err(Error::CacheWrite)?; Ok(BuiltWheelMetadata { path: cache_shard.join(&disk_filename).into_boxed_path(), target: cache_shard.join(filename.stem()).into_boxed_path(), filename, hashes: revision.into_hashes(), cache_info, build_info, }) } /// Build the source distribution's metadata from a local source tree (i.e., a directory), /// either editable or non-editable. /// /// If the build backend supports `prepare_metadata_for_build_wheel`, this method will avoid /// building the wheel. async fn source_tree_metadata( &self, source: &BuildableSource<'_>, resource: &DirectorySourceUrl<'_>, hashes: HashPolicy<'_>, credentials_cache: &CredentialsCache, ) -> Result { // Before running the build, check that the hashes match. if hashes.is_validate() { return Err(Error::HashesNotSupportedSourceTree(source.to_string())); } // If the metadata is static, return it. let dynamic = match StaticMetadata::read(source, &resource.install_path, None).await? { StaticMetadata::Some(metadata) => { return Ok(ArchiveMetadata::from( Metadata::from_workspace( metadata, resource.install_path.as_ref(), None, self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), credentials_cache, ) .await?, )); } StaticMetadata::Dynamic => true, StaticMetadata::None => false, }; let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, if resource.editable.unwrap_or(false) { WheelCache::Editable(resource.url).root() } else { WheelCache::Path(resource.url).root() }, ); // Acquire the advisory lock. let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?; // Fetch the revision for the source distribution. let LocalRevisionPointer { revision, .. } = self .source_tree_revision(source, resource, &cache_shard) .await?; // Scope all operations to the revision. Within the revision, there's no need to check for // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); // If the cache contains compatible metadata, return it. let metadata_entry = cache_shard.entry(METADATA); match CachedMetadata::read(&metadata_entry).await { Ok(Some(metadata)) => { if metadata.matches(source.name(), source.version()) { debug!("Using cached metadata for: {source}"); // If necessary, mark the metadata as dynamic. let metadata = if dynamic { ResolutionMetadata { dynamic: true, ..metadata.into() } } else { metadata.into() }; return Ok(ArchiveMetadata::from( Metadata::from_workspace( metadata, resource.install_path.as_ref(), None, self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), credentials_cache, ) .await?, )); } debug!("Cached metadata does not match expected name and version for: {source}"); } Ok(None) => {} Err(err) => { debug!("Failed to deserialize cached metadata for: {source} ({err})"); } } // If the backend supports `prepare_metadata_for_build_wheel`, use it. if let Some(metadata) = self .build_metadata( source, &resource.install_path, None, self.build_context.sources(), ) .boxed_local() .await? { // Store the metadata. fs::create_dir_all(metadata_entry.dir()) .await .map_err(Error::CacheWrite)?; write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) .await .map_err(Error::CacheWrite)?; // If necessary, mark the metadata as dynamic. let metadata = if dynamic { ResolutionMetadata { dynamic: true, ..metadata } } else { metadata }; return Ok(ArchiveMetadata::from( Metadata::from_workspace( metadata, resource.install_path.as_ref(), None, self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), credentials_cache, ) .await?, )); } // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); let extra_build_deps = self.extra_build_dependencies_for(source.name()); let extra_build_variables = self.extra_build_variables_for(source.name()); let build_info = BuildInfo::from_settings(&config_settings, extra_build_deps, extra_build_variables); let cache_shard = build_info .cache_shard() .map(|digest| cache_shard.shard(digest)) .unwrap_or(cache_shard); // Otherwise, we need to build a wheel. let task = self .reporter .as_ref() .map(|reporter| reporter.on_build_start(source)); let (_disk_filename, _filename, metadata) = self .build_distribution( source, &resource.install_path, None, &cache_shard, self.build_context.sources(), ) .await?; if let Some(task) = task { if let Some(reporter) = self.reporter.as_ref() { reporter.on_build_complete(source, task); } } // Store the metadata. write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) .await .map_err(Error::CacheWrite)?; // If necessary, mark the metadata as dynamic. let metadata = if dynamic { ResolutionMetadata { dynamic: true, ..metadata } } else { metadata }; Ok(ArchiveMetadata::from( Metadata::from_workspace( metadata, resource.install_path.as_ref(), None, self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), credentials_cache, ) .await?, )) } /// Return the [`Revision`] for a local source tree, refreshing it if necessary. async fn source_tree_revision( &self, source: &BuildableSource<'_>, resource: &DirectorySourceUrl<'_>, cache_shard: &CacheShard, ) -> Result { // Verify that the source tree exists. if !resource.install_path.is_dir() { return Err(Error::NotFound(resource.url.clone())); } // Determine the last-modified time of the source distribution. let cache_info = CacheInfo::from_directory(&resource.install_path)?; // Read the existing metadata from the cache. let entry = cache_shard.entry(LOCAL_REVISION); // If the revision is fresh, return it. if self .build_context .cache() .freshness(&entry, source.name(), source.source_tree()) .map_err(Error::CacheRead)? .is_fresh() { match LocalRevisionPointer::read_from(&entry) { Ok(Some(pointer)) => { if *pointer.cache_info() == cache_info { return Ok(pointer); } debug!("Cached revision does not match expected cache info for: {source}"); } Ok(None) => {} Err(err) => { debug!("Failed to deserialize cached revision for: {source} ({err})"); } } } // Otherwise, we need to create a new revision. let revision = Revision::new(); let pointer = LocalRevisionPointer { cache_info, revision, }; pointer.write_to(&entry).await?; Ok(pointer) } /// Return the [`RequiresDist`] from a `pyproject.toml`, if it can be statically extracted. pub(crate) async fn source_tree_requires_dist( &self, path: &Path, pyproject_toml: &PyProjectToml, credentials_cache: &CredentialsCache, ) -> Result, Error> { // Attempt to read static metadata from the `pyproject.toml`. match uv_pypi_types::RequiresDist::from_pyproject_toml(pyproject_toml.clone()) { Ok(requires_dist) => { debug!("Found static `requires-dist` for: {}", path.display()); let requires_dist = RequiresDist::from_project_maybe_workspace( requires_dist, path, None, self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), credentials_cache, ) .await?; Ok(Some(requires_dist)) } Err( err @ (uv_pypi_types::MetadataError::Pep508Error(_) | uv_pypi_types::MetadataError::DynamicField(_) | uv_pypi_types::MetadataError::FieldNotFound(_) | uv_pypi_types::MetadataError::PoetrySyntax), ) => { debug!( "No static `requires-dist` available for: {} ({err:?})", path.display() ); Ok(None) } Err(err) => Err(Error::PyprojectToml(err)), } } /// Build a source distribution from a Git repository. async fn git( &self, source: &BuildableSource<'_>, resource: &GitSourceUrl<'_>, tags: &Tags, hashes: HashPolicy<'_>, client: &ManagedClient<'_>, ) -> Result { // Before running the build, check that the hashes match. if hashes.is_validate() { return Err(Error::HashesNotSupportedGit(source.to_string())); } // Fetch the Git repository. let fetch = self .build_context .git() .fetch( resource.git, client.unmanaged.disable_ssl(resource.git.repository()), client.unmanaged.connectivity() == Connectivity::Offline, self.build_context.cache().bucket(CacheBucket::Git), self.reporter .clone() .map(|reporter| reporter.into_git_reporter()), ) .await?; // Validate that the subdirectory exists. if let Some(subdirectory) = resource.subdirectory { if !fetch.path().join(subdirectory).is_dir() { return Err(Error::MissingSubdirectory( resource.url.to_url(), subdirectory.to_path_buf(), )); } } // Validate that LFS artifacts were fully initialized if resource.git.lfs().enabled() && !fetch.lfs_ready() { if GIT_LFS.is_err() { return Err(Error::MissingGitLfsArtifacts( resource.url.to_url(), GitError::GitLfsNotFound, )); } return Err(Error::MissingGitLfsArtifacts( resource.url.to_url(), GitError::GitLfsNotConfigured, )); } let git_sha = fetch.git().precise().expect("Exact commit after checkout"); let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, WheelCache::Git(resource.url, git_sha.as_short_str()).root(), ); let metadata_entry = cache_shard.entry(METADATA); // Acquire the advisory lock. let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?; // We don't track any cache information for Git-based source distributions; they're assumed // to be immutable. let cache_info = CacheInfo::default(); // We don't compute hashes for Git-based source distributions, since the Git commit SHA is // used as the identifier. let hashes = HashDigests::empty(); // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); let extra_build_deps = self.extra_build_dependencies_for(source.name()); let extra_build_variables = self.extra_build_variables_for(source.name()); let build_info = BuildInfo::from_settings(&config_settings, extra_build_deps, extra_build_variables); let cache_shard = build_info .cache_shard() .map(|digest| cache_shard.shard(digest)) .unwrap_or(cache_shard); // If the cache contains a compatible wheel, return it. if let Some(file) = BuiltWheelFile::find_in_cache(tags, &cache_shard) .ok() .flatten() .filter(|file| file.matches(source.name(), source.version())) { return Ok(BuiltWheelMetadata::from_file( file, hashes, cache_info, build_info, )); } let task = self .reporter .as_ref() .map(|reporter| reporter.on_build_start(source)); let (disk_filename, filename, metadata) = self .build_distribution( source, fetch.path(), resource.subdirectory, &cache_shard, self.build_context.sources(), ) .await?; if let Some(task) = task { if let Some(reporter) = self.reporter.as_ref() { reporter.on_build_complete(source, task); } } // Store the metadata. write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) .await .map_err(Error::CacheWrite)?; Ok(BuiltWheelMetadata { path: cache_shard.join(&disk_filename).into_boxed_path(), target: cache_shard.join(filename.stem()).into_boxed_path(), filename, hashes, cache_info, build_info, }) } /// Build the source distribution's metadata from a Git repository. /// /// If the build backend supports `prepare_metadata_for_build_wheel`, this method will avoid /// building the wheel. async fn git_metadata( &self, source: &BuildableSource<'_>, resource: &GitSourceUrl<'_>, hashes: HashPolicy<'_>, client: &ManagedClient<'_>, credentials_cache: &CredentialsCache, ) -> Result { // Before running the build, check that the hashes match. if hashes.is_validate() { return Err(Error::HashesNotSupportedGit(source.to_string())); } // If the reference appears to be a commit, and we've already checked it out, avoid taking // the GitHub fast path. let cache_shard = resource .git .reference() .as_str() .and_then(|reference| GitOid::from_str(reference).ok()) .map(|oid| { self.build_context.cache().shard( CacheBucket::SourceDistributions, WheelCache::Git(resource.url, oid.as_short_str()).root(), ) }); if cache_shard .as_ref() .is_some_and(|cache_shard| cache_shard.is_dir()) { debug!("Skipping GitHub fast path for: {source} (shard exists)"); } else { debug!("Attempting GitHub fast path for: {source}"); // If this is GitHub URL, attempt to resolve to a precise commit using the GitHub API. match self .build_context .git() .github_fast_path( resource.git, client .unmanaged .uncached_client(resource.git.repository()) .raw_client(), ) .await { Ok(Some(precise)) => { // There's no need to check the cache, since we can't use cached metadata if there are // sources, and we can't know if there are sources without fetching the // `pyproject.toml`. // // For the same reason, there's no need to write to the cache, since we won't be able to // use it on subsequent runs. match self .github_metadata(precise, source, resource, client) .await { Ok(Some(metadata)) => { // Validate the metadata, but ignore it if the metadata doesn't match. match validate_metadata(source, &metadata) { Ok(()) => { debug!( "Found static metadata via GitHub fast path for: {source}" ); return Ok(ArchiveMetadata { metadata: Metadata::from_metadata23(metadata), hashes: HashDigests::empty(), }); } Err(err) => { debug!( "Ignoring `pyproject.toml` from GitHub for {source}: {err}" ); } } } Ok(None) => { // Nothing to do. } Err(err) => { debug!( "Failed to fetch `pyproject.toml` via GitHub fast path for: {source} ({err})" ); } } } Ok(None) => { // Nothing to do. } Err(err) => { debug!("Failed to resolve commit via GitHub fast path for: {source} ({err})"); } } } // Fetch the Git repository. let fetch = self .build_context .git() .fetch( resource.git, client.unmanaged.disable_ssl(resource.git.repository()), client.unmanaged.connectivity() == Connectivity::Offline, self.build_context.cache().bucket(CacheBucket::Git), self.reporter .clone() .map(|reporter| reporter.into_git_reporter()), ) .await?; // Validate that the subdirectory exists. if let Some(subdirectory) = resource.subdirectory { if !fetch.path().join(subdirectory).is_dir() { return Err(Error::MissingSubdirectory( resource.url.to_url(), subdirectory.to_path_buf(), )); } } // Validate that LFS artifacts were fully initialized if resource.git.lfs().enabled() && !fetch.lfs_ready() { if GIT_LFS.is_err() { return Err(Error::MissingGitLfsArtifacts( resource.url.to_url(), GitError::GitLfsNotFound, )); } return Err(Error::MissingGitLfsArtifacts( resource.url.to_url(), GitError::GitLfsNotConfigured, )); } let git_sha = fetch.git().precise().expect("Exact commit after checkout"); let cache_shard = self.build_context.cache().shard( CacheBucket::SourceDistributions, WheelCache::Git(resource.url, git_sha.as_short_str()).root(), ); let metadata_entry = cache_shard.entry(METADATA); // Acquire the advisory lock. let _lock = cache_shard.lock().await.map_err(Error::CacheLock)?; let path = if let Some(subdirectory) = resource.subdirectory { Cow::Owned(fetch.path().join(subdirectory)) } else { Cow::Borrowed(fetch.path()) }; let git_member = GitWorkspaceMember { fetch_root: fetch.path(), git_source: resource, }; // If the metadata is static, return it. let dynamic = match StaticMetadata::read(source, fetch.path(), resource.subdirectory).await? { StaticMetadata::Some(metadata) => { return Ok(ArchiveMetadata::from( Metadata::from_workspace( metadata, &path, Some(&git_member), self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), credentials_cache, ) .await?, )); } StaticMetadata::Dynamic => true, StaticMetadata::None => false, }; // If the cache contains compatible metadata, return it. if self .build_context .cache() .freshness(&metadata_entry, source.name(), source.source_tree()) .map_err(Error::CacheRead)? .is_fresh() { match CachedMetadata::read(&metadata_entry).await { Ok(Some(metadata)) => { if metadata.matches(source.name(), source.version()) { debug!("Using cached metadata for: {source}"); let git_member = GitWorkspaceMember { fetch_root: fetch.path(), git_source: resource, }; return Ok(ArchiveMetadata::from( Metadata::from_workspace( metadata.into(), &path, Some(&git_member), self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), credentials_cache, ) .await?, )); } debug!( "Cached metadata does not match expected name and version for: {source}" ); } Ok(None) => {} Err(err) => { debug!("Failed to deserialize cached metadata for: {source} ({err})"); } } } // If the backend supports `prepare_metadata_for_build_wheel`, use it. if let Some(metadata) = self .build_metadata( source, fetch.path(), resource.subdirectory, self.build_context.sources(), ) .boxed_local() .await? { // If necessary, mark the metadata as dynamic. let metadata = if dynamic { ResolutionMetadata { dynamic: true, ..metadata } } else { metadata }; // Store the metadata. fs::create_dir_all(metadata_entry.dir()) .await .map_err(Error::CacheWrite)?; write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) .await .map_err(Error::CacheWrite)?; return Ok(ArchiveMetadata::from( Metadata::from_workspace( metadata, &path, Some(&git_member), self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), credentials_cache, ) .await?, )); } // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); let extra_build_deps = self.extra_build_dependencies_for(source.name()); let extra_build_variables = self.extra_build_variables_for(source.name()); let build_info = BuildInfo::from_settings(&config_settings, extra_build_deps, extra_build_variables); let cache_shard = build_info .cache_shard() .map(|digest| cache_shard.shard(digest)) .unwrap_or(cache_shard); // Otherwise, we need to build a wheel. let task = self .reporter .as_ref() .map(|reporter| reporter.on_build_start(source)); let (_disk_filename, _filename, metadata) = self .build_distribution( source, fetch.path(), resource.subdirectory, &cache_shard, self.build_context.sources(), ) .await?; if let Some(task) = task { if let Some(reporter) = self.reporter.as_ref() { reporter.on_build_complete(source, task); } } // If necessary, mark the metadata as dynamic. let metadata = if dynamic { ResolutionMetadata { dynamic: true, ..metadata } } else { metadata }; // Store the metadata. write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) .await .map_err(Error::CacheWrite)?; Ok(ArchiveMetadata::from( Metadata::from_workspace( metadata, fetch.path(), Some(&git_member), self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), credentials_cache, ) .await?, )) } /// Resolve a source to a specific revision. pub(crate) async fn resolve_revision( &self, source: &BuildableSource<'_>, client: &ManagedClient<'_>, ) -> Result, Error> { let git = match source { BuildableSource::Dist(SourceDist::Git(source)) => &*source.git, BuildableSource::Url(SourceUrl::Git(source)) => source.git, _ => { return Ok(None); } }; // If the URL is already precise, return it. if let Some(precise) = self.build_context.git().get_precise(git) { debug!("Precise commit already known: {source}"); return Ok(Some(precise)); } // If this is GitHub URL, attempt to resolve to a precise commit using the GitHub API. if let Some(precise) = self .build_context .git() .github_fast_path( git, client .unmanaged .uncached_client(git.repository()) .raw_client(), ) .await? { debug!("Resolved to precise commit via GitHub fast path: {source}"); return Ok(Some(precise)); } // Otherwise, fetch the Git repository. let fetch = self .build_context .git() .fetch( git, client.unmanaged.disable_ssl(git.repository()), client.unmanaged.connectivity() == Connectivity::Offline, self.build_context.cache().bucket(CacheBucket::Git), self.reporter .clone() .map(|reporter| reporter.into_git_reporter()), ) .await?; Ok(fetch.git().precise()) } /// Fetch static [`ResolutionMetadata`] from a GitHub repository, if possible. /// /// Attempts to fetch the `pyproject.toml` from the resolved commit using the GitHub API. async fn github_metadata( &self, commit: GitOid, source: &BuildableSource<'_>, resource: &GitSourceUrl<'_>, client: &ManagedClient<'_>, ) -> Result, Error> { let GitSourceUrl { git, subdirectory, .. } = resource; // The fast path isn't available for subdirectories. If a `pyproject.toml` is in a // subdirectory, it could be part of a workspace; and if it's part of a workspace, it could // have `tool.uv.sources` entries that it inherits from the workspace root. if subdirectory.is_some() { return Ok(None); } let Some(GitHubRepository { owner, repo }) = GitHubRepository::parse(git.repository()) else { return Ok(None); }; // Fetch the `pyproject.toml` from the resolved commit. let url = format!("https://raw.githubusercontent.com/{owner}/{repo}/{commit}/pyproject.toml"); debug!("Attempting to fetch `pyproject.toml` from: {url}"); let content = client .managed(async |client| { let response = client .uncached_client(git.repository()) .get(&url) .send() .await?; // If the `pyproject.toml` does not exist, the GitHub API will return a 404. if response.status() == StatusCode::NOT_FOUND { return Ok::, Error>(None); } response.error_for_status_ref()?; let content = response.text().await?; Ok::, Error>(Some(content)) }) .await?; let Some(content) = content else { debug!("GitHub API returned a 404 for: {url}"); return Ok(None); }; // Parse the `pyproject.toml`. let pyproject_toml = match PyProjectToml::from_toml(&content) { Ok(metadata) => metadata, Err( uv_pypi_types::MetadataError::InvalidPyprojectTomlSyntax(..) | uv_pypi_types::MetadataError::InvalidPyprojectTomlSchema(..), ) => { debug!("Failed to read `pyproject.toml` from GitHub API for: {url}"); return Ok(None); } Err(err) => return Err(err.into()), }; // Parse the metadata. let metadata = match ResolutionMetadata::parse_pyproject_toml(pyproject_toml, source.version()) { Ok(metadata) => metadata, Err( uv_pypi_types::MetadataError::Pep508Error(..) | uv_pypi_types::MetadataError::DynamicField(..) | uv_pypi_types::MetadataError::FieldNotFound(..) | uv_pypi_types::MetadataError::PoetrySyntax, ) => { debug!("Failed to extract static metadata from GitHub API for: {url}"); return Ok(None); } Err(err) => return Err(err.into()), }; // Determine whether the project has `tool.uv.sources`. If the project has sources, it must // be lowered, which requires access to the workspace. For example, it could have workspace // members that need to be translated to concrete paths on disk. // // TODO(charlie): We could still use the `pyproject.toml` if the sources are all `git` or // `url` sources; this is only applicable to `workspace` and `path` sources. It's awkward, // though, because we'd need to pass a path into the lowering routine, and that path would // be incorrect (we'd just be relying on it not being used). match has_sources(&content) { Ok(false) => {} Ok(true) => { debug!("Skipping GitHub fast path; `pyproject.toml` has sources: {url}"); return Ok(None); } Err(err) => { debug!("Failed to parse `tool.uv.sources` from GitHub API for: {url} ({err})"); return Ok(None); } } Ok(Some(metadata)) } /// Heal a [`Revision`] for a local archive. async fn heal_archive_revision( &self, source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, entry: &CacheEntry, revision: Revision, hashes: HashPolicy<'_>, ) -> Result { warn!("Re-extracting missing source distribution: {source}"); // Take the union of the requested and existing hash algorithms. let algorithms = { let mut algorithms = hashes.algorithms(); for digest in revision.hashes() { algorithms.push(digest.algorithm()); } algorithms.sort(); algorithms.dedup(); algorithms }; let hashes = self .persist_archive(&resource.path, resource.ext, entry.path(), &algorithms) .await?; for existing in revision.hashes() { if !hashes.contains(existing) { return Err(Error::CacheHeal(source.to_string(), existing.algorithm())); } } Ok(revision.with_hashes(HashDigests::from(hashes))) } /// Heal a [`Revision`] for a remote archive. async fn heal_url_revision( &self, source: &BuildableSource<'_>, ext: SourceDistExtension, url: &DisplaySafeUrl, index: Option<&IndexUrl>, entry: &CacheEntry, revision: Revision, hashes: HashPolicy<'_>, client: &ManagedClient<'_>, ) -> Result { warn!("Re-downloading missing source distribution: {source}"); let cache_entry = entry.shard().entry(HTTP_REVISION); // Determine the cache control policy for the request. let cache_control = match client.unmanaged.connectivity() { Connectivity::Online => { if let Some(header) = index.and_then(|index| { self.build_context .locations() .artifact_cache_control_for(index) }) { CacheControl::Override(header) } else { CacheControl::from( self.build_context .cache() .freshness(&cache_entry, source.name(), source.source_tree()) .map_err(Error::CacheRead)?, ) } } Connectivity::Offline => CacheControl::AllowStale, }; let download = |response| { async { // Take the union of the requested and existing hash algorithms. let algorithms = { let mut algorithms = hashes.algorithms(); for digest in revision.hashes() { algorithms.push(digest.algorithm()); } algorithms.sort(); algorithms.dedup(); algorithms }; let hashes = self .download_archive(response, source, ext, entry.path(), &algorithms) .await?; for existing in revision.hashes() { if !hashes.contains(existing) { return Err(Error::CacheHeal(source.to_string(), existing.algorithm())); } } Ok(revision.clone().with_hashes(HashDigests::from(hashes))) } .boxed_local() .instrument(info_span!("download", source_dist = %source)) }; client .managed(async |client| { client .cached_client() .skip_cache_with_retry( Self::request(url.clone(), client)?, &cache_entry, cache_control, download, ) .await .map_err(|err| match err { CachedClientError::Callback { err, .. } => err, CachedClientError::Client { err, .. } => Error::Client(err), }) }) .await } /// Download and unzip a source distribution into the cache from an HTTP response. async fn download_archive( &self, response: Response, source: &BuildableSource<'_>, ext: SourceDistExtension, target: &Path, algorithms: &[HashAlgorithm], ) -> Result, Error> { let temp_dir = tempfile::tempdir_in( self.build_context .cache() .bucket(CacheBucket::SourceDistributions), ) .map_err(Error::CacheWrite)?; let reader = response .bytes_stream() .map_err(std::io::Error::other) .into_async_read(); // Create a hasher for each hash algorithm. let mut hashers = algorithms .iter() .copied() .map(Hasher::from) .collect::>(); let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers); // Download and unzip the source distribution into a temporary directory. let span = info_span!("download_source_dist", source_dist = %source); uv_extract::stream::archive(&mut hasher, ext, temp_dir.path()) .await .map_err(|err| Error::Extract(source.to_string(), err))?; drop(span); // If necessary, exhaust the reader to compute the hash. if !algorithms.is_empty() { hasher.finish().await.map_err(Error::HashExhaustion)?; } let hashes = hashers.into_iter().map(HashDigest::from).collect(); // Extract the top-level directory. let extracted = match uv_extract::strip_component(temp_dir.path()) { Ok(top_level) => top_level, Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.keep(), Err(err) => { return Err(Error::Extract( temp_dir.path().to_string_lossy().into_owned(), err, )); } }; // Persist it to the cache. fs_err::tokio::create_dir_all(target.parent().expect("Cache entry to have parent")) .await .map_err(Error::CacheWrite)?; if let Err(err) = rename_with_retry(extracted, target).await { // If the directory already exists, accept it. if err.kind() == std::io::ErrorKind::AlreadyExists { warn!("Directory already exists: {}", target.display()); } else { return Err(Error::CacheWrite(err)); } } Ok(hashes) } /// Extract a local archive, and store it at the given [`CacheEntry`]. async fn persist_archive( &self, path: &Path, ext: SourceDistExtension, target: &Path, algorithms: &[HashAlgorithm], ) -> Result, Error> { debug!("Unpacking for build: {}", path.display()); let temp_dir = tempfile::tempdir_in( self.build_context .cache() .bucket(CacheBucket::SourceDistributions), ) .map_err(Error::CacheWrite)?; let reader = fs_err::tokio::File::open(&path) .await .map_err(Error::CacheRead)?; // Create a hasher for each hash algorithm. let mut hashers = algorithms .iter() .copied() .map(Hasher::from) .collect::>(); let mut hasher = uv_extract::hash::HashReader::new(reader, &mut hashers); // Unzip the archive into a temporary directory. uv_extract::stream::archive(&mut hasher, ext, &temp_dir.path()) .await .map_err(|err| Error::Extract(temp_dir.path().to_string_lossy().into_owned(), err))?; // If necessary, exhaust the reader to compute the hash. if !algorithms.is_empty() { hasher.finish().await.map_err(Error::HashExhaustion)?; } let hashes = hashers.into_iter().map(HashDigest::from).collect(); // Extract the top-level directory from the archive. let extracted = match uv_extract::strip_component(temp_dir.path()) { Ok(top_level) => top_level, Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.path().to_path_buf(), Err(err) => { return Err(Error::Extract( temp_dir.path().to_string_lossy().into_owned(), err, )); } }; // Persist it to the cache. fs_err::tokio::create_dir_all(target.parent().expect("Cache entry to have parent")) .await .map_err(Error::CacheWrite)?; if let Err(err) = rename_with_retry(extracted, target).await { // If the directory already exists, accept it. if err.kind() == std::io::ErrorKind::AlreadyExists { warn!("Directory already exists: {}", target.display()); } else { return Err(Error::CacheWrite(err)); } } Ok(hashes) } /// Build a source distribution, storing the built wheel in the cache. /// /// Returns the un-normalized disk filename, the parsed, normalized filename and the metadata #[instrument(skip_all, fields(dist = %source))] async fn build_distribution( &self, source: &BuildableSource<'_>, source_root: &Path, subdirectory: Option<&Path>, cache_shard: &CacheShard, source_strategy: SourceStrategy, ) -> Result<(String, WheelFilename, ResolutionMetadata), Error> { debug!("Building: {source}"); // Guard against build of source distributions when disabled. if self .build_context .build_options() .no_build_requirement(source.name()) { if source.is_editable() { debug!("Allowing build for editable source distribution: {source}"); } else { return Err(Error::NoBuild); } } // Build into a temporary directory, to prevent partial builds. let temp_dir = self .build_context .cache() .build_dir() .map_err(Error::CacheWrite)?; // Build the wheel. fs::create_dir_all(&cache_shard) .await .map_err(Error::CacheWrite)?; // Try a direct build if that isn't disabled and the uv build backend is used. let disk_filename = if let Some(name) = self .build_context .direct_build( source_root, subdirectory, temp_dir.path(), source_strategy, if source.is_editable() { BuildKind::Editable } else { BuildKind::Wheel }, Some(&source.to_string()), ) .await .map_err(|err| Error::Build(err.into()))? { // In the uv build backend, the normalized filename and the disk filename are the same. name.to_string() } else { // Identify the base Python interpreter to use in the cache key. let base_python = if cfg!(unix) { self.build_context .interpreter() .await .find_base_python() .map_err(Error::BaseInterpreter)? } else { self.build_context .interpreter() .await .to_base_python() .map_err(Error::BaseInterpreter)? }; let build_kind = if source.is_editable() { BuildKind::Editable } else { BuildKind::Wheel }; let build_key = BuildKey { base_python: base_python.into_boxed_path(), source_root: source_root.to_path_buf().into_boxed_path(), subdirectory: subdirectory .map(|subdirectory| subdirectory.to_path_buf().into_boxed_path()), source_strategy, build_kind, }; if let Some(builder) = self.build_context.build_arena().remove(&build_key) { debug!("Creating build environment for: {source}"); let wheel = builder.wheel(temp_dir.path()).await.map_err(Error::Build)?; // Store the build context. self.build_context.build_arena().insert(build_key, builder); wheel } else { debug!("Reusing existing build environment for: {source}"); let builder = self .build_context .setup_build( source_root, subdirectory, source_root, Some(&source.to_string()), source.as_dist(), source_strategy, if source.is_editable() { BuildKind::Editable } else { BuildKind::Wheel }, if uv_flags::contains(uv_flags::EnvironmentFlags::HIDE_BUILD_OUTPUT) { BuildOutput::Quiet } else { BuildOutput::Debug }, self.build_stack.cloned().unwrap_or_default(), ) .await .map_err(|err| Error::Build(err.into()))?; // Build the wheel. let wheel = builder.wheel(temp_dir.path()).await.map_err(Error::Build)?; // Store the build context. self.build_context.build_arena().insert(build_key, builder); wheel } }; // Read the metadata from the wheel. let filename = WheelFilename::from_str(&disk_filename)?; let metadata = read_wheel_metadata(&filename, &temp_dir.path().join(&disk_filename))?; // Validate the metadata. validate_metadata(source, &metadata)?; validate_filename(&filename, &metadata)?; // Move the wheel to the cache. rename_with_retry( temp_dir.path().join(&disk_filename), cache_shard.join(&disk_filename), ) .await .map_err(Error::CacheWrite)?; debug!("Built `{source}` into `{disk_filename}`"); Ok((disk_filename, filename, metadata)) } /// Build the metadata for a source distribution. #[instrument(skip_all, fields(dist = %source))] async fn build_metadata( &self, source: &BuildableSource<'_>, source_root: &Path, subdirectory: Option<&Path>, source_strategy: SourceStrategy, ) -> Result, Error> { debug!("Preparing metadata for: {source}"); // Ensure that the _installed_ Python version is compatible with the `requires-python` // specifier. if let Some(requires_python) = source.requires_python() { let installed = self.build_context.interpreter().await.python_version(); let target = release_specifiers_to_ranges(requires_python.clone()) .bounding_range() .map(|bounding_range| bounding_range.0.cloned()) .unwrap_or(Bound::Unbounded); let is_compatible = match target { Bound::Included(target) => *installed >= target, Bound::Excluded(target) => *installed > target, Bound::Unbounded => true, }; if !is_compatible { return Err(Error::RequiresPython( requires_python.clone(), installed.clone(), )); } } // Identify the base Python interpreter to use in the cache key. let base_python = if cfg!(unix) { self.build_context .interpreter() .await .find_base_python() .map_err(Error::BaseInterpreter)? } else { self.build_context .interpreter() .await .to_base_python() .map_err(Error::BaseInterpreter)? }; // Determine whether this is an editable or non-editable build. let build_kind = if source.is_editable() { BuildKind::Editable } else { BuildKind::Wheel }; // Set up the builder. let mut builder = self .build_context .setup_build( source_root, subdirectory, source_root, Some(&source.to_string()), source.as_dist(), source_strategy, build_kind, if uv_flags::contains(uv_flags::EnvironmentFlags::HIDE_BUILD_OUTPUT) { BuildOutput::Quiet } else { BuildOutput::Debug }, self.build_stack.cloned().unwrap_or_default(), ) .await .map_err(|err| Error::Build(err.into()))?; // Build the metadata. let dist_info = builder.metadata().await.map_err(Error::Build)?; // Store the build context. self.build_context.build_arena().insert( BuildKey { base_python: base_python.into_boxed_path(), source_root: source_root.to_path_buf().into_boxed_path(), subdirectory: subdirectory .map(|subdirectory| subdirectory.to_path_buf().into_boxed_path()), source_strategy, build_kind, }, builder, ); // Return the `.dist-info` directory, if it exists. let Some(dist_info) = dist_info else { return Ok(None); }; // Read the metadata from disk. debug!("Prepared metadata for: {source}"); let content = fs::read(dist_info.join("METADATA")) .await .map_err(Error::CacheRead)?; let metadata = ResolutionMetadata::parse_metadata(&content)?; // Validate the metadata. validate_metadata(source, &metadata)?; Ok(Some(metadata)) } /// Returns a GET [`reqwest::Request`] for the given URL. fn request( url: DisplaySafeUrl, client: &RegistryClient, ) -> Result { client .uncached_client(&url) .get(Url::from(url)) .header( // `reqwest` defaults to accepting compressed responses. // Specify identity encoding to get consistent .whl downloading // behavior from servers. ref: https://github.com/pypa/pip/pull/1688 "accept-encoding", reqwest::header::HeaderValue::from_static("identity"), ) .build() } } /// Prune any unused source distributions from the cache. pub fn prune(cache: &Cache) -> Result { let mut removal = Removal::default(); let bucket = cache.bucket(CacheBucket::SourceDistributions); if bucket.is_dir() { for entry in walkdir::WalkDir::new(bucket) { let entry = entry.map_err(Error::CacheWalk)?; if !entry.file_type().is_dir() { continue; } // If we find a `revision.http` file, read the pointer, and remove any extraneous // directories. let revision = entry.path().join("revision.http"); if revision.is_file() { if let Ok(Some(pointer)) = HttpRevisionPointer::read_from(revision) { // Remove all sibling directories that are not referenced by the pointer. for sibling in entry.path().read_dir().map_err(Error::CacheRead)? { let sibling = sibling.map_err(Error::CacheRead)?; if sibling.file_type().map_err(Error::CacheRead)?.is_dir() { let sibling_name = sibling.file_name(); if sibling_name != pointer.revision.id().as_str() { debug!( "Removing dangling source revision: {}", sibling.path().display() ); removal += uv_cache::rm_rf(sibling.path()).map_err(Error::CacheWrite)?; } } } } } // If we find a `revision.rev` file, read the pointer, and remove any extraneous // directories. let revision = entry.path().join("revision.rev"); if revision.is_file() { if let Ok(Some(pointer)) = LocalRevisionPointer::read_from(revision) { // Remove all sibling directories that are not referenced by the pointer. for sibling in entry.path().read_dir().map_err(Error::CacheRead)? { let sibling = sibling.map_err(Error::CacheRead)?; if sibling.file_type().map_err(Error::CacheRead)?.is_dir() { let sibling_name = sibling.file_name(); if sibling_name != pointer.revision.id().as_str() { debug!( "Removing dangling source revision: {}", sibling.path().display() ); removal += uv_cache::rm_rf(sibling.path()).map_err(Error::CacheWrite)?; } } } } } } } Ok(removal) } /// The result of extracting statically available metadata from a source distribution. #[derive(Debug)] enum StaticMetadata { /// The metadata was found and successfully read. Some(ResolutionMetadata), /// The metadata was found, but it was ignored due to a dynamic version. Dynamic, /// The metadata was not found. None, } impl StaticMetadata { /// Read the [`ResolutionMetadata`] from a source distribution. async fn read( source: &BuildableSource<'_>, source_root: &Path, subdirectory: Option<&Path>, ) -> Result { // Attempt to read the `pyproject.toml`. let pyproject_toml = match read_pyproject_toml(source_root, subdirectory).await { Ok(pyproject_toml) => Some(pyproject_toml), Err(Error::MissingPyprojectToml) => { debug!("No `pyproject.toml` available for: {source}"); None } Err(err) => return Err(err), }; // Determine whether the version is static or dynamic. let dynamic = pyproject_toml.as_ref().is_some_and(|pyproject_toml| { pyproject_toml.project.as_ref().is_some_and(|project| { project .dynamic .as_ref() .is_some_and(|dynamic| dynamic.iter().any(|field| field == "version")) }) }); // Attempt to read static metadata from the `pyproject.toml`. if let Some(pyproject_toml) = pyproject_toml { match ResolutionMetadata::parse_pyproject_toml(pyproject_toml, source.version()) { Ok(metadata) => { debug!("Found static `pyproject.toml` for: {source}"); // Validate the metadata, but ignore it if the metadata doesn't match. match validate_metadata(source, &metadata) { Ok(()) => { return Ok(Self::Some(metadata)); } Err(err) => { debug!("Ignoring `pyproject.toml` for {source}: {err}"); } } } Err( err @ (uv_pypi_types::MetadataError::Pep508Error(_) | uv_pypi_types::MetadataError::DynamicField(_) | uv_pypi_types::MetadataError::FieldNotFound(_) | uv_pypi_types::MetadataError::PoetrySyntax), ) => { debug!("No static `pyproject.toml` available for: {source} ({err:?})"); } Err(err) => return Err(Error::PyprojectToml(err)), } } // If the source distribution is a source tree, avoid reading `PKG-INFO`, since it could be // out-of-date. if source.is_source_tree() { return Ok(if dynamic { Self::Dynamic } else { Self::None }); } // Attempt to read static metadata from the `PKG-INFO` file. match read_pkg_info(source_root, subdirectory).await { Ok(metadata) => { debug!("Found static `PKG-INFO` for: {source}"); // Validate the metadata, but ignore it if the metadata doesn't match. match validate_metadata(source, &metadata) { Ok(()) => { // If necessary, mark the metadata as dynamic. let metadata = if dynamic { ResolutionMetadata { dynamic: true, ..metadata } } else { metadata }; return Ok(Self::Some(metadata)); } Err(err) => { debug!("Ignoring `PKG-INFO` for {source}: {err}"); } } } Err( err @ (Error::MissingPkgInfo | Error::PkgInfo( uv_pypi_types::MetadataError::Pep508Error(_) | uv_pypi_types::MetadataError::DynamicField(_) | uv_pypi_types::MetadataError::FieldNotFound(_) | uv_pypi_types::MetadataError::UnsupportedMetadataVersion(_), )), ) => { debug!("No static `PKG-INFO` available for: {source} ({err:?})"); } Err(err) => return Err(err), } Ok(Self::None) } } /// Returns `true` if a `pyproject.toml` has `tool.uv.sources`. fn has_sources(content: &str) -> Result { #[derive(serde::Deserialize)] struct PyProjectToml { tool: Option, } #[derive(serde::Deserialize)] struct Tool { uv: Option, } #[derive(serde::Deserialize)] struct ToolUv { sources: Option, } let PyProjectToml { tool } = toml::from_str(content)?; if let Some(tool) = tool { if let Some(uv) = tool.uv { if let Some(sources) = uv.sources { if !sources.inner().is_empty() { return Ok(true); } } } } Ok(false) } /// Validate that the source distribution matches the built metadata. fn validate_metadata( source: &BuildableSource<'_>, metadata: &ResolutionMetadata, ) -> Result<(), Error> { if let Some(name) = source.name() { if metadata.name != *name { return Err(Error::WheelMetadataNameMismatch { metadata: metadata.name.clone(), given: name.clone(), }); } } if let Some(version) = source.version() { if *version != metadata.version && *version != metadata.version.clone().without_local() { return Err(Error::WheelMetadataVersionMismatch { metadata: metadata.version.clone(), given: version.clone(), }); } } Ok(()) } /// Validate that the source distribution matches the built filename. fn validate_filename(filename: &WheelFilename, metadata: &ResolutionMetadata) -> Result<(), Error> { if metadata.name != filename.name { return Err(Error::WheelFilenameNameMismatch { metadata: metadata.name.clone(), filename: filename.name.clone(), }); } if metadata.version != filename.version { return Err(Error::WheelFilenameVersionMismatch { metadata: metadata.version.clone(), filename: filename.version.clone(), }); } Ok(()) } /// A pointer to a source distribution revision in the cache, fetched from an HTTP archive. /// /// Encoded with `MsgPack`, and represented on disk by a `.http` file. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct HttpRevisionPointer { revision: Revision, } impl HttpRevisionPointer { /// Read an [`HttpRevisionPointer`] from the cache. pub(crate) fn read_from(path: impl AsRef) -> Result, Error> { match fs_err::File::open(path.as_ref()) { Ok(file) => { let data = DataWithCachePolicy::from_reader(file)?.data; let revision = rmp_serde::from_slice::(&data)?; Ok(Some(Self { revision })) } Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(err) => Err(Error::CacheRead(err)), } } /// Return the [`Revision`] from the pointer. pub(crate) fn into_revision(self) -> Revision { self.revision } } /// A pointer to a source distribution revision in the cache, fetched from a local path. /// /// Encoded with `MsgPack`, and represented on disk by a `.rev` file. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct LocalRevisionPointer { cache_info: CacheInfo, revision: Revision, } impl LocalRevisionPointer { /// Read an [`LocalRevisionPointer`] from the cache. pub(crate) fn read_from(path: impl AsRef) -> Result, Error> { match fs_err::read(path) { Ok(cached) => Ok(Some(rmp_serde::from_slice::(&cached)?)), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(err) => Err(Error::CacheRead(err)), } } /// Write an [`LocalRevisionPointer`] to the cache. async fn write_to(&self, entry: &CacheEntry) -> Result<(), Error> { fs::create_dir_all(&entry.dir()) .await .map_err(Error::CacheWrite)?; write_atomic(entry.path(), rmp_serde::to_vec(&self)?) .await .map_err(Error::CacheWrite) } /// Return the [`CacheInfo`] for the pointer. pub(crate) fn cache_info(&self) -> &CacheInfo { &self.cache_info } /// Return the [`Revision`] for the pointer. pub(crate) fn revision(&self) -> &Revision { &self.revision } /// Return the [`Revision`] for the pointer. pub(crate) fn into_revision(self) -> Revision { self.revision } } /// Read the [`ResolutionMetadata`] from a source distribution's `PKG-INFO` file, if it uses Metadata 2.2 /// or later _and_ none of the required fields (`Requires-Python`, `Requires-Dist`, and /// `Provides-Extra`) are marked as dynamic. async fn read_pkg_info( source_tree: &Path, subdirectory: Option<&Path>, ) -> Result { // Read the `PKG-INFO` file. let pkg_info = match subdirectory { Some(subdirectory) => source_tree.join(subdirectory).join("PKG-INFO"), None => source_tree.join("PKG-INFO"), }; let content = match fs::read(pkg_info).await { Ok(content) => content, Err(err) if err.kind() == std::io::ErrorKind::NotFound => { return Err(Error::MissingPkgInfo); } Err(err) => return Err(Error::CacheRead(err)), }; // Parse the metadata. let metadata = ResolutionMetadata::parse_pkg_info(&content).map_err(Error::PkgInfo)?; Ok(metadata) } /// Read the [`ResolutionMetadata`] from a source distribution's `pyproject.toml` file, if it defines static /// metadata consistent with PEP 621. async fn read_pyproject_toml( source_tree: &Path, subdirectory: Option<&Path>, ) -> Result { // Read the `pyproject.toml` file. let pyproject_toml = match subdirectory { Some(subdirectory) => source_tree.join(subdirectory).join("pyproject.toml"), None => source_tree.join("pyproject.toml"), }; let content = match fs::read_to_string(pyproject_toml).await { Ok(content) => content, Err(err) if err.kind() == std::io::ErrorKind::NotFound => { return Err(Error::MissingPyprojectToml); } Err(err) => return Err(Error::CacheRead(err)), }; let pyproject_toml = PyProjectToml::from_toml(&content)?; Ok(pyproject_toml) } /// Wheel metadata stored in the source distribution cache. #[derive(Debug, Clone)] struct CachedMetadata(ResolutionMetadata); impl CachedMetadata { /// Read an existing cached [`ResolutionMetadata`], if it exists. async fn read(cache_entry: &CacheEntry) -> Result, Error> { match fs::read(&cache_entry.path()).await { Ok(cached) => Ok(Some(Self(rmp_serde::from_slice(&cached)?))), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(err) => Err(Error::CacheRead(err)), } } /// Returns `true` if the metadata matches the given package name and version. fn matches(&self, name: Option<&PackageName>, version: Option<&Version>) -> bool { name.is_none_or(|name| self.0.name == *name) && version.is_none_or(|version| self.0.version == *version) } } impl From for ResolutionMetadata { fn from(value: CachedMetadata) -> Self { value.0 } } /// Read the [`ResolutionMetadata`] from a built wheel. fn read_wheel_metadata( filename: &WheelFilename, wheel: &Path, ) -> Result { let file = fs_err::File::open(wheel).map_err(Error::CacheRead)?; let reader = std::io::BufReader::new(file); let mut archive = ZipArchive::new(reader)?; let dist_info = read_archive_metadata(filename, &mut archive) .map_err(|err| Error::WheelMetadata(wheel.to_path_buf(), Box::new(err)))?; Ok(ResolutionMetadata::parse_metadata(&dist_info)?) } uv-0.9.17+ds1/crates/uv-distribution/src/source/revision.rs000066400000000000000000000042431520155276700236650ustar00rootroot00000000000000use serde::{Deserialize, Serialize}; use std::path::Path; use uv_distribution_types::Hashed; use uv_pypi_types::{HashDigest, HashDigests}; /// The [`Revision`] is a thin wrapper around a unique identifier for the source distribution. /// /// A revision represents a unique version of a source distribution, at a level more granular than /// (e.g.) the version number of the distribution itself. For example, a source distribution hosted /// at a URL or a local file path may have multiple revisions, each representing a unique state of /// the distribution, despite the reported version number remaining the same. #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct Revision { id: RevisionId, hashes: HashDigests, } impl Revision { /// Initialize a new [`Revision`] with a random UUID. pub(crate) fn new() -> Self { Self { id: RevisionId::new(), hashes: HashDigests::empty(), } } /// Return the unique ID of the manifest. pub(crate) fn id(&self) -> &RevisionId { &self.id } /// Return the computed hashes of the archive. pub(crate) fn hashes(&self) -> &[HashDigest] { self.hashes.as_slice() } /// Return the computed hashes of the archive. pub(crate) fn into_hashes(self) -> HashDigests { self.hashes } /// Set the computed hashes of the archive. #[must_use] pub(crate) fn with_hashes(mut self, hashes: HashDigests) -> Self { self.hashes = hashes; self } } impl Hashed for Revision { fn hashes(&self) -> &[HashDigest] { self.hashes.as_slice() } } /// A unique identifier for a revision of a source distribution. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct RevisionId(String); impl RevisionId { /// Generate a new unique identifier for an archive. fn new() -> Self { Self(nanoid::nanoid!()) } pub(crate) fn as_str(&self) -> &str { self.0.as_str() } } impl AsRef for RevisionId { fn as_ref(&self) -> &str { self.0.as_ref() } } impl AsRef for RevisionId { fn as_ref(&self) -> &Path { self.0.as_ref() } } uv-0.9.17+ds1/crates/uv-extract/000077500000000000000000000000001520155276700163225ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-extract/Cargo.toml000066400000000000000000000025251520155276700202560ustar00rootroot00000000000000[package] name = "uv-extract" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-configuration = { workspace = true } uv-distribution-filename = { workspace = true } uv-pypi-types = { workspace = true } uv-static = { workspace = true } astral-tokio-tar = { workspace = true } async-compression = { workspace = true, features = ["bzip2", "gzip", "zstd", "xz"] } async_zip = { workspace = true } blake2 = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } md-5 = { workspace = true } rayon = { workspace = true } regex = { workspace = true } reqwest = { workspace = true } rustc-hash = { workspace = true } sha2 = { workspace = true } tar = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true, features = ["compat"] } tracing = { workspace = true } xz2 = { workspace = true } zip = { workspace = true } zstd = { workspace = true } [features] default = [] # Avoid a liblzma.so dependency static = ["xz2/static"] [package.metadata.cargo-shear] ignored = ["xz2"] uv-0.9.17+ds1/crates/uv-extract/README.md000066400000000000000000000010271520155276700176010ustar00rootroot00000000000000 # uv-extract This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-extract). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-extract/src/000077500000000000000000000000001520155276700171115ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-extract/src/error.rs000066400000000000000000000151351520155276700206150ustar00rootroot00000000000000use std::{ffi::OsString, path::PathBuf}; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("I/O operation failed during extraction")] Io(#[source] std::io::Error), #[error("Invalid zip file")] Zip(#[from] zip::result::ZipError), #[error("Invalid zip file structure")] AsyncZip(#[from] async_zip::error::ZipError), #[error("Invalid tar file")] Tar(#[from] tokio_tar::TarError), #[error( "The top-level of the archive must only contain a list directory, but it contains: {0:?}" )] NonSingularArchive(Vec), #[error("The top-level of the archive must only contain a list directory, but it's empty")] EmptyArchive, #[error("ZIP local header filename at offset {offset} does not use UTF-8 encoding")] LocalHeaderNotUtf8 { offset: u64 }, #[error("ZIP central directory entry filename at index {index} does not use UTF-8 encoding")] CentralDirectoryEntryNotUtf8 { index: u64 }, #[error("Bad CRC (got {computed:08x}, expected {expected:08x}) for file: {}", path.display())] BadCrc32 { path: PathBuf, computed: u32, expected: u32, }, #[error("Bad uncompressed size (got {computed:08x}, expected {expected:08x}) for file: {}", path.display())] BadUncompressedSize { path: PathBuf, computed: u64, expected: u64, }, #[error("Bad compressed size (got {computed:08x}, expected {expected:08x}) for file: {}", path.display())] BadCompressedSize { path: PathBuf, computed: u64, expected: u64, }, #[error("ZIP file contains multiple entries with different contents for: {}", path.display())] DuplicateLocalFileHeader { path: PathBuf }, #[error("ZIP file contains a local file header without a corresponding central-directory record entry for: {} ({offset})", path.display())] MissingCentralDirectoryEntry { path: PathBuf, offset: u64 }, #[error("ZIP file contains an end-of-central-directory record entry, but no local file header for: {} ({offset}", path.display())] MissingLocalFileHeader { path: PathBuf, offset: u64 }, #[error("ZIP file uses conflicting paths for the local file header at {} (got {}, expected {})", offset, local_path.display(), central_directory_path.display())] ConflictingPaths { offset: u64, local_path: PathBuf, central_directory_path: PathBuf, }, #[error("ZIP file uses conflicting checksums for the local file header and central-directory record (got {local_crc32}, expected {central_directory_crc32}) for: {} ({offset})", path.display())] ConflictingChecksums { path: PathBuf, offset: u64, local_crc32: u32, central_directory_crc32: u32, }, #[error("ZIP file uses conflicting compressed sizes for the local file header and central-directory record (got {local_compressed_size}, expected {central_directory_compressed_size}) for: {} ({offset})", path.display())] ConflictingCompressedSizes { path: PathBuf, offset: u64, local_compressed_size: u64, central_directory_compressed_size: u64, }, #[error("ZIP file uses conflicting uncompressed sizes for the local file header and central-directory record (got {local_uncompressed_size}, expected {central_directory_uncompressed_size}) for: {} ({offset})", path.display())] ConflictingUncompressedSizes { path: PathBuf, offset: u64, local_uncompressed_size: u64, central_directory_uncompressed_size: u64, }, #[error("ZIP file contains trailing contents after the end-of-central-directory record")] TrailingContents, #[error( "ZIP file reports a number of entries in the central directory that conflicts with the actual number of entries (got {actual}, expected {expected})" )] ConflictingNumberOfEntries { actual: u64, expected: u64 }, #[error("Data descriptor is missing for file: {}", path.display())] MissingDataDescriptor { path: PathBuf }, #[error("File contains an unexpected data descriptor: {}", path.display())] UnexpectedDataDescriptor { path: PathBuf }, #[error( "ZIP file end-of-central-directory record contains a comment that appears to be an embedded ZIP file" )] ZipInZip, #[error("ZIP64 end-of-central-directory record contains unsupported extensible data")] ExtensibleData, #[error("ZIP file end-of-central-directory record contains multiple entries with the same path, but conflicting modes: {}", path.display())] DuplicateExecutableFileHeader { path: PathBuf }, #[error("Archive contains a file with an empty filename")] EmptyFilename, #[error("Archive contains unacceptable filename: {filename}")] UnacceptableFilename { filename: String }, } impl Error { /// When reading from an archive, the error can either be an IO error from the underlying /// operating system, or an error with the archive. Both get wrapper into an IO error through /// e.g., `io::copy`. This method extracts zip and tar errors, to distinguish them from invalid /// archives. pub(crate) fn io_or_compression(err: std::io::Error) -> Self { if err.kind() != std::io::ErrorKind::Other { return Self::Io(err); } let err = match err.downcast::() { Ok(tar_err) => return Self::Tar(tar_err), Err(err) => err, }; let err = match err.downcast::() { Ok(zip_err) => return Self::AsyncZip(zip_err), Err(err) => err, }; let err = match err.downcast::() { Ok(zip_err) => return Self::Zip(zip_err), Err(err) => err, }; Self::Io(err) } /// Returns `true` if the error is due to the server not supporting HTTP streaming. Most /// commonly, this is due to serving ZIP files with features that are incompatible with /// streaming, like data descriptors. pub fn is_http_streaming_unsupported(&self) -> bool { matches!( self, Self::AsyncZip(async_zip::error::ZipError::FeatureNotSupported(_)) ) } /// Returns `true` if the error is due to HTTP streaming request failed. pub fn is_http_streaming_failed(&self) -> bool { match self { Self::AsyncZip(async_zip::error::ZipError::UpstreamReadError(_)) => true, Self::Io(err) => { if let Some(inner) = err.get_ref() { inner.downcast_ref::().is_some() } else { false } } _ => false, } } } uv-0.9.17+ds1/crates/uv-extract/src/hash.rs000066400000000000000000000063071520155276700204100ustar00rootroot00000000000000use blake2::digest::consts::U32; use sha2::Digest; use std::pin::Pin; use std::task::{Context, Poll}; use tokio::io::{AsyncReadExt, ReadBuf}; use uv_pypi_types::{HashAlgorithm, HashDigest}; #[derive(Debug)] pub enum Hasher { Md5(md5::Md5), Sha256(sha2::Sha256), Sha384(sha2::Sha384), Sha512(sha2::Sha512), Blake2b(blake2::Blake2b), } impl Hasher { pub fn update(&mut self, data: &[u8]) { match self { Self::Md5(hasher) => hasher.update(data), Self::Sha256(hasher) => hasher.update(data), Self::Sha384(hasher) => hasher.update(data), Self::Sha512(hasher) => hasher.update(data), Self::Blake2b(hasher) => hasher.update(data), } } } impl From for Hasher { fn from(algorithm: HashAlgorithm) -> Self { match algorithm { HashAlgorithm::Md5 => Self::Md5(md5::Md5::new()), HashAlgorithm::Sha256 => Self::Sha256(sha2::Sha256::new()), HashAlgorithm::Sha384 => Self::Sha384(sha2::Sha384::new()), HashAlgorithm::Sha512 => Self::Sha512(sha2::Sha512::new()), HashAlgorithm::Blake2b => Self::Blake2b(blake2::Blake2b::new()), } } } impl From for HashDigest { fn from(hasher: Hasher) -> Self { match hasher { Hasher::Md5(hasher) => Self { algorithm: HashAlgorithm::Md5, digest: format!("{:x}", hasher.finalize()).into(), }, Hasher::Sha256(hasher) => Self { algorithm: HashAlgorithm::Sha256, digest: format!("{:x}", hasher.finalize()).into(), }, Hasher::Sha384(hasher) => Self { algorithm: HashAlgorithm::Sha384, digest: format!("{:x}", hasher.finalize()).into(), }, Hasher::Sha512(hasher) => Self { algorithm: HashAlgorithm::Sha512, digest: format!("{:x}", hasher.finalize()).into(), }, Hasher::Blake2b(hasher) => Self { algorithm: HashAlgorithm::Blake2b, digest: format!("{:x}", hasher.finalize()).into(), }, } } } pub struct HashReader<'a, R> { reader: R, hashers: &'a mut [Hasher], } impl<'a, R> HashReader<'a, R> where R: tokio::io::AsyncRead + Unpin, { pub fn new(reader: R, hashers: &'a mut [Hasher]) -> Self { HashReader { reader, hashers } } /// Exhaust the underlying reader. pub async fn finish(&mut self) -> Result<(), std::io::Error> { while self.read(&mut vec![0; 8192]).await? > 0 {} Ok(()) } } impl tokio::io::AsyncRead for HashReader<'_, R> where R: tokio::io::AsyncRead + Unpin, { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { let reader = Pin::new(&mut self.reader); match reader.poll_read(cx, buf) { Poll::Ready(Ok(())) => { for hasher in self.hashers.iter_mut() { hasher.update(buf.filled()); } Poll::Ready(Ok(())) } other => other, } } } uv-0.9.17+ds1/crates/uv-extract/src/lib.rs000066400000000000000000000061371520155276700202340ustar00rootroot00000000000000use std::sync::LazyLock; pub use error::Error; use regex::Regex; pub use sync::*; use uv_static::EnvVars; mod error; pub mod hash; pub mod stream; mod sync; mod vendor; static CONTROL_CHARACTERS_RE: LazyLock = LazyLock::new(|| Regex::new(r"\p{C}").unwrap()); static REPLACEMENT_CHARACTER: &str = "\u{FFFD}"; /// Validate that a given filename (e.g. reported by a ZIP archive's /// local file entries or central directory entries) is "safe" to use. /// /// "Safe" in this context doesn't refer to directory traversal /// risk, but whether we believe that other ZIP implementations /// handle the name correctly and consistently. /// /// Specifically, we want to avoid names that: /// /// - Contain *any* non-printable characters /// - Are empty /// /// In the future, we may also want to check for names that contain /// leading/trailing whitespace, or names that are exceedingly long. pub(crate) fn validate_archive_member_name(name: &str) -> Result<(), Error> { if name.is_empty() { return Err(Error::EmptyFilename); } match CONTROL_CHARACTERS_RE.replace_all(name, REPLACEMENT_CHARACTER) { // No replacements mean no control characters. std::borrow::Cow::Borrowed(_) => Ok(()), std::borrow::Cow::Owned(sanitized) => Err(Error::UnacceptableFilename { filename: sanitized, }), } } /// Returns `true` if ZIP validation is disabled. pub(crate) fn insecure_no_validate() -> bool { // TODO(charlie) Parse this in `EnvironmentOptions`. let Some(value) = std::env::var_os(EnvVars::UV_INSECURE_NO_ZIP_VALIDATION) else { return false; }; let Some(value) = value.to_str() else { return false; }; matches!( value.to_lowercase().as_str(), "y" | "yes" | "t" | "true" | "on" | "1" ) } #[cfg(test)] mod tests { #[test] fn test_validate_archive_member_name() { for (testcase, ok) in &[ // Valid cases. ("normal.txt", true), ("__init__.py", true), ("fine i guess.py", true), ("🌈.py", true), // Invalid cases. ("", false), ("new\nline.py", false), ("carriage\rreturn.py", false), ("tab\tcharacter.py", false), ("null\0byte.py", false), ("control\x01code.py", false), ("control\x02code.py", false), ("control\x03code.py", false), ("control\x04code.py", false), ("backspace\x08code.py", false), ("delete\x7fcode.py", false), ] { assert_eq!( super::validate_archive_member_name(testcase).is_ok(), *ok, "testcase: {testcase}" ); } } #[test] fn test_unacceptable_filename_error_replaces_control_characters() { let err = super::validate_archive_member_name("bad\nname").unwrap_err(); match err { super::Error::UnacceptableFilename { filename } => { assert_eq!(filename, "bad�name"); } _ => panic!("expected UnacceptableFilename error"), } } } uv-0.9.17+ds1/crates/uv-extract/src/stream.rs000066400000000000000000000751421520155276700207630ustar00rootroot00000000000000use std::path::{Component, Path, PathBuf}; use std::pin::Pin; use async_zip::base::read::cd::Entry; use async_zip::error::ZipError; use futures::{AsyncReadExt, StreamExt}; use rustc_hash::{FxHashMap, FxHashSet}; use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; use tracing::{debug, warn}; use uv_distribution_filename::SourceDistExtension; use crate::{Error, insecure_no_validate, validate_archive_member_name}; const DEFAULT_BUF_SIZE: usize = 128 * 1024; #[derive(Debug, Clone, PartialEq, Eq)] struct LocalHeaderEntry { /// The relative path of the entry, as computed from the local file header. relpath: PathBuf, /// The computed CRC32 checksum of the entry. crc32: u32, /// The computed compressed size of the entry. compressed_size: u64, /// The computed uncompressed size of the entry. uncompressed_size: u64, /// Whether the entry has a data descriptor. data_descriptor: bool, } #[derive(Debug, Clone, PartialEq, Eq)] struct ComputedEntry { /// The computed CRC32 checksum of the entry. crc32: u32, /// The computed uncompressed size of the entry. uncompressed_size: u64, /// The computed compressed size of the entry. compressed_size: u64, } /// Unpack a `.zip` archive into the target directory, without requiring `Seek`. /// /// This is useful for unzipping files as they're being downloaded. If the archive /// is already fully on disk, consider using `unzip_archive`, which can use multiple /// threads to work faster in that case. pub async fn unzip( reader: R, target: impl AsRef, ) -> Result<(), Error> { /// Ensure the file path is safe to use as a [`Path`]. /// /// See: pub(crate) fn enclosed_name(file_name: &str) -> Option { if file_name.contains('\0') { return None; } let path = PathBuf::from(file_name); let mut depth = 0usize; for component in path.components() { match component { Component::Prefix(_) | Component::RootDir => return None, Component::ParentDir => depth = depth.checked_sub(1)?, Component::Normal(_) => depth += 1, Component::CurDir => (), } } Some(path) } // Determine whether ZIP validation is disabled. let skip_validation = insecure_no_validate(); let target = target.as_ref(); let mut reader = futures::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader.compat()); let mut zip = async_zip::base::read::stream::ZipFileReader::new(&mut reader); let mut directories = FxHashSet::default(); let mut local_headers = FxHashMap::default(); let mut offset = 0; while let Some(mut entry) = zip.next_with_entry().await? { // Construct the (expected) path to the file on-disk. let path = match entry.reader().entry().filename().as_str() { Ok(path) => path, Err(ZipError::StringNotUtf8) => return Err(Error::LocalHeaderNotUtf8 { offset }), Err(err) => return Err(err.into()), }; // Apply sanity checks to the file names in local headers. if let Err(e) = validate_archive_member_name(path) { if !skip_validation { return Err(e); } } // Sanitize the file name to prevent directory traversal attacks. let Some(relpath) = enclosed_name(path) else { warn!("Skipping unsafe file name: {path}"); // Close current file prior to proceeding, as per: // https://docs.rs/async_zip/0.0.16/async_zip/base/read/stream/ (.., zip) = entry.skip().await?; // Store the current offset. offset = zip.offset(); continue; }; let file_offset = entry.reader().entry().file_offset(); let expected_compressed_size = entry.reader().entry().compressed_size(); let expected_uncompressed_size = entry.reader().entry().uncompressed_size(); let expected_data_descriptor = entry.reader().entry().data_descriptor(); // Either create the directory or write the file to disk. let path = target.join(&relpath); let is_dir = entry.reader().entry().dir()?; let computed = if is_dir { if directories.insert(path.clone()) { fs_err::tokio::create_dir_all(path) .await .map_err(Error::Io)?; } // If this is a directory, we expect the CRC32 to be 0. if entry.reader().entry().crc32() != 0 { if !skip_validation { return Err(Error::BadCrc32 { path: relpath.clone(), computed: 0, expected: entry.reader().entry().crc32(), }); } } // If this is a directory, we expect the uncompressed size to be 0. if entry.reader().entry().uncompressed_size() != 0 { if !skip_validation { return Err(Error::BadUncompressedSize { path: relpath.clone(), computed: 0, expected: entry.reader().entry().uncompressed_size(), }); } } ComputedEntry { crc32: 0, uncompressed_size: 0, compressed_size: 0, } } else { if let Some(parent) = path.parent() { if directories.insert(parent.to_path_buf()) { fs_err::tokio::create_dir_all(parent) .await .map_err(Error::Io)?; } } // We don't know the file permissions here, because we haven't seen the central directory yet. let (actual_uncompressed_size, reader) = match fs_err::tokio::File::create_new(&path) .await { Ok(file) => { // Write the file to disk. let size = entry.reader().entry().uncompressed_size(); let mut writer = if let Ok(size) = usize::try_from(size) { tokio::io::BufWriter::with_capacity(std::cmp::min(size, 1024 * 1024), file) } else { tokio::io::BufWriter::new(file) }; let mut reader = entry.reader_mut().compat(); let bytes_read = tokio::io::copy(&mut reader, &mut writer) .await .map_err(Error::io_or_compression)?; let reader = reader.into_inner(); (bytes_read, reader) } Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { debug!( "Found duplicate local file header for: {}", relpath.display() ); // Read the existing file into memory. let existing_contents = fs_err::tokio::read(&path).await.map_err(Error::Io)?; // Read the entry into memory. let mut expected_contents = Vec::with_capacity(existing_contents.len()); let entry_reader = entry.reader_mut(); let bytes_read = entry_reader .read_to_end(&mut expected_contents) .await .map_err(Error::io_or_compression)?; // Verify that the existing file contents match the expected contents. if existing_contents != expected_contents { if !skip_validation { return Err(Error::DuplicateLocalFileHeader { path: relpath.clone(), }); } } (bytes_read as u64, entry_reader) } Err(err) => return Err(Error::Io(err)), }; // Validate the uncompressed size. if actual_uncompressed_size != expected_uncompressed_size { if !(expected_compressed_size == 0 && expected_data_descriptor) { if !skip_validation { return Err(Error::BadUncompressedSize { path: relpath.clone(), computed: actual_uncompressed_size, expected: expected_uncompressed_size, }); } } } // Validate the compressed size. let actual_compressed_size = reader.bytes_read(); if actual_compressed_size != expected_compressed_size { if !(expected_compressed_size == 0 && expected_data_descriptor) { if !skip_validation { return Err(Error::BadCompressedSize { path: relpath.clone(), computed: actual_compressed_size, expected: expected_compressed_size, }); } } } // Validate the CRC of any file we unpack // (It would be nice if async_zip made it harder to Not do this...) let actual_crc32 = reader.compute_hash(); let expected_crc32 = reader.entry().crc32(); if actual_crc32 != expected_crc32 { if !(expected_crc32 == 0 && expected_data_descriptor) { if !skip_validation { return Err(Error::BadCrc32 { path: relpath.clone(), computed: actual_crc32, expected: expected_crc32, }); } } } ComputedEntry { crc32: actual_crc32, uncompressed_size: actual_uncompressed_size, compressed_size: actual_compressed_size, } }; // Close current file prior to proceeding, as per: // https://docs.rs/async_zip/0.0.16/async_zip/base/read/stream/ let (descriptor, next) = entry.skip().await?; // Verify that the data descriptor field is consistent with the presence (or absence) of a // data descriptor in the local file header. if expected_data_descriptor && descriptor.is_none() { if !skip_validation { return Err(Error::MissingDataDescriptor { path: relpath.clone(), }); } } if !expected_data_descriptor && descriptor.is_some() { if !skip_validation { return Err(Error::UnexpectedDataDescriptor { path: relpath.clone(), }); } } // If we have a data descriptor, validate it. if let Some(descriptor) = descriptor { if descriptor.crc != computed.crc32 { if !skip_validation { return Err(Error::BadCrc32 { path: relpath.clone(), computed: computed.crc32, expected: descriptor.crc, }); } } if descriptor.uncompressed_size != computed.uncompressed_size { if !skip_validation { return Err(Error::BadUncompressedSize { path: relpath.clone(), computed: computed.uncompressed_size, expected: descriptor.uncompressed_size, }); } } if descriptor.compressed_size != computed.compressed_size { if !skip_validation { return Err(Error::BadCompressedSize { path: relpath.clone(), computed: computed.compressed_size, expected: descriptor.compressed_size, }); } } } // Store the offset, for validation, and error if we see a duplicate file. match local_headers.entry(file_offset) { std::collections::hash_map::Entry::Vacant(entry) => { entry.insert(LocalHeaderEntry { relpath, crc32: computed.crc32, uncompressed_size: computed.uncompressed_size, compressed_size: expected_compressed_size, data_descriptor: expected_data_descriptor, }); } std::collections::hash_map::Entry::Occupied(..) => { if !skip_validation { return Err(Error::DuplicateLocalFileHeader { path: relpath.clone(), }); } } } // Advance the reader to the next entry. zip = next; // Store the current offset. offset = zip.offset(); } // Record the actual number of entries in the central directory. let mut num_entries = 0; // Track the file modes on Unix, to ensure that they're consistent across duplicates. #[cfg(unix)] let mut modes = FxHashMap::with_capacity_and_hasher(local_headers.len(), rustc_hash::FxBuildHasher); let mut directory = async_zip::base::read::cd::CentralDirectoryReader::new(&mut reader, offset); loop { match directory.next().await? { Entry::CentralDirectoryEntry(entry) => { // Count the number of entries in the central directory. num_entries += 1; // Construct the (expected) path to the file on-disk. let path = match entry.filename().as_str() { Ok(path) => path, Err(ZipError::StringNotUtf8) => { return Err(Error::CentralDirectoryEntryNotUtf8 { index: num_entries - 1, }); } Err(err) => return Err(err.into()), }; // Apply sanity checks to the file names in CD headers. if let Err(e) = validate_archive_member_name(path) { if !skip_validation { return Err(e); } } // Sanitize the file name to prevent directory traversal attacks. let Some(relpath) = enclosed_name(path) else { continue; }; // Validate that various fields are consistent between the local file header and the // central directory entry. match local_headers.remove(&entry.file_offset()) { Some(local_header) => { if local_header.relpath != relpath { if !skip_validation { return Err(Error::ConflictingPaths { offset: entry.file_offset(), local_path: local_header.relpath.clone(), central_directory_path: relpath.clone(), }); } } if local_header.crc32 != entry.crc32() { if !skip_validation { return Err(Error::ConflictingChecksums { path: relpath.clone(), offset: entry.file_offset(), local_crc32: local_header.crc32, central_directory_crc32: entry.crc32(), }); } } if local_header.uncompressed_size != entry.uncompressed_size() { if !skip_validation { return Err(Error::ConflictingUncompressedSizes { path: relpath.clone(), offset: entry.file_offset(), local_uncompressed_size: local_header.uncompressed_size, central_directory_uncompressed_size: entry.uncompressed_size(), }); } } if local_header.compressed_size != entry.compressed_size() { if !local_header.data_descriptor { if !skip_validation { return Err(Error::ConflictingCompressedSizes { path: relpath.clone(), offset: entry.file_offset(), local_compressed_size: local_header.compressed_size, central_directory_compressed_size: entry.compressed_size(), }); } } } } None => { if !skip_validation { return Err(Error::MissingLocalFileHeader { path: relpath.clone(), offset: entry.file_offset(), }); } } } // On Unix, we need to set file permissions, which are stored in the central directory, at the // end of the archive. The `ZipFileReader` reads until it sees a central directory signature, // which indicates the first entry in the central directory. So we continue reading from there. #[cfg(unix)] { use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; if entry.dir()? { continue; } let Some(mode) = entry.unix_permissions() else { continue; }; // If the file is included multiple times, ensure that the mode is consistent. match modes.entry(relpath.clone()) { std::collections::hash_map::Entry::Vacant(entry) => { entry.insert(mode); } std::collections::hash_map::Entry::Occupied(entry) => { if mode != *entry.get() { if !skip_validation { return Err(Error::DuplicateExecutableFileHeader { path: relpath.clone(), }); } } } } // The executable bit is the only permission we preserve, otherwise we use the OS defaults. // https://github.com/pypa/pip/blob/3898741e29b7279e7bffe044ecfbe20f6a438b1e/src/pip/_internal/utils/unpacking.py#L88-L100 let has_any_executable_bit = mode & 0o111; if has_any_executable_bit != 0 { let path = target.join(relpath); let permissions = fs_err::tokio::metadata(&path) .await .map_err(Error::Io)? .permissions(); if permissions.mode() & 0o111 != 0o111 { fs_err::tokio::set_permissions( &path, Permissions::from_mode(permissions.mode() | 0o111), ) .await .map_err(Error::Io)?; } } } } Entry::EndOfCentralDirectoryRecord { record, comment, extensible, } => { // Reject ZIP64 end-of-central-directory records with extensible data, as the safety // tradeoffs don't outweigh the usefulness. We don't ever expect to encounter wheels // that leverage this feature anyway. if extensible { if !skip_validation { return Err(Error::ExtensibleData); } } // Sanitize the comment by rejecting bytes `01` to `08`. If the comment contains an // embedded ZIP file, it _must_ contain one of these bytes, which are otherwise // very rare (non-printing) characters. if comment.as_bytes().iter().any(|&b| (1..=8).contains(&b)) { if !skip_validation { return Err(Error::ZipInZip); } } // Validate that the reported number of entries match what we experienced while // reading the local file headers. if record.num_entries() != num_entries { if !skip_validation { return Err(Error::ConflictingNumberOfEntries { expected: num_entries, actual: record.num_entries(), }); } } break; } } } // If we didn't see the file in the central directory, it means it was not present in the // archive. if !skip_validation { if let Some((key, value)) = local_headers.iter().next() { return Err(Error::MissingCentralDirectoryEntry { offset: *key, path: value.relpath.clone(), }); } } // Determine whether the reader is exhausted, but allow trailing null bytes, which some zip // implementations incorrectly include. if !skip_validation { let mut has_trailing_bytes = false; let mut buf = [0u8; 256]; loop { let n = reader.read(&mut buf).await.map_err(Error::Io)?; if n == 0 { if has_trailing_bytes { warn!("Ignoring trailing null bytes in ZIP archive"); } break; } for &b in &buf[..n] { if b == 0 { has_trailing_bytes = true; } else { return Err(Error::TrailingContents); } } } } Ok(()) } /// Unpack the given tar archive into the destination directory. /// /// This is equivalent to `archive.unpack_in(dst)`, but it also preserves the executable bit. async fn untar_in( mut archive: tokio_tar::Archive<&'_ mut (dyn tokio::io::AsyncRead + Unpin)>, dst: &Path, ) -> std::io::Result<()> { // Like `tokio-tar`, canonicalize the destination prior to unpacking. let dst = fs_err::tokio::canonicalize(dst).await?; // Memoize filesystem calls to canonicalize paths. let mut memo = FxHashSet::default(); let mut entries = archive.entries()?; let mut pinned = Pin::new(&mut entries); while let Some(entry) = pinned.next().await { // Unpack the file into the destination directory. let mut file = entry?; // On Windows, skip symlink entries, as they're not supported. pip recursively copies the // symlink target instead. if cfg!(windows) && file.header().entry_type().is_symlink() { warn!( "Skipping symlink in tar archive: {}", file.path()?.display() ); continue; } // Unpack the file into the destination directory. #[cfg_attr(not(unix), allow(unused_variables))] let unpacked_at = file.unpack_in_raw(&dst, &mut memo).await?; // Preserve the executable bit. #[cfg(unix)] { use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; let entry_type = file.header().entry_type(); if entry_type.is_file() || entry_type.is_hard_link() { let mode = file.header().mode()?; let has_any_executable_bit = mode & 0o111; if has_any_executable_bit != 0 { if let Some(path) = unpacked_at.as_deref() { let permissions = fs_err::tokio::metadata(&path).await?.permissions(); if permissions.mode() & 0o111 != 0o111 { fs_err::tokio::set_permissions( &path, Permissions::from_mode(permissions.mode() | 0o111), ) .await?; } } } } } } Ok(()) } /// Unpack a `.tar.gz` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. pub async fn untar_gz( reader: R, target: impl AsRef, ) -> Result<(), Error> { let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); let mut decompressed_bytes = async_compression::tokio::bufread::GzipDecoder::new(reader); let archive = tokio_tar::ArchiveBuilder::new( &mut decompressed_bytes as &mut (dyn tokio::io::AsyncRead + Unpin), ) .set_preserve_mtime(false) .set_preserve_permissions(false) .set_allow_external_symlinks(false) .build(); untar_in(archive, target.as_ref()) .await .map_err(Error::io_or_compression) } /// Unpack a `.tar.bz2` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. pub async fn untar_bz2( reader: R, target: impl AsRef, ) -> Result<(), Error> { let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); let mut decompressed_bytes = async_compression::tokio::bufread::BzDecoder::new(reader); let archive = tokio_tar::ArchiveBuilder::new( &mut decompressed_bytes as &mut (dyn tokio::io::AsyncRead + Unpin), ) .set_preserve_mtime(false) .set_preserve_permissions(false) .set_allow_external_symlinks(false) .build(); untar_in(archive, target.as_ref()) .await .map_err(Error::io_or_compression) } /// Unpack a `.tar.zst` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. pub async fn untar_zst( reader: R, target: impl AsRef, ) -> Result<(), Error> { let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); let mut decompressed_bytes = async_compression::tokio::bufread::ZstdDecoder::new(reader); let archive = tokio_tar::ArchiveBuilder::new( &mut decompressed_bytes as &mut (dyn tokio::io::AsyncRead + Unpin), ) .set_preserve_mtime(false) .set_preserve_permissions(false) .set_allow_external_symlinks(false) .build(); untar_in(archive, target.as_ref()) .await .map_err(Error::io_or_compression) } /// Unpack a `.tar.zst` archive from a file on disk into the target directory. pub fn untar_zst_file(reader: R, target: impl AsRef) -> Result<(), Error> { let reader = std::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); let decompressed = zstd::Decoder::new(reader).map_err(Error::Io)?; let mut archive = tar::Archive::new(decompressed); archive.set_preserve_mtime(false); archive.unpack(target).map_err(Error::io_or_compression)?; Ok(()) } /// Unpack a `.tar.xz` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. pub async fn untar_xz( reader: R, target: impl AsRef, ) -> Result<(), Error> { let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); let mut decompressed_bytes = async_compression::tokio::bufread::XzDecoder::new(reader); let archive = tokio_tar::ArchiveBuilder::new( &mut decompressed_bytes as &mut (dyn tokio::io::AsyncRead + Unpin), ) .set_preserve_mtime(false) .set_preserve_permissions(false) .set_allow_external_symlinks(false) .build(); untar_in(archive, target.as_ref()) .await .map_err(Error::io_or_compression)?; Ok(()) } /// Unpack a `.tar` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. pub async fn untar( reader: R, target: impl AsRef, ) -> Result<(), Error> { let mut reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); let archive = tokio_tar::ArchiveBuilder::new(&mut reader as &mut (dyn tokio::io::AsyncRead + Unpin)) .set_preserve_mtime(false) .set_preserve_permissions(false) .set_allow_external_symlinks(false) .build(); untar_in(archive, target.as_ref()) .await .map_err(Error::io_or_compression)?; Ok(()) } /// Unpack a `.zip`, `.tar.gz`, `.tar.bz2`, `.tar.zst`, or `.tar.xz` archive into the target directory, /// without requiring `Seek`. pub async fn archive( reader: R, ext: SourceDistExtension, target: impl AsRef, ) -> Result<(), Error> { match ext { SourceDistExtension::Zip => { unzip(reader, target).await?; } SourceDistExtension::Tar => { untar(reader, target).await?; } SourceDistExtension::Tgz | SourceDistExtension::TarGz => { untar_gz(reader, target).await?; } SourceDistExtension::Tbz | SourceDistExtension::TarBz2 => { untar_bz2(reader, target).await?; } SourceDistExtension::Txz | SourceDistExtension::TarXz | SourceDistExtension::Tlz | SourceDistExtension::TarLz | SourceDistExtension::TarLzma => { untar_xz(reader, target).await?; } SourceDistExtension::TarZst => { untar_zst(reader, target).await?; } } Ok(()) } uv-0.9.17+ds1/crates/uv-extract/src/sync.rs000066400000000000000000000112771520155276700204430ustar00rootroot00000000000000use std::path::{Path, PathBuf}; use std::sync::{LazyLock, Mutex}; use crate::vendor::{CloneableSeekableReader, HasLength}; use crate::{Error, insecure_no_validate, validate_archive_member_name}; use rayon::prelude::*; use rustc_hash::FxHashSet; use tracing::warn; use uv_configuration::RAYON_INITIALIZE; use zip::ZipArchive; /// Unzip a `.zip` archive into the target directory. pub fn unzip( reader: R, target: &Path, ) -> Result<(), Error> { // Unzip in parallel. let reader = std::io::BufReader::new(reader); let archive = ZipArchive::new(CloneableSeekableReader::new(reader))?; let directories = Mutex::new(FxHashSet::default()); let skip_validation = insecure_no_validate(); // Initialize the threadpool with the user settings. LazyLock::force(&RAYON_INITIALIZE); (0..archive.len()) .into_par_iter() .map(|file_number| { let mut archive = archive.clone(); let mut file = archive.by_index(file_number)?; if let Err(e) = validate_archive_member_name(file.name()) { if !skip_validation { return Err(e); } } // Determine the path of the file within the wheel. let Some(enclosed_name) = file.enclosed_name() else { warn!("Skipping unsafe file name: {}", file.name()); return Ok(()); }; // Create necessary parent directories. let path = target.join(enclosed_name); if file.is_dir() { let mut directories = directories.lock().unwrap(); if directories.insert(path.clone()) { fs_err::create_dir_all(path).map_err(Error::Io)?; } return Ok(()); } if let Some(parent) = path.parent() { let mut directories = directories.lock().unwrap(); if directories.insert(parent.to_path_buf()) { fs_err::create_dir_all(parent).map_err(Error::Io)?; } } // Copy the file contents. let outfile = fs_err::File::create(&path).map_err(Error::Io)?; let size = file.size(); if size > 0 { let mut writer = if let Ok(size) = usize::try_from(size) { std::io::BufWriter::with_capacity(std::cmp::min(size, 1024 * 1024), outfile) } else { std::io::BufWriter::new(outfile) }; std::io::copy(&mut file, &mut writer).map_err(Error::io_or_compression)?; } // See `uv_extract::stream::unzip`. For simplicity, this is identical with the code there except for being // sync. #[cfg(unix)] { use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; if let Some(mode) = file.unix_mode() { // https://github.com/pypa/pip/blob/3898741e29b7279e7bffe044ecfbe20f6a438b1e/src/pip/_internal/utils/unpacking.py#L88-L100 let has_any_executable_bit = mode & 0o111; if has_any_executable_bit != 0 { let permissions = fs_err::metadata(&path).map_err(Error::Io)?.permissions(); if permissions.mode() & 0o111 != 0o111 { fs_err::set_permissions( &path, Permissions::from_mode(permissions.mode() | 0o111), ) .map_err(Error::Io)?; } } } } Ok(()) }) .collect::>() } /// Extract the top-level directory from an unpacked archive. /// /// The specification says: /// > A .tar.gz source distribution (sdist) contains a single top-level directory called /// > `{name}-{version}` (e.g. foo-1.0), containing the source files of the package. /// /// This function returns the path to that top-level directory. pub fn strip_component(source: impl AsRef) -> Result { // TODO(konstin): Verify the name of the directory. let top_level = fs_err::read_dir(source.as_ref()) .map_err(Error::Io)? .collect::>>() .map_err(Error::Io)?; match top_level.as_slice() { [root] => Ok(root.path()), [] => Err(Error::EmptyArchive), _ => Err(Error::NonSingularArchive( top_level .into_iter() .map(|entry| entry.file_name()) .collect(), )), } } uv-0.9.17+ds1/crates/uv-extract/src/vendor/000077500000000000000000000000001520155276700204065ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-extract/src/vendor/LICENSE000066400000000000000000000304051520155276700214150ustar00rootroot00000000000000This software is distributed under the terms of both the MIT license and the Apache License (Version 2.0). MIT license Copyright 2022 Google LLC 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. Apache 2 license 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. uv-0.9.17+ds1/crates/uv-extract/src/vendor/cloneable_seekable_reader.rs000066400000000000000000000130651520155276700260620ustar00rootroot00000000000000// Copyright 2022 Google LLC // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. #![allow(clippy::cast_sign_loss)] use std::{ io::{BufReader, Cursor, Read, Seek, SeekFrom}, sync::{Arc, Mutex}, }; /// A trait to represent some reader which has a total length known in /// advance. This is roughly equivalent to the nightly /// [`Seek::stream_len`] API. #[allow(clippy::len_without_is_empty)] pub trait HasLength { /// Return the current total length of this stream. fn len(&self) -> u64; } /// A [`Read`] which refers to its underlying stream by reference count, /// and thus can be cloned cheaply. It supports seeking; each cloned instance /// maintains its own pointer into the file, and the underlying instance /// is seeked prior to each read. pub(crate) struct CloneableSeekableReader { file: Arc>, pos: u64, // TODO determine and store this once instead of per cloneable file file_length: Option, } impl Clone for CloneableSeekableReader { fn clone(&self) -> Self { Self { file: self.file.clone(), pos: self.pos, file_length: self.file_length, } } } impl CloneableSeekableReader { /// Constructor. Takes ownership of the underlying `Read`. /// You should pass in only streams whose total length you expect /// to be fixed and unchanging. Odd behavior may occur if the length /// of the stream changes; any subsequent seeks will not take account /// of the changed stream length. pub(crate) fn new(file: R) -> Self { Self { file: Arc::new(Mutex::new(file)), pos: 0u64, file_length: None, } } /// Determine the length of the underlying stream. fn ascertain_file_length(&mut self) -> u64 { self.file_length.unwrap_or_else(|| { let len = self.file.lock().unwrap().len(); self.file_length = Some(len); len }) } } impl Read for CloneableSeekableReader { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let mut underlying_file = self.file.lock().expect("Unable to get underlying file"); // TODO share an object which knows current position to avoid unnecessary // seeks underlying_file.seek(SeekFrom::Start(self.pos))?; let read_result = underlying_file.read(buf); if let Ok(bytes_read) = read_result { // TODO, once stabilised, use checked_add_signed self.pos += bytes_read as u64; } read_result } } impl Seek for CloneableSeekableReader { fn seek(&mut self, pos: SeekFrom) -> std::io::Result { let new_pos = match pos { SeekFrom::Start(pos) => pos, SeekFrom::End(offset_from_end) => { let file_len = self.ascertain_file_length(); if -offset_from_end as u64 > file_len { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "Seek too far backwards", )); } // TODO, once stabilised, use checked_add_signed file_len - (-offset_from_end as u64) } // TODO, once stabilised, use checked_add_signed SeekFrom::Current(offset_from_pos) => { if offset_from_pos > 0 { self.pos + (offset_from_pos as u64) } else { self.pos - ((-offset_from_pos) as u64) } } }; self.pos = new_pos; Ok(new_pos) } } impl HasLength for BufReader { fn len(&self) -> u64 { self.get_ref().len() } } #[allow(clippy::disallowed_types)] impl HasLength for std::fs::File { fn len(&self) -> u64 { self.metadata().unwrap().len() } } impl HasLength for fs_err::File { fn len(&self) -> u64 { self.metadata().unwrap().len() } } impl HasLength for Cursor> { fn len(&self) -> u64 { self.get_ref().len() as u64 } } impl HasLength for Cursor<&Vec> { fn len(&self) -> u64 { self.get_ref().len() as u64 } } #[cfg(test)] mod test { use std::io::{Cursor, Read, Seek, SeekFrom}; use super::CloneableSeekableReader; #[test] fn test_cloneable_seekable_reader() { let buf: Vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; let buf = Cursor::new(buf); let mut reader = CloneableSeekableReader::new(buf); let mut out = vec![0; 2]; assert!(reader.read_exact(&mut out).is_ok()); assert_eq!(out[0], 0); assert_eq!(out[1], 1); assert!(reader.seek(SeekFrom::Start(0)).is_ok()); assert!(reader.read_exact(&mut out).is_ok()); assert_eq!(out[0], 0); assert_eq!(out[1], 1); assert!(reader.stream_position().is_ok()); assert!(reader.read_exact(&mut out).is_ok()); assert_eq!(out[0], 2); assert_eq!(out[1], 3); assert!(reader.seek(SeekFrom::End(-2)).is_ok()); assert!(reader.read_exact(&mut out).is_ok()); assert_eq!(out[0], 8); assert_eq!(out[1], 9); assert!(reader.read_exact(&mut out).is_err()); } } uv-0.9.17+ds1/crates/uv-extract/src/vendor/mod.rs000066400000000000000000000001601520155276700215300ustar00rootroot00000000000000pub(crate) use cloneable_seekable_reader::{CloneableSeekableReader, HasLength}; mod cloneable_seekable_reader; uv-0.9.17+ds1/crates/uv-flags/000077500000000000000000000000001520155276700157445ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-flags/Cargo.toml000066400000000000000000000006711520155276700177000ustar00rootroot00000000000000[package] name = "uv-flags" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] bitflags = { workspace = true } [dev-dependencies] [features] default = [] uv-0.9.17+ds1/crates/uv-flags/README.md000066400000000000000000000010231520155276700172170ustar00rootroot00000000000000 # uv-flags This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-flags). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-flags/src/000077500000000000000000000000001520155276700165335ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-flags/src/lib.rs000066400000000000000000000012011520155276700176410ustar00rootroot00000000000000use std::sync::OnceLock; static FLAGS: OnceLock = OnceLock::new(); bitflags::bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct EnvironmentFlags: u32 { const SKIP_WHEEL_FILENAME_CHECK = 1 << 0; const HIDE_BUILD_OUTPUT = 1 << 1; } } /// Initialize the environment flags. #[allow(clippy::result_unit_err)] pub fn init(flags: EnvironmentFlags) -> Result<(), ()> { FLAGS.set(flags).map_err(|_| ()) } /// Check if a specific environment flag is set. pub fn contains(flag: EnvironmentFlags) -> bool { FLAGS.get_or_init(EnvironmentFlags::default).contains(flag) } uv-0.9.17+ds1/crates/uv-fs/000077500000000000000000000000001520155276700152605ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-fs/Cargo.toml000066400000000000000000000022231520155276700172070ustar00rootroot00000000000000[package] name = "uv-fs" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-static = { workspace = true } dunce = { workspace = true } either = { workspace = true } encoding_rs_io = { workspace = true } fs-err = { workspace = true } path-slash = { workspace = true } percent-encoding = { workspace = true } same-file = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, optional = true } tracing = { workspace = true } [target.'cfg(any(unix, target_os = "wasi", target_os = "redox"))'.dependencies] rustix = { workspace = true } [target.'cfg(windows)'.dependencies] backon = { workspace = true } junction = { workspace = true } windows = { workspace = true } [features] default = [] tokio = ["dep:tokio", "fs-err/tokio"] uv-0.9.17+ds1/crates/uv-fs/README.md000066400000000000000000000010151520155276700165340ustar00rootroot00000000000000 # uv-fs This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-fs). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-fs/src/000077500000000000000000000000001520155276700160475ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-fs/src/cachedir.rs000066400000000000000000000033511520155276700201610ustar00rootroot00000000000000//! Vendored from cachedir 0.3.1 to replace `std::fs` with `fs_err`. use std::io::Write; use std::{io, path}; /// The `CACHEDIR.TAG` file header as defined by the [specification](https://bford.info/cachedir/). const HEADER: &[u8; 43] = b"Signature: 8a477f597d28d172789f06886806bc55"; /// Adds a tag to the specified `directory`. /// /// Will return an error if: /// /// * The `directory` exists and contains a `CACHEDIR.TAG` file, regardless of its content. /// * The file can't be created for any reason (the `directory` doesn't exist, permission error, /// can't write to the file etc.) pub fn add_tag>(directory: P) -> io::Result<()> { let directory = directory.as_ref(); match fs_err::OpenOptions::new() .write(true) .create_new(true) .open(directory.join("CACHEDIR.TAG")) { Ok(mut cachedir_tag) => cachedir_tag.write_all(HEADER), Err(e) => Err(e), } } /// Ensures the tag exists in `directory`. /// /// This function considers the `CACHEDIR.TAG` file in `directory` existing, regardless of its /// content, as a success. /// /// Will return an error if The tag file doesn't exist and can't be created for any reason /// (the `directory` doesn't exist, permission error, can't write to the file etc.). pub fn ensure_tag>(directory: P) -> io::Result<()> { match add_tag(&directory) { Err(e) => match e.kind() { io::ErrorKind::AlreadyExists => Ok(()), // If it exists, but we can't write to it for some reason don't fail io::ErrorKind::PermissionDenied if directory.as_ref().join("CACHEDIR.TAG").exists() => { Ok(()) } _ => Err(e), }, other => other, } } uv-0.9.17+ds1/crates/uv-fs/src/lib.rs000066400000000000000000000665161520155276700172010ustar00rootroot00000000000000use std::borrow::Cow; use std::path::{Path, PathBuf}; use tempfile::NamedTempFile; use tracing::warn; pub use crate::locked_file::*; pub use crate::path::*; pub mod cachedir; mod locked_file; mod path; pub mod which; /// Append an extension to a [`PathBuf`]. /// /// Unlike [`Path::with_extension`], this function does not replace an existing extension. /// /// If there is no file name, the path is returned unchanged. /// /// This mimics the behavior of the unstable [`Path::with_added_extension`] method. pub fn with_added_extension<'a>(path: &'a Path, extension: &str) -> Cow<'a, Path> { let Some(name) = path.file_name() else { // If there is no file name, we cannot add an extension. return Cow::Borrowed(path); }; let mut name = name.to_os_string(); name.push("."); name.push(extension.trim_start_matches('.')); Cow::Owned(path.with_file_name(name)) } /// Attempt to check if the two paths refer to the same file. /// /// Returns `Some(true)` if the files are missing, but would be the same if they existed. pub fn is_same_file_allow_missing(left: &Path, right: &Path) -> Option { // First, check an exact path comparison. if left == right { return Some(true); } // Second, check the files directly. if let Ok(value) = same_file::is_same_file(left, right) { return Some(value); } // Often, one of the directories won't exist yet so perform the comparison up a level. if let (Some(left_parent), Some(right_parent), Some(left_name), Some(right_name)) = ( left.parent(), right.parent(), left.file_name(), right.file_name(), ) { match same_file::is_same_file(left_parent, right_parent) { Ok(true) => return Some(left_name == right_name), Ok(false) => return Some(false), _ => (), } } // We couldn't determine if they're the same. None } /// Reads data from the path and requires that it be valid UTF-8 or UTF-16. /// /// This uses BOM sniffing to determine if the data should be transcoded /// from UTF-16 to Rust's `String` type (which uses UTF-8). /// /// This should generally only be used when one specifically wants to support /// reading UTF-16 transparently. /// /// If the file path is `-`, then contents are read from stdin instead. #[cfg(feature = "tokio")] pub async fn read_to_string_transcode(path: impl AsRef) -> std::io::Result { use std::io::Read; use encoding_rs_io::DecodeReaderBytes; let path = path.as_ref(); let raw = if path == Path::new("-") { let mut buf = Vec::with_capacity(1024); std::io::stdin().read_to_end(&mut buf)?; buf } else { fs_err::tokio::read(path).await? }; let mut buf = String::with_capacity(1024); DecodeReaderBytes::new(&*raw) .read_to_string(&mut buf) .map_err(|err| { let path = path.display(); std::io::Error::other(format!("failed to decode file {path}: {err}")) })?; Ok(buf) } /// Create a symlink at `dst` pointing to `src`, replacing any existing symlink. /// /// On Windows, this uses the `junction` crate to create a junction point. The /// operation is _not_ atomic, as we first delete the junction, then create a /// junction at the same path. /// /// Note that because junctions are used, the source must be a directory. /// /// Changes to this function should be reflected in [`create_symlink`]. #[cfg(windows)] pub fn replace_symlink(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { // If the source is a file, we can't create a junction if src.as_ref().is_file() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, format!( "Cannot create a junction for {}: is not a directory", src.as_ref().display() ), )); } // Remove the existing symlink, if any. match junction::delete(dunce::simplified(dst.as_ref())) { Ok(()) => match fs_err::remove_dir_all(dst.as_ref()) { Ok(()) => {} Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} Err(err) => return Err(err), }, Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} Err(err) => return Err(err), } // Replace it with a new symlink. junction::create( dunce::simplified(src.as_ref()), dunce::simplified(dst.as_ref()), ) } /// Create a symlink at `dst` pointing to `src`, replacing any existing symlink if necessary. /// /// On Unix, this method creates a temporary file, then moves it into place. #[cfg(unix)] pub fn replace_symlink(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { // Attempt to create the symlink directly. match fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref()) { Ok(()) => Ok(()), Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { // Create a symlink, using a temporary file to ensure atomicity. let temp_dir = tempfile::tempdir_in(dst.as_ref().parent().unwrap())?; let temp_file = temp_dir.path().join("link"); fs_err::os::unix::fs::symlink(src, &temp_file)?; // Move the symlink into the target location. fs_err::rename(&temp_file, dst.as_ref())?; Ok(()) } Err(err) => Err(err), } } /// Create a symlink at `dst` pointing to `src`. /// /// On Windows, this uses the `junction` crate to create a junction point. /// /// Note that because junctions are used, the source must be a directory. /// /// Changes to this function should be reflected in [`replace_symlink`]. #[cfg(windows)] pub fn create_symlink(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { // If the source is a file, we can't create a junction if src.as_ref().is_file() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, format!( "Cannot create a junction for {}: is not a directory", src.as_ref().display() ), )); } junction::create( dunce::simplified(src.as_ref()), dunce::simplified(dst.as_ref()), ) } /// Create a symlink at `dst` pointing to `src`. #[cfg(unix)] pub fn create_symlink(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref()) } #[cfg(unix)] pub fn remove_symlink(path: impl AsRef) -> std::io::Result<()> { fs_err::remove_file(path.as_ref()) } /// Create a symlink at `dst` pointing to `src` on Unix or copy `src` to `dst` on Windows /// /// This does not replace an existing symlink or file at `dst`. /// /// This does not fallback to copying on Unix. /// /// This function should only be used for files. If targeting a directory, use [`replace_symlink`] /// instead; it will use a junction on Windows, which is more performant. pub fn symlink_or_copy_file(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { #[cfg(windows)] { fs_err::copy(src.as_ref(), dst.as_ref())?; } #[cfg(unix)] { fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?; } Ok(()) } #[cfg(windows)] pub fn remove_symlink(path: impl AsRef) -> std::io::Result<()> { match junction::delete(dunce::simplified(path.as_ref())) { Ok(()) => match fs_err::remove_dir_all(path.as_ref()) { Ok(()) => Ok(()), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), Err(err) => Err(err), }, Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), Err(err) => Err(err), } } /// Return a [`NamedTempFile`] in the specified directory. /// /// Sets the permissions of the temporary file to `0o666`, to match the non-temporary file default. /// ([`NamedTempfile`] defaults to `0o600`.) #[cfg(unix)] pub fn tempfile_in(path: &Path) -> std::io::Result { use std::os::unix::fs::PermissionsExt; tempfile::Builder::new() .permissions(std::fs::Permissions::from_mode(0o666)) .tempfile_in(path) } /// Return a [`NamedTempFile`] in the specified directory. #[cfg(not(unix))] pub fn tempfile_in(path: &Path) -> std::io::Result { tempfile::Builder::new().tempfile_in(path) } /// Write `data` to `path` atomically using a temporary file and atomic rename. #[cfg(feature = "tokio")] pub async fn write_atomic(path: impl AsRef, data: impl AsRef<[u8]>) -> std::io::Result<()> { let temp_file = tempfile_in( path.as_ref() .parent() .expect("Write path must have a parent"), )?; fs_err::tokio::write(&temp_file, &data).await?; persist_with_retry(temp_file, path.as_ref()).await } /// Write `data` to `path` atomically using a temporary file and atomic rename. pub fn write_atomic_sync(path: impl AsRef, data: impl AsRef<[u8]>) -> std::io::Result<()> { let temp_file = tempfile_in( path.as_ref() .parent() .expect("Write path must have a parent"), )?; fs_err::write(&temp_file, &data)?; persist_with_retry_sync(temp_file, path.as_ref()) } /// Copy `from` to `to` atomically using a temporary file and atomic rename. pub fn copy_atomic_sync(from: impl AsRef, to: impl AsRef) -> std::io::Result<()> { let temp_file = tempfile_in(to.as_ref().parent().expect("Write path must have a parent"))?; fs_err::copy(from.as_ref(), &temp_file)?; persist_with_retry_sync(temp_file, to.as_ref()) } #[cfg(windows)] fn backoff_file_move() -> backon::ExponentialBackoff { use backon::BackoffBuilder; // This amounts to 10 total seconds of trying the operation. // We start at 10 milliseconds and try 9 times, doubling each time, so the last try will take // about 10*(2^9) milliseconds ~= 5 seconds. All other attempts combined should equal // the length of the last attempt (because it's a sum of powers of 2), so 10 seconds overall. backon::ExponentialBuilder::default() .with_min_delay(std::time::Duration::from_millis(10)) .with_max_times(9) .build() } /// Rename a file, retrying (on Windows) if it fails due to transient operating system errors. #[cfg(feature = "tokio")] pub async fn rename_with_retry( from: impl AsRef, to: impl AsRef, ) -> Result<(), std::io::Error> { #[cfg(windows)] { use backon::Retryable; // On Windows, antivirus software can lock files temporarily, making them inaccessible. // This is most common for DLLs, and the common suggestion is to retry the operation with // some backoff. // // See: & let from = from.as_ref(); let to = to.as_ref(); let rename = async || fs_err::rename(from, to); rename .retry(backoff_file_move()) .sleep(tokio::time::sleep) .when(|e| e.kind() == std::io::ErrorKind::PermissionDenied) .notify(|err, _dur| { warn!( "Retrying rename from {} to {} due to transient error: {}", from.display(), to.display(), err ); }) .await } #[cfg(not(windows))] { fs_err::tokio::rename(from, to).await } } /// Rename or copy a file, retrying (on Windows) if it fails due to transient operating system /// errors, in a synchronous context. #[cfg_attr(not(windows), allow(unused_variables))] pub fn with_retry_sync( from: impl AsRef, to: impl AsRef, operation_name: &str, operation: impl Fn() -> Result<(), std::io::Error>, ) -> Result<(), std::io::Error> { #[cfg(windows)] { use backon::BlockingRetryable; // On Windows, antivirus software can lock files temporarily, making them inaccessible. // This is most common for DLLs, and the common suggestion is to retry the operation with // some backoff. // // See: & let from = from.as_ref(); let to = to.as_ref(); operation .retry(backoff_file_move()) .sleep(std::thread::sleep) .when(|err| err.kind() == std::io::ErrorKind::PermissionDenied) .notify(|err, _dur| { warn!( "Retrying {} from {} to {} due to transient error: {}", operation_name, from.display(), to.display(), err ); }) .call() .map_err(|err| { std::io::Error::other(format!( "Failed {} {} to {}: {}", operation_name, from.display(), to.display(), err )) }) } #[cfg(not(windows))] { operation() } } /// Why a file persist failed #[cfg(windows)] enum PersistRetryError { /// Something went wrong while persisting, maybe retry (contains error message) Persist(String), /// Something went wrong trying to retrieve the file to persist, we must bail LostState, } /// Persist a `NamedTempFile`, retrying (on Windows) if it fails due to transient operating system errors, in a synchronous context. pub async fn persist_with_retry( from: NamedTempFile, to: impl AsRef, ) -> Result<(), std::io::Error> { #[cfg(windows)] { use backon::Retryable; // On Windows, antivirus software can lock files temporarily, making them inaccessible. // This is most common for DLLs, and the common suggestion is to retry the operation with // some backoff. // // See: & let to = to.as_ref(); // Ok there's a lot of complex ownership stuff going on here. // // the `NamedTempFile` `persist` method consumes `self`, and returns it back inside // the Error in case of `PersistError`: // https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist // So every time we fail, we need to reset the `NamedTempFile` to try again. // // Every time we (re)try we call this outer closure (`let persist = ...`), so it needs to // be at least a `FnMut` (as opposed to `Fnonce`). However the closure needs to return a // totally owned `Future` (so effectively it returns a `FnOnce`). // // But if the `Future` is totally owned it *necessarily* can't write back the `NamedTempFile` // to somewhere the outer `FnMut` can see using references. So we need to use `Arc`s // with interior mutability (`Mutex`) to have the closure and all the Futures it creates share // a single memory location that the `NamedTempFile` can be shuttled in and out of. // // In spite of the Mutex all of this code will run logically serially, so there shouldn't be a // chance for a race where we try to get the `NamedTempFile` but it's actually None. The code // is just written pedantically/robustly. let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from))); let persist = || { // Turn our by-ref-captured Arc into an owned Arc that the Future can capture by-value let from2 = from.clone(); async move { let maybe_file: Option = from2 .lock() .map_err(|_| PersistRetryError::LostState)? .take(); if let Some(file) = maybe_file { file.persist(to).map_err(|err| { let error_message: String = err.to_string(); // Set back the `NamedTempFile` returned back by the Error if let Ok(mut guard) = from2.lock() { *guard = Some(err.file); PersistRetryError::Persist(error_message) } else { PersistRetryError::LostState } }) } else { Err(PersistRetryError::LostState) } } }; let persisted = persist .retry(backoff_file_move()) .sleep(tokio::time::sleep) .when(|err| matches!(err, PersistRetryError::Persist(_))) .notify(|err, _dur| { if let PersistRetryError::Persist(error_message) = err { warn!( "Retrying to persist temporary file to {}: {}", to.display(), error_message, ); } }) .await; match persisted { Ok(_) => Ok(()), Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!( "Failed to persist temporary file to {}: {}", to.display(), error_message, ))), Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!( "Failed to retrieve temporary file while trying to persist to {}", to.display() ))), } } #[cfg(not(windows))] { async { fs_err::rename(from, to) }.await } } /// Persist a `NamedTempFile`, retrying (on Windows) if it fails due to transient operating system errors, in a synchronous context. pub fn persist_with_retry_sync( from: NamedTempFile, to: impl AsRef, ) -> Result<(), std::io::Error> { #[cfg(windows)] { use backon::BlockingRetryable; // On Windows, antivirus software can lock files temporarily, making them inaccessible. // This is most common for DLLs, and the common suggestion is to retry the operation with // some backoff. // // See: & let to = to.as_ref(); // the `NamedTempFile` `persist` method consumes `self`, and returns it back inside the Error in case of `PersistError` // https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist // So we will update the `from` optional value in safe and borrow-checker friendly way every retry // Allows us to use the NamedTempFile inside a FnMut closure used for backoff::retry let mut from = Some(from); let persist = || { // Needed because we cannot move out of `from`, a captured variable in an `FnMut` closure, and then pass it to the async move block if let Some(file) = from.take() { file.persist(to).map_err(|err| { let error_message = err.to_string(); // Set back the NamedTempFile returned back by the Error from = Some(err.file); PersistRetryError::Persist(error_message) }) } else { Err(PersistRetryError::LostState) } }; let persisted = persist .retry(backoff_file_move()) .sleep(std::thread::sleep) .when(|err| matches!(err, PersistRetryError::Persist(_))) .notify(|err, _dur| { if let PersistRetryError::Persist(error_message) = err { warn!( "Retrying to persist temporary file to {}: {}", to.display(), error_message, ); } }) .call(); match persisted { Ok(_) => Ok(()), Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!( "Failed to persist temporary file to {}: {}", to.display(), error_message, ))), Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!( "Failed to retrieve temporary file while trying to persist to {}", to.display() ))), } } #[cfg(not(windows))] { fs_err::rename(from, to) } } /// Iterate over the subdirectories of a directory. /// /// If the directory does not exist, returns an empty iterator. pub fn directories( path: impl AsRef, ) -> Result, std::io::Error> { let entries = match path.as_ref().read_dir() { Ok(entries) => Some(entries), Err(err) if err.kind() == std::io::ErrorKind::NotFound => None, Err(err) => return Err(err), }; Ok(entries .into_iter() .flatten() .filter_map(|entry| match entry { Ok(entry) => Some(entry), Err(err) => { warn!("Failed to read entry: {err}"); None } }) .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_dir())) .map(|entry| entry.path())) } /// Iterate over the entries in a directory. /// /// If the directory does not exist, returns an empty iterator. pub fn entries(path: impl AsRef) -> Result, std::io::Error> { let entries = match path.as_ref().read_dir() { Ok(entries) => Some(entries), Err(err) if err.kind() == std::io::ErrorKind::NotFound => None, Err(err) => return Err(err), }; Ok(entries .into_iter() .flatten() .filter_map(|entry| match entry { Ok(entry) => Some(entry), Err(err) => { warn!("Failed to read entry: {err}"); None } }) .map(|entry| entry.path())) } /// Iterate over the files in a directory. /// /// If the directory does not exist, returns an empty iterator. pub fn files(path: impl AsRef) -> Result, std::io::Error> { let entries = match path.as_ref().read_dir() { Ok(entries) => Some(entries), Err(err) if err.kind() == std::io::ErrorKind::NotFound => None, Err(err) => return Err(err), }; Ok(entries .into_iter() .flatten() .filter_map(|entry| match entry { Ok(entry) => Some(entry), Err(err) => { warn!("Failed to read entry: {err}"); None } }) .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_file())) .map(|entry| entry.path())) } /// Returns `true` if a path is a temporary file or directory. pub fn is_temporary(path: impl AsRef) -> bool { path.as_ref() .file_name() .and_then(|name| name.to_str()) .is_some_and(|name| name.starts_with(".tmp")) } /// Checks if the grandparent directory of the given executable is the base /// of a virtual environment. /// /// The procedure described in PEP 405 includes checking both the parent and /// grandparent directory of an executable, but in practice we've found this to /// be unnecessary. pub fn is_virtualenv_executable(executable: impl AsRef) -> bool { executable .as_ref() .parent() .and_then(Path::parent) .is_some_and(is_virtualenv_base) } /// Returns `true` if a path is the base path of a virtual environment, /// indicated by the presence of a `pyvenv.cfg` file. /// /// The procedure described in PEP 405 includes scanning `pyvenv.cfg` /// for a `home` key, but in practice we've found this to be /// unnecessary. pub fn is_virtualenv_base(path: impl AsRef) -> bool { path.as_ref().join("pyvenv.cfg").is_file() } /// Whether the error is due to a lock being held. fn is_known_already_locked_error(err: &std::fs::TryLockError) -> bool { match err { std::fs::TryLockError::WouldBlock => true, std::fs::TryLockError::Error(err) => { // On Windows, we've seen: Os { code: 33, kind: Uncategorized, message: "The process cannot access the file because another process has locked a portion of the file." } if cfg!(windows) && err.raw_os_error() == Some(33) { return true; } false } } } /// An asynchronous reader that reports progress as bytes are read. #[cfg(feature = "tokio")] pub struct ProgressReader { reader: Reader, callback: Callback, } #[cfg(feature = "tokio")] impl ProgressReader { /// Create a new [`ProgressReader`] that wraps another reader. pub fn new(reader: Reader, callback: Callback) -> Self { Self { reader, callback } } } #[cfg(feature = "tokio")] impl tokio::io::AsyncRead for ProgressReader { fn poll_read( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: &mut tokio::io::ReadBuf<'_>, ) -> std::task::Poll> { std::pin::Pin::new(&mut self.as_mut().reader) .poll_read(cx, buf) .map_ok(|()| { (self.callback)(buf.filled().len()); }) } } /// Recursively copy a directory and its contents. pub fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { fs_err::create_dir_all(&dst)?; for entry in fs_err::read_dir(src.as_ref())? { let entry = entry?; let ty = entry.file_type()?; if ty.is_dir() { copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; } else { fs_err::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; } } Ok(()) } #[cfg(test)] mod tests { use super::*; use std::path::PathBuf; #[test] fn test_with_added_extension() { // Test with simple package name (no dots) let path = PathBuf::from("python"); let result = with_added_extension(&path, "exe"); assert_eq!(result, PathBuf::from("python.exe")); // Test with package name containing single dot let path = PathBuf::from("awslabs.cdk-mcp-server"); let result = with_added_extension(&path, "exe"); assert_eq!(result, PathBuf::from("awslabs.cdk-mcp-server.exe")); // Test with package name containing multiple dots let path = PathBuf::from("org.example.tool"); let result = with_added_extension(&path, "exe"); assert_eq!(result, PathBuf::from("org.example.tool.exe")); // Test with different extensions let path = PathBuf::from("script"); let result = with_added_extension(&path, "ps1"); assert_eq!(result, PathBuf::from("script.ps1")); // Test with path that has directory components let path = PathBuf::from("some/path/to/awslabs.cdk-mcp-server"); let result = with_added_extension(&path, "exe"); assert_eq!( result, PathBuf::from("some/path/to/awslabs.cdk-mcp-server.exe") ); // Test with empty path (edge case) let path = PathBuf::new(); let result = with_added_extension(&path, "exe"); assert_eq!(result, path); // Should return unchanged } } uv-0.9.17+ds1/crates/uv-fs/src/locked_file.rs000066400000000000000000000221471520155276700206630ustar00rootroot00000000000000use std::fmt::Display; use std::path::{Path, PathBuf}; use std::sync::LazyLock; use std::time::Duration; use std::{env, io}; use thiserror::Error; use tracing::{debug, error, info, trace, warn}; use uv_static::EnvVars; use crate::{Simplified, is_known_already_locked_error}; /// Parsed value of `UV_LOCK_TIMEOUT`, with a default of 5 min. static LOCK_TIMEOUT: LazyLock = LazyLock::new(|| { let default_timeout = Duration::from_secs(300); let Some(lock_timeout) = env::var_os(EnvVars::UV_LOCK_TIMEOUT) else { return default_timeout; }; if let Some(lock_timeout) = lock_timeout .to_str() .and_then(|lock_timeout| lock_timeout.parse::().ok()) { Duration::from_secs(lock_timeout) } else { warn!( "Could not parse value of {} as integer: {:?}", EnvVars::UV_LOCK_TIMEOUT, lock_timeout ); default_timeout } }); #[derive(Debug, Error)] pub enum LockedFileError { #[error( "Timeout ({}s) when waiting for lock on `{}` at `{}`, is another uv process running? You can set `{}` to increase the timeout.", timeout.as_secs(), resource, path.user_display(), EnvVars::UV_LOCK_TIMEOUT )] Timeout { timeout: Duration, resource: String, path: PathBuf, }, #[error( "Could not acquire lock for `{}` at `{}`", resource, path.user_display() )] Lock { resource: String, path: PathBuf, #[source] source: io::Error, }, #[error(transparent)] Io(#[from] io::Error), #[error(transparent)] #[cfg(feature = "tokio")] JoinError(#[from] tokio::task::JoinError), } impl LockedFileError { pub fn as_io_error(&self) -> Option<&io::Error> { match self { Self::Timeout { .. } => None, #[cfg(feature = "tokio")] Self::JoinError(_) => None, Self::Lock { source, .. } => Some(source), Self::Io(err) => Some(err), } } } /// Whether to acquire a shared (read) lock or exclusive (write) lock. #[derive(Debug, Clone, Copy)] pub enum LockedFileMode { Shared, Exclusive, } impl LockedFileMode { /// Try to lock the file and return an error if the lock is already acquired by another process /// and cannot be acquired immediately. fn try_lock(self, file: &fs_err::File) -> Result<(), std::fs::TryLockError> { match self { Self::Exclusive => file.try_lock()?, Self::Shared => file.try_lock_shared()?, } Ok(()) } /// Lock the file, blocking until the lock becomes available if necessary. fn lock(self, file: &fs_err::File) -> Result<(), io::Error> { match self { Self::Exclusive => file.lock()?, Self::Shared => file.lock_shared()?, } Ok(()) } } impl Display for LockedFileMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Shared => write!(f, "shared"), Self::Exclusive => write!(f, "exclusive"), } } } /// A file lock that is automatically released when dropped. #[cfg(feature = "tokio")] #[derive(Debug)] #[must_use] pub struct LockedFile(fs_err::File); #[cfg(feature = "tokio")] impl LockedFile { /// Inner implementation for [`LockedFile::acquire`]. async fn lock_file( file: fs_err::File, mode: LockedFileMode, resource: &str, ) -> Result { trace!( "Checking lock for `{resource}` at `{}`", file.path().user_display() ); // If there's no contention, return directly. let try_lock_exclusive = tokio::task::spawn_blocking(move || (mode.try_lock(&file), file)); let file = match try_lock_exclusive.await? { (Ok(()), file) => { debug!("Acquired {mode} lock for `{resource}`"); return Ok(Self(file)); } (Err(err), file) => { // Log error code and enum kind to help debugging more exotic failures. if !is_known_already_locked_error(&err) { debug!("Try lock {mode} error: {err:?}"); } file } }; // If there's lock contention, wait and break deadlocks with a timeout if necessary. info!( "Waiting to acquire {mode} lock for `{resource}` at `{}`", file.path().user_display(), ); let path = file.path().to_path_buf(); let lock_exclusive = tokio::task::spawn_blocking(move || (mode.lock(&file), file)); let (result, file) = tokio::time::timeout(*LOCK_TIMEOUT, lock_exclusive) .await .map_err(|_| LockedFileError::Timeout { timeout: *LOCK_TIMEOUT, resource: resource.to_string(), path: path.clone(), })??; // Not an fs_err method, we need to build our own path context result.map_err(|err| LockedFileError::Lock { resource: resource.to_string(), path, source: err, })?; debug!("Acquired {mode} lock for `{resource}`"); Ok(Self(file)) } /// Inner implementation for [`LockedFile::acquire_no_wait`]. fn lock_file_no_wait(file: fs_err::File, mode: LockedFileMode, resource: &str) -> Option { trace!( "Checking lock for `{resource}` at `{}`", file.path().user_display() ); match mode.try_lock(&file) { Ok(()) => { debug!("Acquired {mode} lock for `{resource}`"); Some(Self(file)) } Err(err) => { // Log error code and enum kind to help debugging more exotic failures. if !is_known_already_locked_error(&err) { debug!("Try lock error: {err:?}"); } debug!("Lock is busy for `{resource}`"); None } } } /// Acquire a cross-process lock for a resource using a file at the provided path. pub async fn acquire( path: impl AsRef, mode: LockedFileMode, resource: impl Display, ) -> Result { let file = Self::create(path)?; let resource = resource.to_string(); Self::lock_file(file, mode, &resource).await } /// Acquire a cross-process lock for a resource using a file at the provided path /// /// Unlike [`LockedFile::acquire`] this function will not wait for the lock to become available. /// /// If the lock is not immediately available, [`None`] is returned. pub fn acquire_no_wait( path: impl AsRef, mode: LockedFileMode, resource: impl Display, ) -> Option { let file = Self::create(path).ok()?; let resource = resource.to_string(); Self::lock_file_no_wait(file, mode, &resource) } #[cfg(unix)] fn create(path: impl AsRef) -> Result { use std::os::unix::fs::PermissionsExt; use tempfile::NamedTempFile; // If path already exists, return it. if let Ok(file) = fs_err::OpenOptions::new() .read(true) .write(true) .open(path.as_ref()) { return Ok(file); } // Otherwise, create a temporary file with 666 permissions. We must set // permissions _after_ creating the file, to override the `umask`. let file = if let Some(parent) = path.as_ref().parent() { NamedTempFile::new_in(parent)? } else { NamedTempFile::new()? }; if let Err(err) = file .as_file() .set_permissions(std::fs::Permissions::from_mode(0o666)) { warn!("Failed to set permissions on temporary file: {err}"); } // Try to move the file to path, but if path exists now, just open path match file.persist_noclobber(path.as_ref()) { Ok(file) => Ok(fs_err::File::from_parts(file, path.as_ref())), Err(err) => { if err.error.kind() == std::io::ErrorKind::AlreadyExists { fs_err::OpenOptions::new() .read(true) .write(true) .open(path.as_ref()) } else { Err(err.error) } } } } #[cfg(not(unix))] fn create(path: impl AsRef) -> std::io::Result { fs_err::OpenOptions::new() .read(true) .write(true) .create(true) .open(path.as_ref()) } } #[cfg(feature = "tokio")] impl Drop for LockedFile { /// Unlock the file. fn drop(&mut self) { if let Err(err) = self.0.unlock() { error!( "Failed to unlock resource at `{}`; program may be stuck: {err}", self.0.path().display() ); } else { debug!("Released lock at `{}`", self.0.path().display()); } } } uv-0.9.17+ds1/crates/uv-fs/src/path.rs000066400000000000000000000432731520155276700173620ustar00rootroot00000000000000use std::borrow::Cow; use std::path::{Component, Path, PathBuf}; use std::sync::LazyLock; use either::Either; use path_slash::PathExt; /// The current working directory. #[allow(clippy::exit, clippy::print_stderr)] pub static CWD: LazyLock = LazyLock::new(|| { std::env::current_dir().unwrap_or_else(|_e| { eprintln!("Current directory does not exist"); std::process::exit(1); }) }); pub trait Simplified { /// Simplify a [`Path`]. /// /// On Windows, this will strip the `\\?\` prefix from paths. On other platforms, it's a no-op. fn simplified(&self) -> &Path; /// Render a [`Path`] for display. /// /// On Windows, this will strip the `\\?\` prefix from paths. On other platforms, it's /// equivalent to [`std::path::Display`]. fn simplified_display(&self) -> impl std::fmt::Display; /// Canonicalize a path without a `\\?\` prefix on Windows. /// For a path that can't be canonicalized (e.g. on network drive or RAM drive on Windows), /// this will return the absolute path if it exists. fn simple_canonicalize(&self) -> std::io::Result; /// Render a [`Path`] for user-facing display. /// /// Like [`simplified_display`], but relativizes the path against the current working directory. fn user_display(&self) -> impl std::fmt::Display; /// Render a [`Path`] for user-facing display, where the [`Path`] is relative to a base path. /// /// If the [`Path`] is not relative to the base path, will attempt to relativize the path /// against the current working directory. fn user_display_from(&self, base: impl AsRef) -> impl std::fmt::Display; /// Render a [`Path`] for user-facing display using a portable representation. /// /// Like [`user_display`], but uses a portable representation for relative paths. fn portable_display(&self) -> impl std::fmt::Display; } impl> Simplified for T { fn simplified(&self) -> &Path { dunce::simplified(self.as_ref()) } fn simplified_display(&self) -> impl std::fmt::Display { dunce::simplified(self.as_ref()).display() } fn simple_canonicalize(&self) -> std::io::Result { dunce::canonicalize(self.as_ref()) } fn user_display(&self) -> impl std::fmt::Display { let path = dunce::simplified(self.as_ref()); // If current working directory is root, display the path as-is. if CWD.ancestors().nth(1).is_none() { return path.display(); } // Attempt to strip the current working directory, then the canonicalized current working // directory, in case they differ. let path = path.strip_prefix(CWD.simplified()).unwrap_or(path); if path.as_os_str() == "" { // Avoid printing an empty string for the current directory return Path::new(".").display(); } path.display() } fn user_display_from(&self, base: impl AsRef) -> impl std::fmt::Display { let path = dunce::simplified(self.as_ref()); // If current working directory is root, display the path as-is. if CWD.ancestors().nth(1).is_none() { return path.display(); } // Attempt to strip the base, then the current working directory, then the canonicalized // current working directory, in case they differ. let path = path .strip_prefix(base.as_ref()) .unwrap_or_else(|_| path.strip_prefix(CWD.simplified()).unwrap_or(path)); if path.as_os_str() == "" { // Avoid printing an empty string for the current directory return Path::new(".").display(); } path.display() } fn portable_display(&self) -> impl std::fmt::Display { let path = dunce::simplified(self.as_ref()); // Attempt to strip the current working directory, then the canonicalized current working // directory, in case they differ. let path = path.strip_prefix(CWD.simplified()).unwrap_or(path); // Use a portable representation for relative paths. path.to_slash() .map(Either::Left) .unwrap_or_else(|| Either::Right(path.display())) } } pub trait PythonExt { /// Escape a [`Path`] for use in Python code. fn escape_for_python(&self) -> String; } impl> PythonExt for T { fn escape_for_python(&self) -> String { self.as_ref() .to_string_lossy() .replace('\\', "\\\\") .replace('"', "\\\"") } } /// Normalize the `path` component of a URL for use as a file path. /// /// For example, on Windows, transforms `C:\Users\ferris\wheel-0.42.0.tar.gz` to /// `/C:/Users/ferris/wheel-0.42.0.tar.gz`. /// /// On other platforms, this is a no-op. pub fn normalize_url_path(path: &str) -> Cow<'_, str> { // Apply percent-decoding to the URL. let path = percent_encoding::percent_decode_str(path) .decode_utf8() .unwrap_or(Cow::Borrowed(path)); // Return the path. if cfg!(windows) { Cow::Owned( path.strip_prefix('/') .unwrap_or(&path) .replace('/', std::path::MAIN_SEPARATOR_STR), ) } else { path } } /// Normalize a path, removing things like `.` and `..`. /// /// Source: /// /// CAUTION: Assumes that the path is already absolute. /// /// CAUTION: This does not resolve symlinks (unlike /// [`std::fs::canonicalize`]). This may cause incorrect or surprising /// behavior at times. This should be used carefully. Unfortunately, /// [`std::fs::canonicalize`] can be hard to use correctly, since it can often /// fail, or on Windows returns annoying device paths. /// /// # Errors /// /// When a relative path is provided with `..` components that extend beyond the base directory. /// For example, `./a/../../b` cannot be normalized because it escapes the base directory. pub fn normalize_absolute_path(path: &Path) -> Result { let mut components = path.components().peekable(); let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() { components.next(); PathBuf::from(c.as_os_str()) } else { PathBuf::new() }; for component in components { match component { Component::Prefix(..) => unreachable!(), Component::RootDir => { ret.push(component.as_os_str()); } Component::CurDir => {} Component::ParentDir => { if !ret.pop() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, format!( "cannot normalize a relative path beyond the base directory: {}", path.display() ), )); } } Component::Normal(c) => { ret.push(c); } } } Ok(ret) } /// Normalize a [`Path`], removing things like `.` and `..`. pub fn normalize_path(path: &Path) -> Cow<'_, Path> { // Fast path: if the path is already normalized, return it as-is. if path.components().all(|component| match component { Component::Prefix(_) | Component::RootDir | Component::Normal(_) => true, Component::ParentDir | Component::CurDir => false, }) { Cow::Borrowed(path) } else { Cow::Owned(normalized(path)) } } /// Normalize a [`PathBuf`], removing things like `.` and `..`. pub fn normalize_path_buf(path: PathBuf) -> PathBuf { // Fast path: if the path is already normalized, return it as-is. if path.components().all(|component| match component { Component::Prefix(_) | Component::RootDir | Component::Normal(_) => true, Component::ParentDir | Component::CurDir => false, }) { path } else { normalized(&path) } } /// Normalize a [`Path`]. /// /// Unlike [`normalize_absolute_path`], this works with relative paths and does never error. /// /// Note that we can theoretically go beyond the root dir here (e.g. `/usr/../../foo` becomes /// `/../foo`), but that's not a (correctness) problem, we will fail later with a file not found /// error with a path computed from the user's input. /// /// # Examples /// /// In: `../../workspace-git-path-dep-test/packages/c/../../packages/d` /// Out: `../../workspace-git-path-dep-test/packages/d` /// /// In: `workspace-git-path-dep-test/packages/c/../../packages/d` /// Out: `workspace-git-path-dep-test/packages/d` /// /// In: `./a/../../b` fn normalized(path: &Path) -> PathBuf { let mut normalized = PathBuf::new(); for component in path.components() { match component { Component::Prefix(_) | Component::RootDir | Component::Normal(_) => { // Preserve filesystem roots and regular path components. normalized.push(component); } Component::ParentDir => { match normalized.components().next_back() { None | Some(Component::ParentDir | Component::RootDir) => { // Preserve leading and above-root `..` normalized.push(component); } Some(Component::Normal(_) | Component::Prefix(_) | Component::CurDir) => { // Remove inner `..` normalized.pop(); } } } Component::CurDir => { // Remove `.` } } } normalized } /// Compute a path describing `path` relative to `base`. /// /// `lib/python/site-packages/foo/__init__.py` and `lib/python/site-packages` -> `foo/__init__.py` /// `lib/marker.txt` and `lib/python/site-packages` -> `../../marker.txt` /// `bin/foo_launcher` and `lib/python/site-packages` -> `../../../bin/foo_launcher` /// /// Returns `Err` if there is no relative path between `path` and `base` (for example, if the paths /// are on different drives on Windows). pub fn relative_to( path: impl AsRef, base: impl AsRef, ) -> Result { // Normalize both paths, to avoid intermediate `..` components. let path = normalize_path(path.as_ref()); let base = normalize_path(base.as_ref()); // Find the longest common prefix, and also return the path stripped from that prefix let (stripped, common_prefix) = base .ancestors() .find_map(|ancestor| { // Simplifying removes the UNC path prefix on windows. dunce::simplified(&path) .strip_prefix(dunce::simplified(ancestor)) .ok() .map(|stripped| (stripped, ancestor)) }) .ok_or_else(|| { std::io::Error::other(format!( "Trivial strip failed: {} vs. {}", path.simplified_display(), base.simplified_display() )) })?; // go as many levels up as required let levels_up = base.components().count() - common_prefix.components().count(); let up = std::iter::repeat_n("..", levels_up).collect::(); Ok(up.join(stripped)) } /// A path that can be serialized and deserialized in a portable way by converting Windows-style /// backslashes to forward slashes, and using a `.` for an empty path. /// /// This implementation assumes that the path is valid UTF-8; otherwise, it won't roundtrip. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PortablePath<'a>(&'a Path); #[derive(Debug, Clone, PartialEq, Eq)] pub struct PortablePathBuf(Box); #[cfg(feature = "schemars")] impl schemars::JsonSchema for PortablePathBuf { fn schema_name() -> Cow<'static, str> { Cow::Borrowed("PortablePathBuf") } fn json_schema(_gen: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { PathBuf::json_schema(_gen) } } impl AsRef for PortablePath<'_> { fn as_ref(&self) -> &Path { self.0 } } impl<'a, T> From<&'a T> for PortablePath<'a> where T: AsRef + ?Sized, { fn from(path: &'a T) -> Self { PortablePath(path.as_ref()) } } impl std::fmt::Display for PortablePath<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let path = self.0.to_slash_lossy(); if path.is_empty() { write!(f, ".") } else { write!(f, "{path}") } } } impl std::fmt::Display for PortablePathBuf { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let path = self.0.to_slash_lossy(); if path.is_empty() { write!(f, ".") } else { write!(f, "{path}") } } } impl From<&str> for PortablePathBuf { fn from(path: &str) -> Self { if path == "." { Self(PathBuf::new().into_boxed_path()) } else { Self(PathBuf::from(path).into_boxed_path()) } } } impl From for Box { fn from(portable: PortablePathBuf) -> Self { portable.0 } } impl From> for PortablePathBuf { fn from(path: Box) -> Self { Self(path) } } impl<'a> From<&'a Path> for PortablePathBuf { fn from(path: &'a Path) -> Self { Box::::from(path).into() } } #[cfg(feature = "serde")] impl serde::Serialize for PortablePathBuf { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { self.to_string().serialize(serializer) } } #[cfg(feature = "serde")] impl serde::Serialize for PortablePath<'_> { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { self.to_string().serialize(serializer) } } #[cfg(feature = "serde")] impl<'de> serde::de::Deserialize<'de> for PortablePathBuf { fn deserialize(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { let s = String::deserialize(deserializer)?; if s == "." { Ok(Self(PathBuf::new().into_boxed_path())) } else { Ok(Self(PathBuf::from(s).into_boxed_path())) } } } impl AsRef for PortablePathBuf { fn as_ref(&self) -> &Path { &self.0 } } #[cfg(test)] mod tests { use super::*; #[test] fn test_normalize_url() { if cfg!(windows) { assert_eq!( normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"), "C:\\Users\\ferris\\wheel-0.42.0.tar.gz" ); } else { assert_eq!( normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"), "/C:/Users/ferris/wheel-0.42.0.tar.gz" ); } if cfg!(windows) { assert_eq!( normalize_url_path("./ferris/wheel-0.42.0.tar.gz"), ".\\ferris\\wheel-0.42.0.tar.gz" ); } else { assert_eq!( normalize_url_path("./ferris/wheel-0.42.0.tar.gz"), "./ferris/wheel-0.42.0.tar.gz" ); } if cfg!(windows) { assert_eq!( normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"), ".\\wheel cache\\wheel-0.42.0.tar.gz" ); } else { assert_eq!( normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"), "./wheel cache/wheel-0.42.0.tar.gz" ); } } #[test] fn test_normalize_path() { let path = Path::new("/a/b/../c/./d"); let normalized = normalize_absolute_path(path).unwrap(); assert_eq!(normalized, Path::new("/a/c/d")); let path = Path::new("/a/../c/./d"); let normalized = normalize_absolute_path(path).unwrap(); assert_eq!(normalized, Path::new("/c/d")); // This should be an error. let path = Path::new("/a/../../c/./d"); let err = normalize_absolute_path(path).unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); } #[test] fn test_relative_to() { assert_eq!( relative_to( Path::new("/home/ferris/carcinization/lib/python/site-packages/foo/__init__.py"), Path::new("/home/ferris/carcinization/lib/python/site-packages"), ) .unwrap(), Path::new("foo/__init__.py") ); assert_eq!( relative_to( Path::new("/home/ferris/carcinization/lib/marker.txt"), Path::new("/home/ferris/carcinization/lib/python/site-packages"), ) .unwrap(), Path::new("../../marker.txt") ); assert_eq!( relative_to( Path::new("/home/ferris/carcinization/bin/foo_launcher"), Path::new("/home/ferris/carcinization/lib/python/site-packages"), ) .unwrap(), Path::new("../../../bin/foo_launcher") ); } #[test] fn test_normalize_relative() { let cases = [ ( "../../workspace-git-path-dep-test/packages/c/../../packages/d", "../../workspace-git-path-dep-test/packages/d", ), ( "workspace-git-path-dep-test/packages/c/../../packages/d", "workspace-git-path-dep-test/packages/d", ), ("./a/../../b", "../b"), ("/usr/../../foo", "/../foo"), ]; for (input, expected) in cases { assert_eq!(normalize_path(Path::new(input)), Path::new(expected)); } } } uv-0.9.17+ds1/crates/uv-fs/src/which.rs000066400000000000000000000035621520155276700175250ustar00rootroot00000000000000use std::path::Path; #[cfg(windows)] #[allow(unsafe_code)] // We need to do an FFI call through the windows-* crates. fn get_binary_type(path: &Path) -> windows::core::Result { use std::os::windows::ffi::OsStrExt; use windows::Win32::Storage::FileSystem::GetBinaryTypeW; use windows::core::PCWSTR; // References: // https://github.com/denoland/deno/blob/01a6379505712be34ebf2cdc874fa7f54a6e9408/runtime/permissions/which.rs#L131-L154 // https://github.com/conradkleinespel/rooster/blob/afa78dc9918535752c4af59d2f812197ad754e5a/src/quale.rs#L51-L77 let mut binary_type = 0u32; let name = path .as_os_str() .encode_wide() .chain(Some(0)) .collect::>(); // SAFETY: winapi call unsafe { GetBinaryTypeW(PCWSTR(name.as_ptr()), &raw mut binary_type)? }; Ok(binary_type) } /// Check whether a path in PATH is a valid executable. /// /// Derived from `which`'s `Checker`. pub fn is_executable(path: &Path) -> bool { #[cfg(any(unix, target_os = "wasi", target_os = "redox"))] { if rustix::fs::access(path, rustix::fs::Access::EXEC_OK).is_err() { return false; } } #[cfg(target_os = "windows")] { let Ok(file_type) = fs_err::symlink_metadata(path).map(|metadata| metadata.file_type()) else { return false; }; if !file_type.is_file() && !file_type.is_symlink() { return false; } if path.extension().is_none() && get_binary_type(path).is_err() { return false; } } #[cfg(not(target_os = "windows"))] { use std::os::unix::fs::PermissionsExt; if !fs_err::metadata(path) .map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) .unwrap_or(false) { return false; } } true } uv-0.9.17+ds1/crates/uv-git-types/000077500000000000000000000000001520155276700165755ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-git-types/Cargo.toml000066400000000000000000000010551520155276700205260ustar00rootroot00000000000000[package] name = "uv-git-types" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-redacted = { workspace = true } uv-static = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } url = { workspace = true } uv-0.9.17+ds1/crates/uv-git-types/README.md000066400000000000000000000010331520155276700200510ustar00rootroot00000000000000 # uv-git-types This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-git-types). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-git-types/src/000077500000000000000000000000001520155276700173645ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-git-types/src/github.rs000066400000000000000000000054641520155276700212250ustar00rootroot00000000000000use tracing::debug; use url::Url; /// A reference to a repository on GitHub. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct GitHubRepository<'a> { /// The `owner` field for the repository, i.e., the user or organization that owns the /// repository, like `astral-sh`. pub owner: &'a str, /// The `repo` field for the repository, i.e., the name of the repository, like `uv`. pub repo: &'a str, } impl<'a> GitHubRepository<'a> { /// Parse a GitHub repository from a URL. /// /// Expects to receive a URL of the form: `https://github.com/{user}/{repo}[.git]`, e.g., /// `https://github.com/astral-sh/uv`. Otherwise, returns `None`. pub fn parse(url: &'a Url) -> Option { // The fast path is only available for GitHub repositories. if url.host_str() != Some("github.com") { return None; } // The GitHub URL must take the form: `https://github.com/{user}/{repo}`, e.g., // `https://github.com/astral-sh/uv`. let Some(mut segments) = url.path_segments() else { debug!("GitHub URL is missing path segments: {url}"); return None; }; let Some(owner) = segments.next() else { debug!("GitHub URL is missing owner: {url}"); return None; }; let Some(repo) = segments.next() else { debug!("GitHub URL is missing repo: {url}"); return None; }; if segments.next().is_some() { debug!("GitHub URL has too many path segments: {url}"); return None; } // Trim off the `.git` from the repository, if present. let repo = repo.strip_suffix(".git").unwrap_or(repo); Some(Self { owner, repo }) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_valid_url() { let url = Url::parse("https://github.com/astral-sh/uv").unwrap(); let repo = GitHubRepository::parse(&url).unwrap(); assert_eq!(repo.owner, "astral-sh"); assert_eq!(repo.repo, "uv"); } #[test] fn test_parse_with_git_suffix() { let url = Url::parse("https://github.com/astral-sh/uv.git").unwrap(); let repo = GitHubRepository::parse(&url).unwrap(); assert_eq!(repo.owner, "astral-sh"); assert_eq!(repo.repo, "uv"); } #[test] fn test_parse_invalid_host() { let url = Url::parse("https://gitlab.com/astral-sh/uv").unwrap(); assert!(GitHubRepository::parse(&url).is_none()); } #[test] fn test_parse_invalid_path() { let url = Url::parse("https://github.com/astral-sh").unwrap(); assert!(GitHubRepository::parse(&url).is_none()); let url = Url::parse("https://github.com/astral-sh/uv/extra").unwrap(); assert!(GitHubRepository::parse(&url).is_none()); } } uv-0.9.17+ds1/crates/uv-git-types/src/lib.rs000066400000000000000000000156151520155276700205100ustar00rootroot00000000000000pub use crate::github::GitHubRepository; pub use crate::oid::{GitOid, OidParseError}; pub use crate::reference::GitReference; use std::sync::LazyLock; use thiserror::Error; use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; mod github; mod oid; mod reference; /// Initialize [`GitLfs`] mode from `UV_GIT_LFS` environment. pub static UV_GIT_LFS: LazyLock = LazyLock::new(|| { // TODO(konsti): Parse this in `EnvironmentOptions`. if std::env::var_os(EnvVars::UV_GIT_LFS) .and_then(|v| v.to_str().map(str::to_lowercase)) .is_some_and(|v| matches!(v.as_str(), "y" | "yes" | "t" | "true" | "on" | "1")) { GitLfs::Enabled } else { GitLfs::Disabled } }); /// Configuration for Git LFS (Large File Storage) support. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] pub enum GitLfs { /// Git LFS is disabled (default). #[default] Disabled, /// Git LFS is enabled. Enabled, } impl GitLfs { /// Create a `GitLfs` configuration from environment variables. pub fn from_env() -> Self { *UV_GIT_LFS } /// Returns true if LFS is enabled. pub fn enabled(self) -> bool { matches!(self, Self::Enabled) } } impl From> for GitLfs { fn from(value: Option) -> Self { match value { Some(true) => Self::Enabled, Some(false) => Self::Disabled, None => Self::from_env(), } } } impl From for GitLfs { fn from(value: bool) -> Self { if value { Self::Enabled } else { Self::Disabled } } } impl std::fmt::Display for GitLfs { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Enabled => write!(f, "enabled"), Self::Disabled => write!(f, "disabled"), } } } #[derive(Debug, Error)] pub enum GitUrlParseError { #[error( "Unsupported Git URL scheme `{0}:` in `{1}` (expected one of `https:`, `ssh:`, or `file:`)" )] UnsupportedGitScheme(String, DisplaySafeUrl), } /// A URL reference to a Git repository. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Hash, Ord)] pub struct GitUrl { /// The URL of the Git repository, with any query parameters, fragments, and leading `git+` /// removed. repository: DisplaySafeUrl, /// The reference to the commit to use, which could be a branch, tag or revision. reference: GitReference, /// The precise commit to use, if known. precise: Option, /// Git LFS configuration for this repository. lfs: GitLfs, } impl GitUrl { /// Create a new [`GitUrl`] from a repository URL and a reference. pub fn from_reference( repository: DisplaySafeUrl, reference: GitReference, lfs: GitLfs, ) -> Result { Self::from_fields(repository, reference, None, lfs) } /// Create a new [`GitUrl`] from a repository URL and a precise commit. pub fn from_commit( repository: DisplaySafeUrl, reference: GitReference, precise: GitOid, lfs: GitLfs, ) -> Result { Self::from_fields(repository, reference, Some(precise), lfs) } /// Create a new [`GitUrl`] from a repository URL and a precise commit, if known. pub fn from_fields( repository: DisplaySafeUrl, reference: GitReference, precise: Option, lfs: GitLfs, ) -> Result { match repository.scheme() { "http" | "https" | "ssh" | "file" => {} unsupported => { return Err(GitUrlParseError::UnsupportedGitScheme( unsupported.to_string(), repository, )); } } Ok(Self { repository, reference, precise, lfs, }) } /// Set the precise [`GitOid`] to use for this Git URL. #[must_use] pub fn with_precise(mut self, precise: GitOid) -> Self { self.precise = Some(precise); self } /// Set the [`GitReference`] to use for this Git URL. #[must_use] pub fn with_reference(mut self, reference: GitReference) -> Self { self.reference = reference; self } /// Return the [`Url`] of the Git repository. pub fn repository(&self) -> &DisplaySafeUrl { &self.repository } /// Return the reference to the commit to use, which could be a branch, tag or revision. pub fn reference(&self) -> &GitReference { &self.reference } /// Return the precise commit, if known. pub fn precise(&self) -> Option { self.precise } /// Return the Git LFS configuration. pub fn lfs(&self) -> GitLfs { self.lfs } /// Set the Git LFS configuration. #[must_use] pub fn with_lfs(mut self, lfs: GitLfs) -> Self { self.lfs = lfs; self } } impl TryFrom for GitUrl { type Error = GitUrlParseError; /// Initialize a [`GitUrl`] source from a URL. fn try_from(mut url: DisplaySafeUrl) -> Result { // Remove any query parameters and fragments. url.set_fragment(None); url.set_query(None); // If the URL ends with a reference, like `https://git.example.com/MyProject.git@v1.0`, // extract it. let mut reference = GitReference::DefaultBranch; if let Some((prefix, suffix)) = url .path() .rsplit_once('@') .map(|(prefix, suffix)| (prefix.to_string(), suffix.to_string())) { reference = GitReference::from_rev(suffix); url.set_path(&prefix); } // TODO(samypr100): GitLfs::from_env() for now unless we want to support parsing lfs=true Self::from_reference(url, reference, GitLfs::from_env()) } } impl From for DisplaySafeUrl { fn from(git: GitUrl) -> Self { let mut url = git.repository; // If we have a precise commit, add `@` and the commit hash to the URL. if let Some(precise) = git.precise { let path = format!("{}@{}", url.path(), precise); url.set_path(&path); } else { // Otherwise, add the branch or tag name. match git.reference { GitReference::Branch(rev) | GitReference::Tag(rev) | GitReference::BranchOrTag(rev) | GitReference::NamedRef(rev) | GitReference::BranchOrTagOrCommit(rev) => { let path = format!("{}@{}", url.path(), rev); url.set_path(&path); } GitReference::DefaultBranch => {} } } url } } impl std::fmt::Display for GitUrl { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self.repository) } } uv-0.9.17+ds1/crates/uv-git-types/src/oid.rs000066400000000000000000000067531520155276700205200ustar00rootroot00000000000000use std::fmt::{Display, Formatter}; use std::str::{self, FromStr}; use thiserror::Error; /// Unique identity of any Git object (commit, tree, blob, tag). /// /// This type's `FromStr` implementation validates that it's exactly 40 hex characters, i.e. a /// full-length git commit. /// /// If Git's SHA-256 support becomes more widespread in the future (in particular if GitHub ever /// adds support), we might need to make this an enum. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct GitOid { bytes: [u8; 40], } impl GitOid { /// Return the string representation of an object ID. pub fn as_str(&self) -> &str { str::from_utf8(&self.bytes).unwrap() } /// Return a truncated representation, i.e., the first 16 characters of the SHA. pub fn as_short_str(&self) -> &str { &self.as_str()[..16] } /// Return a (very) truncated representation, i.e., the first 8 characters of the SHA. pub fn as_tiny_str(&self) -> &str { &self.as_str()[..8] } } #[derive(Debug, Error, PartialEq)] pub enum OidParseError { #[error("Object ID cannot be parsed from empty string")] Empty, #[error("Object ID must be exactly 40 hex characters")] WrongLength, #[error("Object ID must be valid hex characters")] NotHex, } impl FromStr for GitOid { type Err = OidParseError; fn from_str(s: &str) -> Result { if s.is_empty() { return Err(OidParseError::Empty); } if s.len() != 40 { return Err(OidParseError::WrongLength); } if !s.chars().all(|ch| ch.is_ascii_hexdigit()) { return Err(OidParseError::NotHex); } let mut bytes = [0; 40]; bytes.copy_from_slice(s.as_bytes()); Ok(Self { bytes }) } } impl Display for GitOid { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) } } impl serde::Serialize for GitOid { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { self.as_str().serialize(serializer) } } impl<'de> serde::Deserialize<'de> for GitOid { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct Visitor; impl serde::de::Visitor<'_> for Visitor { type Value = GitOid; fn expecting(&self, f: &mut Formatter) -> std::fmt::Result { f.write_str("a string") } fn visit_str(self, v: &str) -> Result { GitOid::from_str(v).map_err(serde::de::Error::custom) } } deserializer.deserialize_str(Visitor) } } #[cfg(test)] mod tests { use std::str::FromStr; use super::{GitOid, OidParseError}; #[test] fn git_oid() { GitOid::from_str("4a23745badf5bf5ef7928f1e346e9986bd696d82").unwrap(); GitOid::from_str("4A23745BADF5BF5EF7928F1E346E9986BD696D82").unwrap(); assert_eq!(GitOid::from_str(""), Err(OidParseError::Empty)); assert_eq!( GitOid::from_str(&str::repeat("a", 41)), Err(OidParseError::WrongLength) ); assert_eq!( GitOid::from_str(&str::repeat("a", 39)), Err(OidParseError::WrongLength) ); assert_eq!( GitOid::from_str(&str::repeat("x", 40)), Err(OidParseError::NotHex) ); } } uv-0.9.17+ds1/crates/uv-git-types/src/reference.rs000066400000000000000000000051041520155276700216700ustar00rootroot00000000000000use std::fmt::Display; use std::str; /// A reference to commit or commit-ish. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum GitReference { /// A specific branch. Branch(String), /// A specific tag. Tag(String), /// From a reference that's ambiguously a branch or tag. BranchOrTag(String), /// From a reference that's ambiguously a commit, branch, or tag. BranchOrTagOrCommit(String), /// From a named reference, like `refs/pull/493/head`. NamedRef(String), /// The default branch of the repository, the reference named `HEAD`. DefaultBranch, } impl GitReference { /// Creates a [`GitReference`] from an arbitrary revision string, which could represent a /// branch, tag, commit, or named ref. pub fn from_rev(rev: String) -> Self { if rev.starts_with("refs/") { Self::NamedRef(rev) } else if looks_like_commit_hash(&rev) { Self::BranchOrTagOrCommit(rev) } else { Self::BranchOrTag(rev) } } /// Converts the [`GitReference`] to a `str`. pub fn as_str(&self) -> Option<&str> { match self { Self::Tag(rev) => Some(rev), Self::Branch(rev) => Some(rev), Self::BranchOrTag(rev) => Some(rev), Self::BranchOrTagOrCommit(rev) => Some(rev), Self::NamedRef(rev) => Some(rev), Self::DefaultBranch => None, } } /// Converts the [`GitReference`] to a `str` that can be used as a revision. pub fn as_rev(&self) -> &str { match self { Self::Tag(rev) => rev, Self::Branch(rev) => rev, Self::BranchOrTag(rev) => rev, Self::BranchOrTagOrCommit(rev) => rev, Self::NamedRef(rev) => rev, Self::DefaultBranch => "HEAD", } } /// Returns the kind of this reference. pub fn kind_str(&self) -> &str { match self { Self::Branch(_) => "branch", Self::Tag(_) => "tag", Self::BranchOrTag(_) => "branch or tag", Self::BranchOrTagOrCommit(_) => "branch, tag, or commit", Self::NamedRef(_) => "ref", Self::DefaultBranch => "default branch", } } } impl Display for GitReference { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str().unwrap_or("HEAD")) } } /// Whether a `rev` looks like a commit hash (ASCII hex digits). fn looks_like_commit_hash(rev: &str) -> bool { rev.len() >= 7 && rev.chars().all(|ch| ch.is_ascii_hexdigit()) } uv-0.9.17+ds1/crates/uv-git/000077500000000000000000000000001520155276700154335ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-git/Cargo.toml000066400000000000000000000020721520155276700173640ustar00rootroot00000000000000[package] name = "uv-git" version = "0.0.7" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lib] doctest = false [lints] workspace = true [dependencies] uv-auth = { workspace = true } uv-cache-key = { workspace = true } uv-fs = { workspace = true, features = ["tokio"] } uv-git-types = { workspace = true } uv-redacted = { workspace = true } uv-static = { workspace = true } uv-version = { workspace = true } uv-warnings = { workspace = true } anyhow = { workspace = true } cargo-util = { workspace = true } dashmap = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } owo-colors = { workspace = true } reqwest = { workspace = true, features = ["blocking"] } reqwest-middleware = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } url = { workspace = true } which = { workspace = true } uv-0.9.17+ds1/crates/uv-git/README.md000066400000000000000000000010171520155276700167110ustar00rootroot00000000000000 # uv-git This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-git). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-git/src/000077500000000000000000000000001520155276700162225ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-git/src/credentials.rs000066400000000000000000000025701520155276700210710ustar00rootroot00000000000000use std::collections::HashMap; use std::sync::{Arc, LazyLock, RwLock}; use tracing::trace; use uv_auth::Credentials; use uv_cache_key::RepositoryUrl; use uv_redacted::DisplaySafeUrl; /// Global authentication cache for a uv invocation. /// /// This is used to share Git credentials within a single process. pub static GIT_STORE: LazyLock = LazyLock::new(GitStore::default); /// A store for Git credentials. #[derive(Debug, Default)] pub struct GitStore(RwLock>>); impl GitStore { /// Insert [`Credentials`] for the given URL into the store. pub fn insert(&self, url: RepositoryUrl, credentials: Credentials) -> Option> { self.0.write().unwrap().insert(url, Arc::new(credentials)) } /// Get the [`Credentials`] for the given URL, if they exist. pub fn get(&self, url: &RepositoryUrl) -> Option> { self.0.read().unwrap().get(url).cloned() } } /// Populate the global authentication store with credentials on a Git URL, if there are any. /// /// Returns `true` if the store was updated. pub fn store_credentials_from_url(url: &DisplaySafeUrl) -> bool { if let Some(credentials) = Credentials::from_url(url) { trace!("Caching credentials for {url}"); GIT_STORE.insert(RepositoryUrl::new(url), credentials); true } else { false } } uv-0.9.17+ds1/crates/uv-git/src/git.rs000066400000000000000000000747131520155276700173670ustar00rootroot00000000000000//! Git support is derived from Cargo's implementation. //! Cargo is dual-licensed under either Apache 2.0 or MIT, at the user's choice. //! Source: use std::fmt::Display; use std::path::{Path, PathBuf}; use std::str::{self}; use std::sync::LazyLock; use anyhow::{Context, Result, anyhow}; use cargo_util::{ProcessBuilder, paths}; use owo_colors::OwoColorize; use tracing::{debug, instrument, warn}; use url::Url; use uv_fs::Simplified; use uv_git_types::{GitOid, GitReference}; use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; use uv_warnings::warn_user_once; /// A file indicates that if present, `git reset` has been done and a repo /// checkout is ready to go. See [`GitCheckout::reset`] for why we need this. const CHECKOUT_READY_LOCK: &str = ".ok"; #[derive(Debug, thiserror::Error)] pub enum GitError { #[error("Git executable not found. Ensure that Git is installed and available.")] GitNotFound, #[error("Git LFS extension not found. Ensure that Git LFS is installed and available.")] GitLfsNotFound, #[error("Is Git LFS configured? Run `{}` to initialize Git LFS.", "git lfs install".green())] GitLfsNotConfigured, #[error(transparent)] Other(#[from] which::Error), #[error( "Remote Git fetches are not allowed because network connectivity is disabled (i.e., with `--offline`)" )] TransportNotAllowed, } /// A global cache of the result of `which git`. pub static GIT: LazyLock> = LazyLock::new(|| { which::which("git").map_err(|err| match err { which::Error::CannotFindBinaryPath => GitError::GitNotFound, err => GitError::Other(err), }) }); /// Strategy when fetching refspecs for a [`GitReference`] enum RefspecStrategy { /// All refspecs should be fetched, if any fail then the fetch will fail. All, /// Stop after the first successful fetch, if none succeed then the fetch will fail. First, } /// A Git reference (like a tag or branch) or a specific commit. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] enum ReferenceOrOid<'reference> { /// A Git reference, like a tag or branch. Reference(&'reference GitReference), /// A specific commit. Oid(GitOid), } impl ReferenceOrOid<'_> { /// Resolves the [`ReferenceOrOid`] to an object ID with objects the `repo` currently has. fn resolve(&self, repo: &GitRepository) -> Result { let refkind = self.kind_str(); let result = match self { // Resolve the commit pointed to by the tag. // // `^0` recursively peels away from the revision to the underlying commit object. // This also verifies that the tag indeed refers to a commit. Self::Reference(GitReference::Tag(s)) => { repo.rev_parse(&format!("refs/remotes/origin/tags/{s}^0")) } // Resolve the commit pointed to by the branch. Self::Reference(GitReference::Branch(s)) => repo.rev_parse(&format!("origin/{s}^0")), // Attempt to resolve the branch, then the tag. Self::Reference(GitReference::BranchOrTag(s)) => repo .rev_parse(&format!("origin/{s}^0")) .or_else(|_| repo.rev_parse(&format!("refs/remotes/origin/tags/{s}^0"))), // Attempt to resolve the branch, then the tag, then the commit. Self::Reference(GitReference::BranchOrTagOrCommit(s)) => repo .rev_parse(&format!("origin/{s}^0")) .or_else(|_| repo.rev_parse(&format!("refs/remotes/origin/tags/{s}^0"))) .or_else(|_| repo.rev_parse(&format!("{s}^0"))), // We'll be using the HEAD commit. Self::Reference(GitReference::DefaultBranch) => { repo.rev_parse("refs/remotes/origin/HEAD") } // Resolve a named reference. Self::Reference(GitReference::NamedRef(s)) => repo.rev_parse(&format!("{s}^0")), // Resolve a specific commit. Self::Oid(s) => repo.rev_parse(&format!("{s}^0")), }; result.with_context(|| anyhow::format_err!("failed to find {refkind} `{self}`")) } /// Returns the kind of this [`ReferenceOrOid`]. fn kind_str(&self) -> &str { match self { Self::Reference(reference) => reference.kind_str(), Self::Oid(_) => "commit", } } /// Converts the [`ReferenceOrOid`] to a `str` that can be used as a revision. fn as_rev(&self) -> &str { match self { Self::Reference(r) => r.as_rev(), Self::Oid(rev) => rev.as_str(), } } } impl Display for ReferenceOrOid<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Reference(reference) => write!(f, "{reference}"), Self::Oid(oid) => write!(f, "{oid}"), } } } /// A remote repository. It gets cloned into a local [`GitDatabase`]. #[derive(PartialEq, Clone, Debug)] pub(crate) struct GitRemote { /// URL to a remote repository. url: DisplaySafeUrl, } /// A local clone of a remote repository's database. Multiple [`GitCheckout`]s /// can be cloned from a single [`GitDatabase`]. pub(crate) struct GitDatabase { /// Underlying Git repository instance for this database. repo: GitRepository, /// Git LFS artifacts have been initialized (if requested). lfs_ready: Option, } /// A local checkout of a particular revision from a [`GitRepository`]. pub(crate) struct GitCheckout { /// The git revision this checkout is for. revision: GitOid, /// Underlying Git repository instance for this checkout. repo: GitRepository, /// Git LFS artifacts have been initialized (if requested). lfs_ready: Option, } /// A local Git repository. pub(crate) struct GitRepository { /// Path to the underlying Git repository on the local filesystem. path: PathBuf, } impl GitRepository { /// Opens an existing Git repository at `path`. pub(crate) fn open(path: &Path) -> Result { // Make sure there is a Git repository at the specified path. ProcessBuilder::new(GIT.as_ref()?) .arg("rev-parse") .cwd(path) .exec_with_output()?; Ok(Self { path: path.to_path_buf(), }) } /// Initializes a Git repository at `path`. fn init(path: &Path) -> Result { // TODO(ibraheem): see if this still necessary now that we no longer use libgit2 // Skip anything related to templates, they just call all sorts of issues as // we really don't want to use them yet they insist on being used. See #6240 // for an example issue that comes up. // opts.external_template(false); // Initialize the repository. ProcessBuilder::new(GIT.as_ref()?) .arg("init") .cwd(path) .exec_with_output()?; Ok(Self { path: path.to_path_buf(), }) } /// Parses the object ID of the given `refname`. fn rev_parse(&self, refname: &str) -> Result { let result = ProcessBuilder::new(GIT.as_ref()?) .arg("rev-parse") .arg(refname) .cwd(&self.path) .exec_with_output()?; let mut result = String::from_utf8(result.stdout)?; result.truncate(result.trim_end().len()); Ok(result.parse()?) } /// Verifies LFS artifacts have been initialized for a given `refname`. #[instrument(skip_all, fields(path = %self.path.user_display(), refname = %refname))] fn lfs_fsck_objects(&self, refname: &str) -> bool { let mut cmd = if let Ok(lfs) = GIT_LFS.as_ref() { lfs.clone() } else { warn!("Git LFS is not available, skipping LFS fetch"); return false; }; // Requires Git LFS 3.x (2021 release) let result = cmd .arg("fsck") .arg("--objects") .arg(refname) .cwd(&self.path) .exec_with_output(); match result { Ok(_) => true, Err(err) => { let lfs_error = err.to_string(); if lfs_error.contains("unknown flag: --objects") { warn_user_once!( "Skipping Git LFS validation as Git LFS extension is outdated. \ Upgrade to `git-lfs>=3.0.2` or manually verify git-lfs objects were \ properly fetched after the current operation finishes." ); true } else { debug!("Git LFS validation failed: {err}"); false } } } } } impl GitRemote { /// Creates an instance for a remote repository URL. pub(crate) fn new(url: &DisplaySafeUrl) -> Self { Self { url: url.clone() } } /// Gets the remote repository URL. pub(crate) fn url(&self) -> &DisplaySafeUrl { &self.url } /// Fetches and checkouts to a reference or a revision from this remote /// into a local path. /// /// This ensures that it gets the up-to-date commit when a named reference /// is given (tag, branch, refs/*). Thus, network connection is involved. /// /// When `locked_rev` is provided, it takes precedence over `reference`. /// /// If we have a previous instance of [`GitDatabase`] then fetch into that /// if we can. If that can successfully load our revision then we've /// populated the database with the latest version of `reference`, so /// return that database and the rev we resolve to. pub(crate) fn checkout( &self, into: &Path, db: Option, reference: &GitReference, locked_rev: Option, disable_ssl: bool, offline: bool, with_lfs: bool, ) -> Result<(GitDatabase, GitOid)> { let reference = locked_rev .map(ReferenceOrOid::Oid) .unwrap_or(ReferenceOrOid::Reference(reference)); if let Some(mut db) = db { fetch(&mut db.repo, &self.url, reference, disable_ssl, offline) .with_context(|| format!("failed to fetch into: {}", into.user_display()))?; let resolved_commit_hash = match locked_rev { Some(rev) => db.contains(rev).then_some(rev), None => reference.resolve(&db.repo).ok(), }; if let Some(rev) = resolved_commit_hash { if with_lfs { let lfs_ready = fetch_lfs(&mut db.repo, &self.url, &rev, disable_ssl) .with_context(|| format!("failed to fetch LFS objects at {rev}"))?; db = db.with_lfs_ready(Some(lfs_ready)); } return Ok((db, rev)); } } // Otherwise start from scratch to handle corrupt git repositories. // After our fetch (which is interpreted as a clone now) we do the same // resolution to figure out what we cloned. match fs_err::remove_dir_all(into) { Ok(()) => {} Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} Err(e) => return Err(e.into()), } fs_err::create_dir_all(into)?; let mut repo = GitRepository::init(into)?; fetch(&mut repo, &self.url, reference, disable_ssl, offline) .with_context(|| format!("failed to clone into: {}", into.user_display()))?; let rev = match locked_rev { Some(rev) => rev, None => reference.resolve(&repo)?, }; let lfs_ready = with_lfs .then(|| { fetch_lfs(&mut repo, &self.url, &rev, disable_ssl) .with_context(|| format!("failed to fetch LFS objects at {rev}")) }) .transpose()?; Ok((GitDatabase { repo, lfs_ready }, rev)) } /// Creates a [`GitDatabase`] of this remote at `db_path`. #[allow(clippy::unused_self)] pub(crate) fn db_at(&self, db_path: &Path) -> Result { let repo = GitRepository::open(db_path)?; Ok(GitDatabase { repo, lfs_ready: None, }) } } impl GitDatabase { /// Checkouts to a revision at `destination` from this database. pub(crate) fn copy_to(&self, rev: GitOid, destination: &Path) -> Result { // If the existing checkout exists, and it is fresh, use it. // A non-fresh checkout can happen if the checkout operation was // interrupted. In that case, the checkout gets deleted and a new // clone is created. let checkout = match GitRepository::open(destination) .ok() .map(|repo| GitCheckout::new(rev, repo)) .filter(GitCheckout::is_fresh) { Some(co) => co.with_lfs_ready(self.lfs_ready), None => GitCheckout::clone_into(destination, self, rev)?, }; Ok(checkout) } /// Get a short OID for a `revision`, usually 7 chars or more if ambiguous. pub(crate) fn to_short_id(&self, revision: GitOid) -> Result { let output = ProcessBuilder::new(GIT.as_ref()?) .arg("rev-parse") .arg("--short") .arg(revision.as_str()) .cwd(&self.repo.path) .exec_with_output()?; let mut result = String::from_utf8(output.stdout)?; result.truncate(result.trim_end().len()); Ok(result) } /// Checks if `oid` resolves to a commit in this database. pub(crate) fn contains(&self, oid: GitOid) -> bool { self.repo.rev_parse(&format!("{oid}^0")).is_ok() } /// Checks if `oid` contains necessary LFS artifacts in this database. pub(crate) fn contains_lfs_artifacts(&self, oid: GitOid) -> bool { self.repo.lfs_fsck_objects(&format!("{oid}^0")) } /// Set the Git LFS validation state (if any). #[must_use] pub(crate) fn with_lfs_ready(mut self, lfs: Option) -> Self { self.lfs_ready = lfs; self } } impl GitCheckout { /// Creates an instance of [`GitCheckout`]. This doesn't imply the checkout /// is done. Use [`GitCheckout::is_fresh`] to check. /// /// * The `repo` will be the checked out Git repository. fn new(revision: GitOid, repo: GitRepository) -> Self { Self { revision, repo, lfs_ready: None, } } /// Clone a repo for a `revision` into a local path from a `database`. /// This is a filesystem-to-filesystem clone. fn clone_into(into: &Path, database: &GitDatabase, revision: GitOid) -> Result { let dirname = into.parent().unwrap(); fs_err::create_dir_all(dirname)?; match fs_err::remove_dir_all(into) { Ok(()) => {} Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} Err(e) => return Err(e.into()), } // Perform a local clone of the repository, which will attempt to use // hardlinks to set up the repository. This should speed up the clone operation // quite a bit if it works. let res = ProcessBuilder::new(GIT.as_ref()?) .arg("clone") .arg("--local") // Make sure to pass the local file path and not a file://... url. If given a url, // Git treats the repository as a remote origin and gets confused because we don't // have a HEAD checked out. .arg(database.repo.path.simplified_display().to_string()) .arg(into.simplified_display().to_string()) .exec_with_output(); if let Err(e) = res { debug!("Cloning git repo with --local failed, retrying without hardlinks: {e}"); ProcessBuilder::new(GIT.as_ref()?) .arg("clone") .arg("--no-hardlinks") .arg(database.repo.path.simplified_display().to_string()) .arg(into.simplified_display().to_string()) .exec_with_output()?; } let repo = GitRepository::open(into)?; let checkout = Self::new(revision, repo); let lfs_ready = checkout.reset(database.lfs_ready)?; Ok(checkout.with_lfs_ready(lfs_ready)) } /// Checks if the `HEAD` of this checkout points to the expected revision. fn is_fresh(&self) -> bool { match self.repo.rev_parse("HEAD") { Ok(id) if id == self.revision => { // See comments in reset() for why we check this self.repo.path.join(CHECKOUT_READY_LOCK).exists() } _ => false, } } /// Indicates Git LFS artifacts have been initialized (when requested). pub(crate) fn lfs_ready(&self) -> Option { self.lfs_ready } /// Set the Git LFS validation state (if any). #[must_use] pub(crate) fn with_lfs_ready(mut self, lfs: Option) -> Self { self.lfs_ready = lfs; self } /// This performs `git reset --hard` to the revision of this checkout, with /// additional interrupt protection by a dummy file [`CHECKOUT_READY_LOCK`]. /// /// If we're interrupted while performing a `git reset` (e.g., we die /// because of a signal) uv needs to be sure to try to check out this /// repo again on the next go-round. /// /// To enable this we have a dummy file in our checkout, [`.ok`], /// which if present means that the repo has been successfully reset and is /// ready to go. Hence, if we start to do a reset, we make sure this file /// *doesn't* exist, and then once we're done we create the file. /// /// [`.ok`]: CHECKOUT_READY_LOCK fn reset(&self, with_lfs: Option) -> Result> { let ok_file = self.repo.path.join(CHECKOUT_READY_LOCK); let _ = paths::remove_file(&ok_file); // We want to skip smudge if lfs was disabled for the repository // as smudge filters can trigger on a reset even if lfs artifacts // were not originally "fetched". let lfs_skip_smudge = if with_lfs == Some(true) { "0" } else { "1" }; debug!("Reset {} to {}", self.repo.path.display(), self.revision); // Perform the hard reset. ProcessBuilder::new(GIT.as_ref()?) .arg("reset") .arg("--hard") .arg(self.revision.as_str()) .env(EnvVars::GIT_LFS_SKIP_SMUDGE, lfs_skip_smudge) .cwd(&self.repo.path) .exec_with_output()?; // Update submodules (`git submodule update --recursive`). ProcessBuilder::new(GIT.as_ref()?) .arg("submodule") .arg("update") .arg("--recursive") .arg("--init") .env(EnvVars::GIT_LFS_SKIP_SMUDGE, lfs_skip_smudge) .cwd(&self.repo.path) .exec_with_output() .map(drop)?; // Validate Git LFS objects (if needed) after the reset. // See `fetch_lfs` why we do this. let lfs_validation = match with_lfs { None => None, Some(false) => Some(false), Some(true) => Some(self.repo.lfs_fsck_objects(self.revision.as_str())), }; // The .ok file should be written when the reset is successful. // When Git LFS is enabled, the objects must also be fetched and // validated successfully as part of the corresponding db. if with_lfs.is_none() || lfs_validation == Some(true) { paths::create(ok_file)?; } Ok(lfs_validation) } } /// Attempts to fetch the given git `reference` for a Git repository. /// /// This is the main entry for git clone/fetch. It does the following: /// /// * Turns [`GitReference`] into refspecs accordingly. /// * Dispatches `git fetch` using the git CLI. /// /// The `remote_url` argument is the git remote URL where we want to fetch from. fn fetch( repo: &mut GitRepository, remote_url: &Url, reference: ReferenceOrOid<'_>, disable_ssl: bool, offline: bool, ) -> Result<()> { let oid_to_fetch = if let ReferenceOrOid::Oid(rev) = reference { let local_object = reference.resolve(repo).ok(); if let Some(local_object) = local_object { if rev == local_object { return Ok(()); } } // If we know the reference is a full commit hash, we can just return it without // querying GitHub. Some(rev) } else { None }; // Translate the reference desired here into an actual list of refspecs // which need to get fetched. Additionally record if we're fetching tags. let mut refspecs = Vec::new(); let mut tags = false; let mut refspec_strategy = RefspecStrategy::All; // The `+` symbol on the refspec means to allow a forced (fast-forward) // update which is needed if there is ever a force push that requires a // fast-forward. match reference { // For branches and tags we can fetch simply one reference and copy it // locally, no need to fetch other branches/tags. ReferenceOrOid::Reference(GitReference::Branch(branch)) => { refspecs.push(format!("+refs/heads/{branch}:refs/remotes/origin/{branch}")); } ReferenceOrOid::Reference(GitReference::Tag(tag)) => { refspecs.push(format!("+refs/tags/{tag}:refs/remotes/origin/tags/{tag}")); } ReferenceOrOid::Reference(GitReference::BranchOrTag(branch_or_tag)) => { refspecs.push(format!( "+refs/heads/{branch_or_tag}:refs/remotes/origin/{branch_or_tag}" )); refspecs.push(format!( "+refs/tags/{branch_or_tag}:refs/remotes/origin/tags/{branch_or_tag}" )); refspec_strategy = RefspecStrategy::First; } // For ambiguous references, we can fetch the exact commit (if known); otherwise, // we fetch all branches and tags. ReferenceOrOid::Reference(GitReference::BranchOrTagOrCommit(branch_or_tag_or_commit)) => { // The `oid_to_fetch` is the exact commit we want to fetch. But it could be the exact // commit of a branch or tag. We should only fetch it directly if it's the exact commit // of a short commit hash. if let Some(oid_to_fetch) = oid_to_fetch.filter(|oid| is_short_hash_of(branch_or_tag_or_commit, *oid)) { refspecs.push(format!("+{oid_to_fetch}:refs/commit/{oid_to_fetch}")); } else { // We don't know what the rev will point to. To handle this // situation we fetch all branches and tags, and then we pray // it's somewhere in there. refspecs.push(String::from("+refs/heads/*:refs/remotes/origin/*")); refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD")); tags = true; } } ReferenceOrOid::Reference(GitReference::DefaultBranch) => { refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD")); } ReferenceOrOid::Reference(GitReference::NamedRef(rev)) => { refspecs.push(format!("+{rev}:{rev}")); } ReferenceOrOid::Oid(rev) => { refspecs.push(format!("+{rev}:refs/commit/{rev}")); } } debug!("Performing a Git fetch for: {remote_url}"); let result = match refspec_strategy { RefspecStrategy::All => fetch_with_cli( repo, remote_url, refspecs.as_slice(), tags, disable_ssl, offline, ), RefspecStrategy::First => { // Try each refspec let mut errors = refspecs .iter() .map_while(|refspec| { let fetch_result = fetch_with_cli( repo, remote_url, std::slice::from_ref(refspec), tags, disable_ssl, offline, ); // Stop after the first success and log failures match fetch_result { Err(ref err) => { debug!("Failed to fetch refspec `{refspec}`: {err}"); Some(fetch_result) } Ok(()) => None, } }) .collect::>(); if errors.len() == refspecs.len() { if let Some(result) = errors.pop() { // Use the last error for the message result } else { // Can only occur if there were no refspecs to fetch Ok(()) } } else { Ok(()) } } }; match reference { // With the default branch, adding context is confusing ReferenceOrOid::Reference(GitReference::DefaultBranch) => result, _ => result.with_context(|| { format!( "failed to fetch {} `{}`", reference.kind_str(), reference.as_rev() ) }), } } /// Attempts to use `git` CLI installed on the system to fetch a repository. fn fetch_with_cli( repo: &mut GitRepository, url: &Url, refspecs: &[String], tags: bool, disable_ssl: bool, offline: bool, ) -> Result<()> { let mut cmd = ProcessBuilder::new(GIT.as_ref()?); // Disable interactive prompts in the terminal, as they'll be erased by the progress bar // animation and the process will "hang". Interactive prompts via the GUI like `SSH_ASKPASS` // are still usable. cmd.env(EnvVars::GIT_TERMINAL_PROMPT, "0"); cmd.arg("fetch"); if tags { cmd.arg("--tags"); } if disable_ssl { debug!("Disabling SSL verification for Git fetch via `GIT_SSL_NO_VERIFY`"); cmd.env(EnvVars::GIT_SSL_NO_VERIFY, "true"); } if offline { debug!("Disabling remote protocols for Git fetch via `GIT_ALLOW_PROTOCOL=file`"); cmd.env(EnvVars::GIT_ALLOW_PROTOCOL, "file"); } cmd.arg("--force") // handle force pushes .arg("--update-head-ok") // see discussion in #2078 .arg(url.as_str()) .args(refspecs) // If cargo is run by git (for example, the `exec` command in `git // rebase`), the GIT_DIR is set by git and will point to the wrong // location (this takes precedence over the cwd). Make sure this is // unset so git will look at cwd for the repo. .env_remove(EnvVars::GIT_DIR) // The reset of these may not be necessary, but I'm including them // just to be extra paranoid and avoid any issues. .env_remove(EnvVars::GIT_WORK_TREE) .env_remove(EnvVars::GIT_INDEX_FILE) .env_remove(EnvVars::GIT_OBJECT_DIRECTORY) .env_remove(EnvVars::GIT_ALTERNATE_OBJECT_DIRECTORIES) .cwd(&repo.path); // We capture the output to avoid streaming it to the user's console during clones. // The required `on...line` callbacks currently do nothing. // The output appears to be included in error messages by default. cmd.exec_with_output().map_err(|err| { let msg = err.to_string(); if msg.contains("transport '") && msg.contains("' not allowed") && offline { return GitError::TransportNotAllowed.into(); } err })?; Ok(()) } /// A global cache of the `git lfs` command. /// /// Returns an error if Git LFS isn't available. /// Caching the command allows us to only check if LFS is installed once. /// /// We also support a helper private environment variable to allow /// controlling the LFS extension from being loaded for testing purposes. /// Once installed, Git will always load `git-lfs` as a built-in alias /// which takes priority over loading from `PATH` which prevents us /// from shadowing the extension with other means. pub static GIT_LFS: LazyLock> = LazyLock::new(|| { if std::env::var_os(EnvVars::UV_INTERNAL__TEST_LFS_DISABLED).is_some() { return Err(anyhow!("Git LFS extension has been forcefully disabled.")); } let mut cmd = ProcessBuilder::new(GIT.as_ref()?); cmd.arg("lfs"); // Run a simple command to verify LFS is installed cmd.clone().arg("version").exec_with_output()?; Ok(cmd) }); /// Attempts to use `git-lfs` CLI to fetch required LFS objects for a given revision. fn fetch_lfs( repo: &mut GitRepository, url: &Url, revision: &GitOid, disable_ssl: bool, ) -> Result { let mut cmd = if let Ok(lfs) = GIT_LFS.as_ref() { debug!("Fetching Git LFS objects"); lfs.clone() } else { // Since this feature is opt-in, warn if not available warn!("Git LFS is not available, skipping LFS fetch"); return Ok(false); }; if disable_ssl { debug!("Disabling SSL verification for Git LFS"); cmd.env(EnvVars::GIT_SSL_NO_VERIFY, "true"); } cmd.arg("fetch") .arg(url.as_str()) .arg(revision.as_str()) // These variables are unset for the same reason as in `fetch_with_cli`. .env_remove(EnvVars::GIT_DIR) .env_remove(EnvVars::GIT_WORK_TREE) .env_remove(EnvVars::GIT_INDEX_FILE) .env_remove(EnvVars::GIT_OBJECT_DIRECTORY) .env_remove(EnvVars::GIT_ALTERNATE_OBJECT_DIRECTORIES) // We should not support requesting LFS artifacts with skip smudge being set. // While this may not be necessary, it's added to avoid any potential future issues. .env_remove(EnvVars::GIT_LFS_SKIP_SMUDGE) .cwd(&repo.path); cmd.exec_with_output()?; // We now validate the Git LFS objects explicitly (if supported). This is // needed to avoid issues with Git LFS not being installed or configured // on the system and giving the wrong impression to the user that Git LFS // objects were initialized correctly when installation finishes. // We may want to allow the user to skip validation in the future via // UV_GIT_LFS_NO_VALIDATION environment variable on rare cases where // validation costs outweigh the benefit. let validation_result = repo.lfs_fsck_objects(revision.as_str()); Ok(validation_result) } /// Whether `rev` is a shorter hash of `oid`. fn is_short_hash_of(rev: &str, oid: GitOid) -> bool { let long_hash = oid.to_string(); match long_hash.get(..rev.len()) { Some(truncated_long_hash) => truncated_long_hash.eq_ignore_ascii_case(rev), None => false, } } uv-0.9.17+ds1/crates/uv-git/src/lib.rs000066400000000000000000000005401520155276700173350ustar00rootroot00000000000000pub use crate::credentials::{GIT_STORE, store_credentials_from_url}; pub use crate::git::{GIT, GIT_LFS, GitError}; pub use crate::resolver::{ GitResolver, GitResolverError, RepositoryReference, ResolvedRepositoryReference, }; pub use crate::source::{Fetch, GitSource, Reporter}; mod credentials; mod git; mod rate_limit; mod resolver; mod source; uv-0.9.17+ds1/crates/uv-git/src/rate_limit.rs000066400000000000000000000026031520155276700207220ustar00rootroot00000000000000use reqwest::{Response, StatusCode}; use std::sync::atomic::{AtomicBool, Ordering}; /// A global state on whether we are being rate-limited by GitHub's REST API. /// If we are, avoid "fast-path" attempts. pub(crate) static GITHUB_RATE_LIMIT_STATUS: GitHubRateLimitStatus = GitHubRateLimitStatus::new(); /// GitHub REST API rate limit status tracker. /// /// ## Assumptions /// /// The rate limit timeout duration is much longer than the runtime of a `uv` command. /// And so we do not need to invalidate this state based on `x-ratelimit-reset`. #[derive(Debug)] pub(crate) struct GitHubRateLimitStatus(AtomicBool); impl GitHubRateLimitStatus { const fn new() -> Self { Self(AtomicBool::new(false)) } pub(crate) fn activate(&self) { self.0.store(true, Ordering::Relaxed); } pub(crate) fn is_active(&self) -> bool { self.0.load(Ordering::Relaxed) } } /// Determine if GitHub is applying rate-limiting based on the response pub(crate) fn is_github_rate_limited(response: &Response) -> bool { // HTTP 403 and 429 are possible status codes in the event of a primary or secondary rate limit. // Source: https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api?apiVersion=2022-11-28#rate-limit-errors let status_code = response.status(); status_code == StatusCode::FORBIDDEN || status_code == StatusCode::TOO_MANY_REQUESTS } uv-0.9.17+ds1/crates/uv-git/src/resolver.rs000066400000000000000000000232641520155276700204400ustar00rootroot00000000000000use std::borrow::Cow; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use dashmap::DashMap; use dashmap::mapref::one::Ref; use fs_err::tokio as fs; use reqwest_middleware::ClientWithMiddleware; use tracing::debug; use uv_cache_key::{RepositoryUrl, cache_digest}; use uv_fs::{LockedFile, LockedFileError, LockedFileMode}; use uv_git_types::{GitHubRepository, GitOid, GitReference, GitUrl}; use uv_static::EnvVars; use uv_version::version; use crate::{ Fetch, GitSource, Reporter, rate_limit::{GITHUB_RATE_LIMIT_STATUS, is_github_rate_limited}, }; #[derive(Debug, thiserror::Error)] pub enum GitResolverError { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] LockedFile(#[from] LockedFileError), #[error(transparent)] Join(#[from] tokio::task::JoinError), #[error("Git operation failed")] Git(#[source] anyhow::Error), #[error(transparent)] Reqwest(#[from] reqwest::Error), #[error(transparent)] ReqwestMiddleware(#[from] reqwest_middleware::Error), } /// A resolver for Git repositories. #[derive(Default, Clone)] pub struct GitResolver(Arc>); impl GitResolver { /// Inserts a new [`GitOid`] for the given [`RepositoryReference`]. pub fn insert(&self, reference: RepositoryReference, sha: GitOid) { self.0.insert(reference, sha); } /// Returns the [`GitOid`] for the given [`RepositoryReference`], if it exists. fn get(&self, reference: &RepositoryReference) -> Option> { self.0.get(reference) } /// Return the [`GitOid`] for the given [`GitUrl`], if it is already known. pub fn get_precise(&self, url: &GitUrl) -> Option { // If the URL is already precise, return it. if let Some(precise) = url.precise() { return Some(precise); } // If we know the precise commit already, return it. let reference = RepositoryReference::from(url); if let Some(precise) = self.get(&reference) { return Some(*precise); } None } /// Resolve a Git URL to a specific commit without performing any Git operations. /// /// Returns a [`GitOid`] if the URL has already been resolved (i.e., is available in the cache), /// or if it can be fetched via the GitHub API. Otherwise, returns `None`. pub async fn github_fast_path( &self, url: &GitUrl, client: &ClientWithMiddleware, ) -> Result, GitResolverError> { if std::env::var_os(EnvVars::UV_NO_GITHUB_FAST_PATH).is_some() { return Ok(None); } // If the URL is already precise or we know the precise commit, return it. if let Some(precise) = self.get_precise(url) { return Ok(Some(precise)); } // If the URL is a GitHub URL, attempt to resolve it via the GitHub API. let Some(GitHubRepository { owner, repo }) = GitHubRepository::parse(url.repository()) else { return Ok(None); }; // Check if we're rate-limited by GitHub, before determining the Git reference if GITHUB_RATE_LIMIT_STATUS.is_active() { debug!("Rate-limited by GitHub. Skipping GitHub fast path attempt for: {url}"); return Ok(None); } // Determine the Git reference. let rev = url.reference().as_rev(); let github_api_base_url = std::env::var(EnvVars::UV_GITHUB_FAST_PATH_URL) .unwrap_or("https://api.github.com/repos".to_owned()); let github_api_url = format!("{github_api_base_url}/{owner}/{repo}/commits/{rev}"); debug!("Querying GitHub for commit at: {github_api_url}"); let mut request = client.get(&github_api_url); request = request.header("Accept", "application/vnd.github.3.sha"); request = request.header( "User-Agent", format!("uv/{} (+https://github.com/astral-sh/uv)", version()), ); let response = request.send().await?; let status = response.status(); if !status.is_success() { // Returns a 404 if the repository does not exist, and a 422 if GitHub is unable to // resolve the requested rev. debug!( "GitHub API request failed for: {github_api_url} ({})", response.status() ); if is_github_rate_limited(&response) { // Mark that we are being rate-limited by GitHub GITHUB_RATE_LIMIT_STATUS.activate(); } return Ok(None); } // Parse the response as a Git SHA. let precise = response.text().await?; let precise = GitOid::from_str(&precise).map_err(|err| GitResolverError::Git(err.into()))?; // Insert the resolved URL into the in-memory cache. This ensures that subsequent fetches // resolve to the same precise commit. self.insert(RepositoryReference::from(url), precise); Ok(Some(precise)) } /// Fetch a remote Git repository. pub async fn fetch( &self, url: &GitUrl, disable_ssl: bool, offline: bool, cache: PathBuf, reporter: Option>, ) -> Result { debug!("Fetching source distribution from Git: {url}"); let reference = RepositoryReference::from(url); // If we know the precise commit already, reuse it, to ensure that all fetches within a // single process are consistent. let url = { if let Some(precise) = self.get(&reference) { Cow::Owned(url.clone().with_precise(*precise)) } else { Cow::Borrowed(url) } }; // Avoid races between different processes, too. let lock_dir = cache.join("locks"); fs::create_dir_all(&lock_dir).await?; let repository_url = RepositoryUrl::new(url.repository()); let _lock = LockedFile::acquire( lock_dir.join(cache_digest(&repository_url)), LockedFileMode::Exclusive, &repository_url, ) .await?; // Fetch the Git repository. let source = if let Some(reporter) = reporter { GitSource::new(url.as_ref().clone(), cache, offline).with_reporter(reporter) } else { GitSource::new(url.as_ref().clone(), cache, offline) }; // If necessary, disable SSL. let source = if disable_ssl { source.dangerous() } else { source }; let fetch = tokio::task::spawn_blocking(move || source.fetch()) .await? .map_err(GitResolverError::Git)?; // Insert the resolved URL into the in-memory cache. This ensures that subsequent fetches // resolve to the same precise commit. if let Some(precise) = fetch.git().precise() { self.insert(reference, precise); } Ok(fetch) } /// Given a remote source distribution, return a precise variant, if possible. /// /// For example, given a Git dependency with a reference to a branch or tag, return a URL /// with a precise reference to the current commit of that branch or tag. /// /// This method takes into account various normalizations that are independent of the Git /// layer. For example: removing `#subdirectory=pkg_dir`-like fragments, and removing `git+` /// prefix kinds. /// /// This method will only return precise URLs for URLs that have already been resolved via /// [`resolve_precise`], and will return `None` for URLs that have not been resolved _or_ /// already have a precise reference. pub fn precise(&self, url: GitUrl) -> Option { let reference = RepositoryReference::from(&url); let precise = self.get(&reference)?; Some(url.with_precise(*precise)) } /// Returns `true` if the two Git URLs refer to the same precise commit. pub fn same_ref(&self, a: &GitUrl, b: &GitUrl) -> bool { // Convert `a` to a repository URL. let a_ref = RepositoryReference::from(a); // Convert `b` to a repository URL. let b_ref = RepositoryReference::from(b); // The URLs must refer to the same repository. if a_ref.url != b_ref.url { return false; } // If the URLs have the same tag, they refer to the same commit. if a_ref.reference == b_ref.reference { return true; } // Otherwise, the URLs must resolve to the same precise commit. let Some(a_precise) = a.precise().or_else(|| self.get(&a_ref).map(|sha| *sha)) else { return false; }; let Some(b_precise) = b.precise().or_else(|| self.get(&b_ref).map(|sha| *sha)) else { return false; }; a_precise == b_precise } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ResolvedRepositoryReference { /// An abstract reference to a Git repository, including the URL and the commit (e.g., a branch, /// tag, or revision). pub reference: RepositoryReference, /// The precise commit SHA of the reference. pub sha: GitOid, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct RepositoryReference { /// The URL of the Git repository, with any query parameters and fragments removed. pub url: RepositoryUrl, /// The reference to the commit to use, which could be a branch, tag, or revision. pub reference: GitReference, } impl From<&GitUrl> for RepositoryReference { fn from(git: &GitUrl) -> Self { Self { url: RepositoryUrl::new(git.repository()), reference: git.reference().clone(), } } } uv-0.9.17+ds1/crates/uv-git/src/source.rs000066400000000000000000000202111520155276700200640ustar00rootroot00000000000000//! Git support is derived from Cargo's implementation. //! Cargo is dual-licensed under either Apache 2.0 or MIT, at the user's choice. //! Source: use std::borrow::Cow; use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::Result; use tracing::{debug, instrument}; use uv_cache_key::{RepositoryUrl, cache_digest}; use uv_git_types::{GitOid, GitReference, GitUrl}; use uv_redacted::DisplaySafeUrl; use crate::GIT_STORE; use crate::git::{GitDatabase, GitRemote}; /// A remote Git source that can be checked out locally. pub struct GitSource { /// The Git reference from the manifest file. git: GitUrl, /// Whether to disable SSL verification. disable_ssl: bool, /// Whether to operate without network connectivity. offline: bool, /// The path to the Git source database. cache: PathBuf, /// The reporter to use for this source. reporter: Option>, } impl GitSource { /// Initialize a [`GitSource`] with the given Git URL, HTTP client, and cache path. pub fn new(git: GitUrl, cache: impl Into, offline: bool) -> Self { Self { git, disable_ssl: false, offline, cache: cache.into(), reporter: None, } } /// Disable SSL verification for this [`GitSource`]. #[must_use] pub fn dangerous(self) -> Self { Self { disable_ssl: true, ..self } } /// Set the [`Reporter`] to use for the [`GitSource`]. #[must_use] pub fn with_reporter(self, reporter: Arc) -> Self { Self { reporter: Some(reporter), ..self } } /// Fetch the underlying Git repository at the given revision. #[instrument(skip(self), fields(repository = %self.git.repository(), rev = ?self.git.precise()))] pub fn fetch(self) -> Result { let lfs_requested = self.git.lfs().enabled(); // Compute the canonical URL for the repository. let canonical = RepositoryUrl::new(self.git.repository()); // The path to the repo, within the Git database. let ident = cache_digest(&canonical); let db_path = self.cache.join("db").join(&ident); // Authenticate the URL, if necessary. let remote = if let Some(credentials) = GIT_STORE.get(&canonical) { Cow::Owned(credentials.apply(self.git.repository().clone())) } else { Cow::Borrowed(self.git.repository()) }; // Fetch the commit, if we don't already have it. Wrapping this section in a closure makes // it easier to short-circuit this in the cases where we do have the commit. let (db, actual_rev, maybe_task) = || -> Result<(GitDatabase, GitOid, Option)> { let git_remote = GitRemote::new(&remote); let maybe_db = git_remote.db_at(&db_path).ok(); // If we have a locked revision, and we have a pre-existing database which has that // revision, then no update needs to happen. // When requested, we also check if LFS artifacts have been fetched and validated. if let (Some(rev), Some(db)) = (self.git.precise(), &maybe_db) { if db.contains(rev) && (!lfs_requested || db.contains_lfs_artifacts(rev)) { debug!("Using existing Git source `{}`", self.git.repository()); return Ok(( maybe_db .unwrap() .with_lfs_ready(lfs_requested.then_some(true)), rev, None, )); } } // If the revision isn't locked, but it looks like it might be an exact commit hash, // and we do have a pre-existing database, then check whether it is, in fact, a commit // hash. If so, treat it like it's locked. // When requested, we also check if LFS artifacts have been fetched and validated. if let Some(db) = &maybe_db { if let GitReference::BranchOrTagOrCommit(maybe_commit) = self.git.reference() { if let Ok(oid) = maybe_commit.parse::() { if db.contains(oid) && (!lfs_requested || db.contains_lfs_artifacts(oid)) { // This reference is an exact commit. Treat it like it's locked. debug!("Using existing Git source `{}`", self.git.repository()); return Ok(( maybe_db .unwrap() .with_lfs_ready(lfs_requested.then_some(true)), oid, None, )); } } } } // ... otherwise, we use this state to update the Git database. Note that we still check // for being offline here, for example in the situation that we have a locked revision // but the database doesn't have it. debug!("Updating Git source `{}`", self.git.repository()); // Report the checkout operation to the reporter. let task = self.reporter.as_ref().map(|reporter| { reporter.on_checkout_start(git_remote.url(), self.git.reference().as_rev()) }); let (db, actual_rev) = git_remote.checkout( &db_path, maybe_db, self.git.reference(), self.git.precise(), self.disable_ssl, self.offline, lfs_requested, )?; Ok((db, actual_rev, task)) }()?; // Don’t use the full hash, in order to contribute less to reaching the // path length limit on Windows. let short_id = db.to_short_id(actual_rev)?; // Compute the canonical URL for the repository checkout. let canonical = canonical.with_lfs(Some(lfs_requested)); // Recompute the checkout hash when Git LFS is enabled as we want // to distinctly differentiate between LFS vs non-LFS source trees. let ident = if lfs_requested { cache_digest(&canonical) } else { ident }; let checkout_path = self .cache .join("checkouts") .join(&ident) .join(short_id.as_str()); // Check out `actual_rev` from the database to a scoped location on the // filesystem. This will use hard links and such to ideally make the // checkout operation here pretty fast. let checkout = db.copy_to(actual_rev, &checkout_path)?; // Report the checkout operation to the reporter. if let Some(task) = maybe_task { if let Some(reporter) = self.reporter.as_ref() { reporter.on_checkout_complete(remote.as_ref(), actual_rev.as_str(), task); } } Ok(Fetch { git: self.git.with_precise(actual_rev), path: checkout_path, lfs_ready: checkout.lfs_ready().unwrap_or(false), }) } } pub struct Fetch { /// The [`GitUrl`] reference that was fetched. git: GitUrl, /// The path to the checked out repository. path: PathBuf, /// Git LFS artifacts have been initialized (if requested). lfs_ready: bool, } impl Fetch { pub fn git(&self) -> &GitUrl { &self.git } pub fn path(&self) -> &Path { &self.path } pub fn lfs_ready(&self) -> &bool { &self.lfs_ready } pub fn into_git(self) -> GitUrl { self.git } pub fn into_path(self) -> PathBuf { self.path } } pub trait Reporter: Send + Sync { /// Callback to invoke when a repository checkout begins. fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize; /// Callback to invoke when a repository checkout completes. fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, index: usize); } uv-0.9.17+ds1/crates/uv-globfilter/000077500000000000000000000000001520155276700170015ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-globfilter/Cargo.toml000066400000000000000000000013411520155276700207300ustar00rootroot00000000000000[package] name = "uv-globfilter" version = "0.0.7" description = "This is an internal component crate of uv" readme = "README.md" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [dependencies] globset = { workspace = true } owo-colors = { workspace = true } regex = { workspace = true } regex-automata = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } walkdir = { workspace = true } [dev-dependencies] anstream = { workspace = true } fs-err = { workspace = true } insta = { workspace = true } tempfile = { workspace = true } [lints] workspace = true uv-0.9.17+ds1/crates/uv-globfilter/README.md000066400000000000000000000030201520155276700202530ustar00rootroot00000000000000# globfilter Portable directory walking with includes and excludes. Motivating example: You want to allow the user to select paths within a project. ```toml include = ["src", "License.txt", "resources/icons/*.svg"] exclude = ["target", "/dist", ".cache", "*.tmp"] ``` When traversing the directory, you can use `GlobDirFilter::from_globs(...)?.match_directory(&relative)` skip directories that never match in `WalkDir`s `filter_entry`. ## Syntax This crate supports the cross-language, restricted glob syntax from [PEP 639](https://packaging.python.org/en/latest/specifications/glob-patterns/): - Alphanumeric characters, underscores (`_`), hyphens (`-`) and dots (`.`) are matched verbatim. - The special glob characters are: - `*`: Matches any number of characters except path separators - `?`: Matches a single character except the path separator - `**`: Matches any number of characters including path separators - `[]`, containing only the verbatim matched characters: Matches a single of the characters contained. Within `[...]`, the hyphen indicates a locale-agnostic range (e.g., `a-z`, order based on Unicode code points). Hyphens at the start or end are matched literally. - The path separator is the forward slash character (`/`). Patterns are relative to the given directory, a leading slash character for absolute paths is not supported. - Parent directory indicators (`..`) are not allowed. These rules mean that matching the backslash (`\`) is forbidden, which avoid collisions with the windows path separator. uv-0.9.17+ds1/crates/uv-globfilter/src/000077500000000000000000000000001520155276700175705ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-globfilter/src/glob_dir_filter.rs000066400000000000000000000236641520155276700232770ustar00rootroot00000000000000use globset::{Glob, GlobSet, GlobSetBuilder}; use regex_automata::dfa; use regex_automata::dfa::Automaton; use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR, Path}; use tracing::warn; /// Chosen at a whim -Konsti const DFA_SIZE_LIMIT: usize = 1_000_000; /// Filter a directory tree traversal (walkdir) by whether any paths of a directory can be included /// at all. /// /// Internally, the globs are converted to a regex and then to a DFA, which unlike the globs and the /// regex allows to check for prefix matches. pub struct GlobDirFilter { glob_set: GlobSet, dfa: Option>>, } impl GlobDirFilter { /// The filter matches if any of the globs matches. /// /// See for the error returned. pub fn from_globs(globs: &[Glob]) -> Result { let mut glob_set_builder = GlobSetBuilder::new(); for glob in globs { glob_set_builder.add(glob.clone()); } let glob_set = glob_set_builder.build()?; let regexes: Vec<_> = globs .iter() .map(|glob| { let main_separator = regex::escape(MAIN_SEPARATOR_STR); glob.regex() // We are using a custom DFA builder .strip_prefix("(?-u)") .expect("a glob is a non-unicode byte regex") // Match windows paths if applicable .replace('/', &main_separator) }) .collect(); let dfa_builder = dfa::dense::Builder::new() .syntax( // The glob regex is a byte matcher regex_automata::util::syntax::Config::new() .unicode(false) .utf8(false), ) .configure( dfa::dense::Config::new() .start_kind(dfa::StartKind::Anchored) // DFA can grow exponentially, in which case we bail out .dfa_size_limit(Some(DFA_SIZE_LIMIT)) .determinize_size_limit(Some(DFA_SIZE_LIMIT)), ) .build_many(®exes); let dfa = if let Ok(dfa) = dfa_builder { Some(dfa) } else { // TODO(konsti): `regex_automata::dfa::dense::BuildError` should allow asking whether // is a size error warn!( "Glob expressions regex is larger than {DFA_SIZE_LIMIT} bytes, \ falling back to full directory traversal!" ); None }; Ok(Self { glob_set, dfa }) } /// Whether the path (file or directory) matches any of the globs. /// /// We include a directory if we are potentially including files it contains. pub fn match_path(&self, path: &Path) -> bool { self.match_directory(path) || self.glob_set.is_match(path) } /// Check whether a directory or any of its children can be matched by any of the globs. /// /// This option never returns false if any child matches, but it may return true even if we /// don't end up including any child. pub fn match_directory(&self, path: &Path) -> bool { let Some(dfa) = &self.dfa else { return true; }; // Allow the root path if path == Path::new("") { return true; } let config_anchored = regex_automata::util::start::Config::new().anchored(regex_automata::Anchored::Yes); let mut state = dfa.start_state(&config_anchored).unwrap(); // Paths aren't necessarily UTF-8, which we can gloss over since the globs match bytes only // anyway. let byte_path = path.as_os_str().as_encoded_bytes(); for b in byte_path { state = dfa.next_state(state, *b); } // Say we're looking at a directory `foo/bar`. We want to continue if either `foo/bar` is // a match, e.g., from `foo/*`, or a path below it can match, e.g., from `foo/bar/*`. let eoi_state = dfa.next_eoi_state(state); // We must not call `next_eoi_state` on the slash state, we want to only check if more // characters (path components) are allowed, not if we're matching the `$` anchor at the // end. let slash_state = dfa.next_state(state, u8::try_from(MAIN_SEPARATOR).unwrap()); debug_assert!( !dfa.is_quit_state(eoi_state) && !dfa.is_quit_state(slash_state), "matcher is in quit state" ); dfa.is_match_state(eoi_state) || !dfa.is_dead_state(slash_state) } } #[cfg(test)] mod tests { use crate::PortableGlobParser; use crate::glob_dir_filter::GlobDirFilter; use std::path::{MAIN_SEPARATOR, Path}; use tempfile::tempdir; use walkdir::WalkDir; const FILES: [&str; 5] = [ "path1/dir1/subdir/a.txt", "path2/dir2/subdir/a.txt", "path3/dir3/subdir/a.txt", "path4/dir4/subdir/a.txt", "path5/dir5/subdir/a.txt", ]; const PATTERNS: [&str; 5] = [ // Only sufficient for descending one level "path1/*", // Only sufficient for descending one level "path2/dir2", // Sufficient for descending "path3/dir3/subdir/a.txt", // Sufficient for descending "path4/**/*", // Not sufficient for descending "path5", ]; #[test] fn match_directory() { let patterns = PATTERNS.map(|pattern| PortableGlobParser::Pep639.parse(pattern).unwrap()); let matcher = GlobDirFilter::from_globs(&patterns).unwrap(); assert!(matcher.match_directory(&Path::new("path1").join("dir1"))); assert!(matcher.match_directory(&Path::new("path2").join("dir2"))); assert!(matcher.match_directory(&Path::new("path3").join("dir3"))); assert!(matcher.match_directory(&Path::new("path4").join("dir4"))); assert!(!matcher.match_directory(&Path::new("path5").join("dir5"))); } /// Check that we skip directories that can never match. #[test] fn prefilter() { let dir = tempdir().unwrap(); for file in FILES { let file = dir.path().join(file); fs_err::create_dir_all(file.parent().unwrap()).unwrap(); fs_err::File::create(file).unwrap(); } let patterns = PATTERNS.map(|pattern| PortableGlobParser::Pep639.parse(pattern).unwrap()); let matcher = GlobDirFilter::from_globs(&patterns).unwrap(); // Test the prefix filtering let visited: Vec<_> = WalkDir::new(dir.path()) .sort_by_file_name() .into_iter() .filter_entry(|entry| { let relative = entry .path() .strip_prefix(dir.path()) .expect("walkdir starts with root"); matcher.match_directory(relative) }) .map(|entry| { let entry = entry.unwrap(); let relative = entry .path() .strip_prefix(dir.path()) .expect("walkdir starts with root") .to_str() .unwrap() .to_string(); // Translate windows paths back to the unix fixture relative.replace(MAIN_SEPARATOR, "/") }) .collect(); assert_eq!( visited, [ "", "path1", "path1/dir1", "path2", "path2/dir2", "path3", "path3/dir3", "path3/dir3/subdir", "path3/dir3/subdir/a.txt", "path4", "path4/dir4", "path4/dir4/subdir", "path4/dir4/subdir/a.txt", "path5" ] ); } /// Check that the walkdir yield the correct set of files. #[test] fn walk_dir() { let dir = tempdir().unwrap(); for file in FILES { let file = dir.path().join(file); fs_err::create_dir_all(file.parent().unwrap()).unwrap(); fs_err::File::create(file).unwrap(); } let patterns = PATTERNS.map(|pattern| PortableGlobParser::Pep639.parse(pattern).unwrap()); let include_matcher = GlobDirFilter::from_globs(&patterns).unwrap(); let walkdir_root = dir.path(); let mut matches: Vec<_> = WalkDir::new(walkdir_root) .sort_by_file_name() .into_iter() .filter_entry(|entry| { // TODO(konsti): This should be prettier. let relative = entry .path() .strip_prefix(walkdir_root) .expect("walkdir starts with root"); include_matcher.match_directory(relative) }) .filter_map(|entry| { let entry = entry.as_ref().unwrap(); // TODO(konsti): This should be prettier. let relative = entry .path() .strip_prefix(walkdir_root) .expect("walkdir starts with root"); if include_matcher.match_path(relative) { // Translate windows paths back to the unix fixture Some(relative.to_str().unwrap().replace(MAIN_SEPARATOR, "/")) } else { None } }) .collect(); matches.sort(); assert_eq!( matches, [ "", "path1", "path1/dir1", "path2", "path2/dir2", "path3", "path3/dir3", "path3/dir3/subdir", "path3/dir3/subdir/a.txt", "path4", "path4/dir4", "path4/dir4/subdir", "path4/dir4/subdir/a.txt", "path5" ] ); } } uv-0.9.17+ds1/crates/uv-globfilter/src/lib.rs000066400000000000000000000005261520155276700207070ustar00rootroot00000000000000//! Implementation of PEP 639 cross-language restricted globs and a related directory traversal //! prefilter. //! //! The goal is globs that are portable between languages and operating systems. mod glob_dir_filter; mod portable_glob; pub use glob_dir_filter::GlobDirFilter; pub use portable_glob::{PortableGlobError, PortableGlobParser}; uv-0.9.17+ds1/crates/uv-globfilter/src/main.rs000066400000000000000000000041021520155276700210570ustar00rootroot00000000000000#![allow(clippy::print_stdout)] use globset::GlobSetBuilder; use std::env::args; use tracing::trace; use uv_globfilter::{GlobDirFilter, PortableGlobParser}; use walkdir::WalkDir; fn main() { let includes = ["src/**", "pyproject.toml"]; let excludes = ["__pycache__", "*.pyc", "*.pyo"]; let mut include_globs = Vec::new(); for include in includes { let glob = PortableGlobParser::Pep639.parse(include).unwrap(); include_globs.push(glob.clone()); } let include_matcher = GlobDirFilter::from_globs(&include_globs).unwrap(); let mut exclude_builder = GlobSetBuilder::new(); for exclude in excludes { // Excludes are unanchored let exclude = if let Some(exclude) = exclude.strip_prefix("/") { exclude.to_string() } else { format!("**/{exclude}").to_string() }; let glob = PortableGlobParser::Pep639.parse(&exclude).unwrap(); exclude_builder.add(glob); } // https://github.com/BurntSushi/ripgrep/discussions/2927 let exclude_matcher = exclude_builder.build().unwrap(); let walkdir_root = args().next().unwrap(); for entry in WalkDir::new(&walkdir_root) .sort_by_file_name() .into_iter() .filter_entry(|entry| { // TODO(konsti): This should be prettier. let relative = entry .path() .strip_prefix(&walkdir_root) .expect("walkdir starts with root") .to_path_buf(); include_matcher.match_directory(&relative) && !exclude_matcher.is_match(&relative) }) { let entry = entry.unwrap(); // TODO(konsti): This should be prettier. let relative = entry .path() .strip_prefix(&walkdir_root) .expect("walkdir starts with root") .to_path_buf(); if !include_matcher.match_path(&relative) || exclude_matcher.is_match(&relative) { trace!("Excluding: `{}`", relative.display()); continue; } println!("{}", relative.display()); } } uv-0.9.17+ds1/crates/uv-globfilter/src/portable_glob.rs000066400000000000000000000313671520155276700227630ustar00rootroot00000000000000//! Cross-language glob syntax from //! [PEP 639](https://packaging.python.org/en/latest/specifications/glob-patterns/). use globset::{Glob, GlobBuilder}; use owo_colors::OwoColorize; use thiserror::Error; #[derive(Debug, Error)] pub enum PortableGlobError { /// Shows the failing glob in the error message. #[error(transparent)] GlobError(#[from] globset::Error), #[error( "The parent directory operator (`..`) at position {pos} is not allowed in glob: `{glob}`" )] ParentDirectory { glob: String, pos: usize }, #[error("Invalid character `{invalid}` at position {pos} in glob: `{glob}`")] InvalidCharacter { glob: String, pos: usize, invalid: char, }, #[error( "Invalid character `{invalid}` at position {pos} in glob: `{glob}`. {}{} Characters can be escaped with a backslash", "hint".bold().cyan(), ":".bold() )] InvalidCharacterUv { glob: String, pos: usize, invalid: char, }, #[error( "Only forward slashes are allowed as path separator, invalid character at position {pos} in glob: `{glob}`" )] InvalidBackslash { glob: String, pos: usize }, #[error( "Path separators can't be escaped, invalid character at position {pos} in glob: `{glob}`" )] InvalidEscapee { glob: String, pos: usize }, #[error("Invalid character `{invalid}` in range at position {pos} in glob: `{glob}`")] InvalidCharacterRange { glob: String, pos: usize, invalid: char, }, #[error("Too many at stars at position {pos} in glob: `{glob}`")] TooManyStars { glob: String, pos: usize }, #[error("Trailing backslash at position {pos} in glob: `{glob}`")] TrailingEscape { glob: String, pos: usize }, } /// Cross-language glob syntax from /// [PEP 639](https://packaging.python.org/en/latest/specifications/glob-patterns/). /// /// The variant determines whether the parser strictly adheres to PEP 639 rules or allows extensions /// such as backslash escapes. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum PortableGlobParser { /// Follow the PEP 639 rules strictly. Pep639, /// In addition to the PEP 639 syntax, allow escaping characters with backslashes. /// /// For cross-platform compatibility, escaping path separators is not allowed, i.e., forward /// slashes and backslashes can't be escaped. Uv, } impl PortableGlobParser { fn backslash_escape(self) -> bool { match self { Self::Pep639 => false, Self::Uv => true, } } /// Parse cross-language glob syntax based on [PEP 639](https://packaging.python.org/en/latest/specifications/glob-patterns/): /// /// - Alphanumeric characters, underscores (`_`), hyphens (`-`) and dots (`.`) are matched verbatim. /// - The special glob characters are: /// - `*`: Matches any number of characters except path separators /// - `?`: Matches a single character except the path separator /// - `**`: Matches any number of characters including path separators /// - `[]`, containing only the verbatim matched characters: Matches a single of the characters contained. Within /// `[...]`, the hyphen indicates a locale-agnostic range (e.g. `a-z`, order based on Unicode code points). Hyphens at /// the start or end are matched literally. /// - `\`: Disallowed in PEP 639 mode. In uv mode, it escapes the following character to be matched verbatim. /// - The path separator is the forward slash character (`/`). Patterns are relative to the given directory, a leading slash /// character for absolute paths is not supported. /// - Parent directory indicators (`..`) are not allowed. /// /// These rules mean that matching the backslash (`\`) is forbidden, which avoid collisions with the windows path separator. pub fn parse(&self, glob: &str) -> Result { self.check(glob)?; Ok(GlobBuilder::new(glob) .literal_separator(true) // No need to support Windows-style paths, so the backslash can be used a escape. .backslash_escape(self.backslash_escape()) .build()?) } /// See [`parse_portable_glob`]. pub fn check(&self, glob: &str) -> Result<(), PortableGlobError> { let mut chars = glob.chars().enumerate().peekable(); // A `..` is on a parent directory indicator at the start of the string or after a directory // separator. let mut start_or_slash = true; // The number of consecutive stars before the current character. while let Some((pos, c)) = chars.next() { // `***` or `**literals` can be correctly represented with less stars. They are banned by // `glob`, they are allowed by `globset` and PEP 639 is ambiguous, so we're filtering them // out. if c == '*' { let mut star_run = 1; while let Some((_, c)) = chars.peek() { if *c == '*' { star_run += 1; chars.next(); } else { break; } } if star_run >= 3 { return Err(PortableGlobError::TooManyStars { glob: glob.to_string(), // We don't update pos for the stars. pos, }); } else if star_run == 2 { if chars.peek().is_some_and(|(_, c)| *c != '/') { return Err(PortableGlobError::TooManyStars { glob: glob.to_string(), // We don't update pos for the stars. pos, }); } } start_or_slash = false; } else if c.is_alphanumeric() || matches!(c, '_' | '-' | '?') { start_or_slash = false; } else if c == '.' { if start_or_slash && matches!(chars.peek(), Some((_, '.'))) { return Err(PortableGlobError::ParentDirectory { pos, glob: glob.to_string(), }); } start_or_slash = false; } else if c == '/' { start_or_slash = true; } else if c == '[' { for (pos, c) in chars.by_ref() { if c.is_alphanumeric() || matches!(c, '_' | '-' | '.') { // Allowed. } else if c == ']' { break; } else { return Err(PortableGlobError::InvalidCharacterRange { glob: glob.to_string(), pos, invalid: c, }); } } start_or_slash = false; } else if c == '\\' { match self { Self::Pep639 => { return Err(PortableGlobError::InvalidBackslash { glob: glob.to_string(), pos, }); } Self::Uv => { match chars.next() { Some((pos, '/' | '\\')) => { // For cross-platform compatibility, we don't allow forward slashes or // backslashes to be escaped. return Err(PortableGlobError::InvalidEscapee { glob: glob.to_string(), pos, }); } Some(_) => { // Escaped character } None => { return Err(PortableGlobError::TrailingEscape { glob: glob.to_string(), pos, }); } } } } } else { let err = match self { Self::Pep639 => PortableGlobError::InvalidCharacter { glob: glob.to_string(), pos, invalid: c, }, Self::Uv => PortableGlobError::InvalidCharacterUv { glob: glob.to_string(), pos, invalid: c, }, }; return Err(err); } } Ok(()) } } #[cfg(test)] mod tests { use super::*; use insta::assert_snapshot; #[test] fn test_error() { let parse_err = |glob| { let error = PortableGlobParser::Pep639.parse(glob).unwrap_err(); anstream::adapter::strip_str(&error.to_string()).to_string() }; assert_snapshot!( parse_err(".."), @"The parent directory operator (`..`) at position 0 is not allowed in glob: `..`" ); assert_snapshot!( parse_err("licenses/.."), @"The parent directory operator (`..`) at position 9 is not allowed in glob: `licenses/..`" ); assert_snapshot!( parse_err("licenses/LICEN!E.txt"), @"Invalid character `!` at position 14 in glob: `licenses/LICEN!E.txt`" ); assert_snapshot!( parse_err("licenses/LICEN[!C]E.txt"), @"Invalid character `!` in range at position 15 in glob: `licenses/LICEN[!C]E.txt`" ); assert_snapshot!( parse_err("licenses/LICEN[C?]E.txt"), @"Invalid character `?` in range at position 16 in glob: `licenses/LICEN[C?]E.txt`" ); assert_snapshot!( parse_err("******"), @"Too many at stars at position 0 in glob: `******`" ); assert_snapshot!( parse_err("licenses/**license"), @"Too many at stars at position 9 in glob: `licenses/**license`" ); assert_snapshot!( parse_err("licenses/***/licenses.csv"), @"Too many at stars at position 9 in glob: `licenses/***/licenses.csv`" ); assert_snapshot!( parse_err(r"licenses\eula.txt"), @r"Only forward slashes are allowed as path separator, invalid character at position 8 in glob: `licenses\eula.txt`" ); assert_snapshot!( parse_err(r"**/@test"), @"Invalid character `@` at position 3 in glob: `**/@test`" ); // Escapes are not allowed in strict PEP 639 mode assert_snapshot!( parse_err(r"public domain/Gulliver\\’s Travels.txt"), @r"Invalid character ` ` at position 6 in glob: `public domain/Gulliver\\’s Travels.txt`" ); let parse_err_uv = |glob| { let error = PortableGlobParser::Uv.parse(glob).unwrap_err(); anstream::adapter::strip_str(&error.to_string()).to_string() }; assert_snapshot!( parse_err_uv(r"**/@test"), @"Invalid character `@` at position 3 in glob: `**/@test`. hint: Characters can be escaped with a backslash" ); // Escaping slashes is not allowed. assert_snapshot!( parse_err_uv(r"licenses\\MIT.txt"), @r"Path separators can't be escaped, invalid character at position 9 in glob: `licenses\\MIT.txt`" ); assert_snapshot!( parse_err_uv(r"licenses\/MIT.txt"), @r"Path separators can't be escaped, invalid character at position 9 in glob: `licenses\/MIT.txt`" ); } #[test] fn test_valid() { let cases = [ r"licenses/*.txt", r"licenses/**/*.txt", r"LICEN[CS]E.txt", r"LICEN?E.txt", r"[a-z].txt", r"[a-z._-].txt", r"*/**", r"LICENSE..txt", r"LICENSE_file-1.txt", // (google translate) r"licenses/ë¼ì´ì„¼ìФ*.txt", r"licenses/ライセンス*.txt", r"licenses/执照*.txt", r"src/**", ]; let cases_uv = [ r"public-domain/Gulliver\’s\ Travels.txt", // https://github.com/astral-sh/uv/issues/13280 r"**/\@test", ]; for case in cases { PortableGlobParser::Pep639.parse(case).unwrap(); } for case in cases.iter().chain(cases_uv.iter()) { PortableGlobParser::Uv.parse(case).unwrap(); } } } uv-0.9.17+ds1/crates/uv-install-wheel/000077500000000000000000000000001520155276700174205ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-install-wheel/Cargo.toml000066400000000000000000000032571520155276700213570ustar00rootroot00000000000000[package] name = "uv-install-wheel" version = "0.0.7" description = "This is an internal component crate of uv" keywords = ["wheel", "python"] edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } repository = { workspace = true } authors = { workspace = true } license = { workspace = true } [lints] workspace = true [lib] doctest = false name = "uv_install_wheel" [dependencies] uv-distribution-filename = { workspace = true } uv-flags = { workspace = true } uv-fs = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-shell = { workspace = true } uv-trampoline-builder = { workspace = true } uv-warnings = { workspace = true } clap = { workspace = true, optional = true, features = ["derive"] } configparser = { workspace = true } csv = { workspace = true } data-encoding = { workspace = true } fs-err = { workspace = true } mailparse = { workspace = true } owo-colors = { workspace = true } pathdiff = { workspace = true } reflink-copy = { workspace = true } regex = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha2 = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } walkdir = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] same-file = { workspace = true } self-replace = { workspace = true } [dev-dependencies] anyhow = { workspace = true } assert_fs = { workspace = true } indoc = { workspace = true } uv-0.9.17+ds1/crates/uv-install-wheel/README.md000066400000000000000000000010431520155276700206750ustar00rootroot00000000000000 # uv-install-wheel This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. This version (0.0.7) is a component of [uv 0.9.17](https://crates.io/crates/uv/0.9.17). The source can be found [here](https://github.com/astral-sh/uv/blob/0.9.17/crates/uv-install-wheel). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) for details on versioning. uv-0.9.17+ds1/crates/uv-install-wheel/src/000077500000000000000000000000001520155276700202075ustar00rootroot00000000000000uv-0.9.17+ds1/crates/uv-install-wheel/src/install.rs000066400000000000000000000134531520155276700222310ustar00rootroot00000000000000//! Like `wheel.rs`, but for installing wheels that have already been unzipped, rather than //! reading from a zip file. use std::path::Path; use std::str::FromStr; use fs_err::File; use tracing::{instrument, trace}; use uv_distribution_filename::WheelFilename; use uv_pep440::Version; use uv_pypi_types::{DirectUrl, Metadata10}; use crate::linker::{LinkMode, Locks}; use crate::wheel::{ LibKind, WheelFile, dist_info_metadata, find_dist_info, install_data, parse_scripts, read_record_file, write_installer_metadata, write_script_entrypoints, }; use crate::{Error, Layout}; /// Install the given wheel to the given venv /// /// The caller must ensure that the wheel is compatible to the environment. /// /// /// /// Wheel 1.0: #[instrument(skip_all, fields(wheel = %filename))] pub fn install_wheel( layout: &Layout, relocatable: bool, wheel: impl AsRef, filename: &WheelFilename, direct_url: Option<&DirectUrl>, cache_info: Option<&Cache>, build_info: Option<&Build>, installer: Option<&str>, installer_metadata: bool, link_mode: LinkMode, locks: &Locks, ) -> Result<(), Error> { let dist_info_prefix = find_dist_info(&wheel)?; let metadata = dist_info_metadata(&dist_info_prefix, &wheel)?; let Metadata10 { name, version } = Metadata10::parse_pkg_info(&metadata) .map_err(|err| Error::InvalidWheel(err.to_string()))?; let version = Version::from_str(&version)?; // Validate the wheel name and version. if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) { if name != filename.name { return Err(Error::MismatchedName(name, filename.name.clone())); } if version != filename.version && version != filename.version.clone().without_local() { return Err(Error::MismatchedVersion(version, filename.version.clone())); } } // We're going step by step though // https://packaging.python.org/en/latest/specifications/binary-distribution-format/#installing-a-wheel-distribution-1-0-py32-none-any-whl // > 1.a Parse distribution-1.0.dist-info/WHEEL. // > 1.b Check that installer is compatible with Wheel-Version. Warn if minor version is greater, abort if major version is greater. let wheel_file_path = wheel .as_ref() .join(format!("{dist_info_prefix}.dist-info/WHEEL")); let wheel_text = fs_err::read_to_string(wheel_file_path)?; let lib_kind = WheelFile::parse(&wheel_text)?.lib_kind(); // > 1.c If Root-Is-Purelib == ‘true’, unpack archive into purelib (site-packages). // > 1.d Else unpack archive into platlib (site-packages). trace!(?name, "Extracting file"); let site_packages = match lib_kind { LibKind::Pure => &layout.scheme.purelib, LibKind::Plat => &layout.scheme.platlib, }; let num_unpacked = link_mode.link_wheel_files(site_packages, &wheel, locks, filename)?; trace!(?name, "Extracted {num_unpacked} files"); // Read the RECORD file. let mut record_file = File::open( wheel .as_ref() .join(format!("{dist_info_prefix}.dist-info/RECORD")), )?; let mut record = read_record_file(&mut record_file)?; let (console_scripts, gui_scripts) = parse_scripts(&wheel, &dist_info_prefix, None, layout.python_version.1)?; if console_scripts.is_empty() && gui_scripts.is_empty() { trace!(?name, "No entrypoints"); } else { trace!(?name, "Writing entrypoints"); fs_err::create_dir_all(&layout.scheme.scripts)?; write_script_entrypoints( layout, relocatable, site_packages, &console_scripts, &mut record, false, )?; write_script_entrypoints( layout, relocatable, site_packages, &gui_scripts, &mut record, true, )?; } // 2.a Unpacked archive includes distribution-1.0.dist-info/ and (if there is data) distribution-1.0.data/. // 2.b Move each subtree of distribution-1.0.data/ onto its destination path. Each subdirectory of distribution-1.0.data/ is a key into a dict of destination directories, such as distribution-1.0.data/(purelib|platlib|headers|scripts|data). The initially supported paths are taken from distutils.command.install. let data_dir = site_packages.join(format!("{dist_info_prefix}.data")); if data_dir.is_dir() { install_data( layout, relocatable, site_packages, &data_dir, &name, &console_scripts, &gui_scripts, &mut record, )?; // 2.c If applicable, update scripts starting with #!python to point to the correct interpreter. // Script are unsupported through data // 2.e Remove empty distribution-1.0.data directory. fs_err::remove_dir_all(data_dir)?; } else { trace!(?name, "No data"); } if installer_metadata { trace!(?name, "Writing installer metadata"); write_installer_metadata( site_packages, &dist_info_prefix, true, direct_url, cache_info, build_info, installer, &mut record, )?; } trace!(?name, "Writing record"); let mut record_writer = csv::WriterBuilder::new() .has_headers(false) .escape(b'"') .from_path(site_packages.join(format!("{dist_info_prefix}.dist-info/RECORD")))?; record.sort(); for entry in record { record_writer.serialize(entry)?; } Ok(()) } uv-0.9.17+ds1/crates/uv-install-wheel/src/lib.rs000066400000000000000000000063521520155276700213310ustar00rootroot00000000000000//! Takes a wheel and installs it into a venv. use std::io; use std::path::PathBuf; use owo_colors::OwoColorize; use thiserror::Error; use uv_fs::Simplified; use uv_normalize::PackageName; use uv_pep440::Version; use uv_pypi_types::Scheme; pub use install::install_wheel; pub use linker::{LinkMode, Locks}; pub use uninstall::{Uninstall, uninstall_egg, uninstall_legacy_editable, uninstall_wheel}; pub use wheel::{LibKind, WheelFile, read_record_file}; mod install; mod linker; mod record; mod script; mod uninstall; mod wheel; /// The layout of the target environment into which a wheel can be installed. #[derive(Debug, Clone)] pub struct Layout { /// The Python interpreter, as returned by `sys.executable`. pub sys_executable: PathBuf, /// The Python version, as returned by `sys.version_info`. pub python_version: (u8, u8), /// The `os.name` value for the current platform. pub os_name: String, /// The [`Scheme`] paths for the interpreter. pub scheme: Scheme, } /// Note: The caller is responsible for adding the path of the wheel we're installing. #[derive(Error, Debug)] pub enum Error { #[error(transparent)] Io(#[from] io::Error), /// Custom error type to add a path to error reading a file from a zip #[error("Failed to reflink {} to {}", from.user_display(), to.user_display())] Reflink { from: PathBuf, to: PathBuf, #[source] err: io::Error, }, /// The wheel is broken #[error("The wheel is invalid: {0}")] InvalidWheel(String), /// Doesn't follow file name schema #[error("Failed to move data files")] WalkDir(#[from] walkdir::Error), #[error("RECORD file doesn't match wheel contents: {0}")] RecordFile(String), #[error("RECORD file is invalid")] RecordCsv(#[from] csv::Error), #[error("Broken virtual environment: {0}")] BrokenVenv(String), #[error( "Unable to create Windows launcher for: {0} (only x86_64, x86, and arm64 are supported)" )] UnsupportedWindowsArch(&'static str), #[error("Unable to create Windows launcher on non-Windows platform")] NotWindows, #[error("Invalid `direct_url.json`")] DirectUrlJson(#[from] serde_json::Error), #[error("Cannot uninstall package; `RECORD` file not found at: {}", _0.user_display())] MissingRecord(PathBuf), #[error("Cannot uninstall package; `top_level.txt` file not found at: {}", _0.user_display())] MissingTopLevel(PathBuf), #[error("Invalid package version")] InvalidVersion(#[from] uv_pep440::VersionParseError), #[error("Wheel package name does not match filename ({0} != {1}), which indicates a malformed wheel. If this is intentional, set `{env_var}`.", env_var = "UV_SKIP_WHEEL_FILENAME_CHECK=1".green())] MismatchedName(PackageName, PackageName), #[error("Wheel version does not match filename ({0} != {1}), which indicates a malformed wheel. If this is intentional, set `{env_var}`.", env_var = "UV_SKIP_WHEEL_FILENAME_CHECK=1".green())] MismatchedVersion(Version, Version), #[error("Invalid egg-link")] InvalidEggLink(PathBuf), #[error(transparent)] LauncherError(#[from] uv_trampoline_builder::Error), #[error("Scripts must not use the reserved name {0}")] ReservedScriptName(String), } uv-0.9.17+ds1/crates/uv-install-wheel/src/linker.rs000066400000000000000000000565341520155276700220560ustar00rootroot00000000000000use std::ffi::{OsStr, OsString}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::SystemTime; use fs_err as fs; use fs_err::DirEntry; use reflink_copy as reflink; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use tempfile::tempdir_in; use tracing::{debug, instrument, trace}; use walkdir::WalkDir; use uv_distribution_filename::WheelFilename; use uv_fs::Simplified; use uv_preview::{Preview, PreviewFeatures}; use uv_warnings::{warn_user, warn_user_once}; use crate::Error; #[allow(clippy::struct_field_names)] #[derive(Debug, Default)] pub struct Locks { /// The parent directory of a file in a synchronized copy copy_dir_locks: Mutex>>>, /// Top level modules (excluding namespaces) we write to. modules: Mutex>, /// Preview settings for feature flags. preview: Preview, } impl Locks { /// Create a new Locks instance with the given preview settings. pub fn new(preview: Preview) -> Self { Self { copy_dir_locks: Mutex::new(FxHashMap::default()), modules: Mutex::new(FxHashMap::default()), preview, } } /// Warn when a module exists in multiple packages. fn warn_module_conflict(&self, module: &OsStr, wheel_a: &WheelFilename) { if let Some(wheel_b) = self .modules .lock() .unwrap() .insert(module.to_os_string(), wheel_a.clone()) { // Only warn if the preview feature is enabled if !self .preview .is_enabled(PreviewFeatures::DETECT_MODULE_CONFLICTS) { return; } // Sort for consistent output, at least with two packages let (wheel_a, wheel_b) = if wheel_b.name > wheel_a.name { (&wheel_b, wheel_a) } else { (wheel_a, &wheel_b) }; warn_user!( "The module `{}` is provided by more than one package, \ which causes an install race condition and can result in a broken module. \ Consider removing your dependency on either `{}` ({}) or `{}` ({}).", module.simplified_display().green(), wheel_a.name.cyan(), format!("v{}", wheel_a.version).cyan(), wheel_b.name.cyan(), format!("v{}", wheel_b.version).cyan() ); } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum LinkMode { /// Clone (i.e., copy-on-write) packages from the wheel into the `site-packages` directory. Clone, /// Copy packages from the wheel into the `site-packages` directory. Copy, /// Hard link packages from the wheel into the `site-packages` directory. Hardlink, /// Symbolically link packages from the wheel into the `site-packages` directory. Symlink, } impl Default for LinkMode { fn default() -> Self { if cfg!(any(target_os = "macos", target_os = "ios")) { Self::Clone } else { Self::Hardlink } } } impl LinkMode { /// Extract a wheel by linking all of its files into site packages. #[instrument(skip_all)] pub fn link_wheel_files( self, site_packages: impl AsRef, wheel: impl AsRef, locks: &Locks, filename: &WheelFilename, ) -> Result { match self { Self::Clone => clone_wheel_files(site_packages, wheel, locks, filename), Self::Copy => copy_wheel_files(site_packages, wheel, locks, filename), Self::Hardlink => hardlink_wheel_files(site_packages, wheel, locks, filename), Self::Symlink => symlink_wheel_files(site_packages, wheel, locks, filename), } } /// Returns `true` if the link mode is [`LinkMode::Symlink`]. pub fn is_symlink(&self) -> bool { matches!(self, Self::Symlink) } } /// Extract a wheel by cloning all of its files into site packages. The files will be cloned /// via copy-on-write, which is similar to a hard link, but allows the files to be modified /// independently (that is, the file is copied upon modification). /// /// This method uses `clonefile` on macOS, and `reflink` on Linux. See [`clone_recursive`] for /// details. fn clone_wheel_files( site_packages: impl AsRef, wheel: impl AsRef, locks: &Locks, filename: &WheelFilename, ) -> Result { let wheel = wheel.as_ref(); let mut count = 0usize; let mut attempt = Attempt::default(); for entry in fs::read_dir(wheel)? { let entry = entry?; if entry.path().join("__init__.py").is_file() { locks.warn_module_conflict( entry .path() .strip_prefix(wheel) .expect("wheel path starts with wheel root") .as_os_str(), filename, ); } clone_recursive(site_packages.as_ref(), wheel, locks, &entry, &mut attempt)?; count += 1; } // The directory mtime is not updated when cloning and the mtime is used by CPython's // import mechanisms to determine if it should look for new packages in a directory. // Here, we force the mtime to be updated to ensure that packages are importable without // manual cache invalidation. // // let now = SystemTime::now(); // `File.set_modified` is not available in `fs_err` yet #[allow(clippy::disallowed_types)] match std::fs::File::open(site_packages.as_ref()) { Ok(dir) => { if let Err(err) = dir.set_modified(now) { debug!( "Failed to update mtime for {}: {err}", site_packages.as_ref().display() ); } } Err(err) => debug!( "Failed to open {} to update mtime: {err}", site_packages.as_ref().display() ), } Ok(count) } // Hard linking / reflinking might not be supported but we (afaik) can't detect this ahead of time, // so we'll try hard linking / reflinking the first file - if this succeeds we'll know later // errors are not due to lack of os/fs support. If it fails, we'll switch to copying for the rest of the // install. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] enum Attempt { #[default] Initial, Subsequent, UseCopyFallback, } /// Recursively clone the contents of `from` into `to`. /// /// Note the behavior here is platform-dependent. /// /// On macOS, directories can be recursively copied with a single `clonefile` call. So we only /// need to iterate over the top-level of the directory, and copy each file or subdirectory /// unless the subdirectory exists already in which case we'll need to recursively merge its /// contents with the existing directory. /// /// On Linux, we need to always reflink recursively, as `FICLONE` ioctl does not support /// directories. Also note, that reflink is only supported on certain filesystems (btrfs, xfs, /// ...), and only when it does not cross filesystem boundaries. /// /// On Windows, we also always need to reflink recursively, as `FSCTL_DUPLICATE_EXTENTS_TO_FILE` /// ioctl is not supported on directories. Also, it is only supported on certain filesystems /// (ReFS, SMB, ...). fn clone_recursive( site_packages: &Path, wheel: &Path, locks: &Locks, entry: &DirEntry, attempt: &mut Attempt, ) -> Result<(), Error> { // Determine the existing and destination paths. let from = entry.path(); let to = site_packages.join( from.strip_prefix(wheel) .expect("wheel path starts with wheel root"), ); trace!("Cloning {} to {}", from.display(), to.display()); if (cfg!(windows) || cfg!(target_os = "linux")) && from.is_dir() { fs::create_dir_all(&to)?; for entry in fs::read_dir(from)? { clone_recursive(site_packages, wheel, locks, &entry?, attempt)?; } return Ok(()); } match attempt { Attempt::Initial => { if let Err(err) = reflink::reflink(&from, &to) { if err.kind() == std::io::ErrorKind::AlreadyExists { // If cloning or copying fails and the directory exists already, it must be // merged recursively. if entry.file_type()?.is_dir() { for entry in fs::read_dir(from)? { clone_recursive(site_packages, wheel, locks, &entry?, attempt)?; } } else { // If file already exists, overwrite it. let tempdir = tempdir_in(site_packages)?; let tempfile = tempdir.path().join(from.file_name().unwrap()); if reflink::reflink(&from, &tempfile).is_ok() { fs::rename(&tempfile, to)?; } else { debug!( "Failed to clone `{}` to temporary location `{}`, attempting to copy files as a fallback", from.display(), tempfile.display(), ); *attempt = Attempt::UseCopyFallback; synchronized_copy(&from, &to, locks)?; } } } else { debug!( "Failed to clone `{}` to `{}`, attempting to copy files as a fallback", from.display(), to.display() ); // Fallback to copying *attempt = Attempt::UseCopyFallback; clone_recursive(site_packages, wheel, locks, entry, attempt)?; } } } Attempt::Subsequent => { if let Err(err) = reflink::reflink(&from, &to) { if err.kind() == std::io::ErrorKind::AlreadyExists { // If cloning/copying fails and the directory exists already, it must be merged recursively. if entry.file_type()?.is_dir() { for entry in fs::read_dir(from)? { clone_recursive(site_packages, wheel, locks, &entry?, attempt)?; } } else { // If file already exists, overwrite it. let tempdir = tempdir_in(site_packages)?; let tempfile = tempdir.path().join(from.file_name().unwrap()); reflink::reflink(&from, &tempfile)?; fs::rename(&tempfile, to)?; } } else { return Err(Error::Reflink { from, to, err }); } } } Attempt::UseCopyFallback => { if entry.file_type()?.is_dir() { fs::create_dir_all(&to)?; for entry in fs::read_dir(from)? { clone_recursive(site_packages, wheel, locks, &entry?, attempt)?; } } else { synchronized_copy(&from, &to, locks)?; } warn_user_once!( "Failed to clone files; falling back to full copy. This may lead to degraded performance.\n If the cache and target directories are on different filesystems, reflinking may not be supported.\n If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning." ); } } if *attempt == Attempt::Initial { *attempt = Attempt::Subsequent; } Ok(()) } /// Extract a wheel by copying all of its files into site packages. fn copy_wheel_files( site_packages: impl AsRef, wheel: impl AsRef, locks: &Locks, filename: &WheelFilename, ) -> Result { let mut count = 0usize; // Walk over the directory. for entry in WalkDir::new(&wheel) { let entry = entry?; let path = entry.path(); let relative = path.strip_prefix(&wheel).expect("walkdir starts with root"); let out_path = site_packages.as_ref().join(relative); warn_module_conflict(locks, filename, relative); if entry.file_type().is_dir() { fs::create_dir_all(&out_path)?; continue; } synchronized_copy(path, &out_path, locks)?; count += 1; } Ok(count) } /// Extract a wheel by hard-linking all of its files into site packages. fn hardlink_wheel_files( site_packages: impl AsRef, wheel: impl AsRef, locks: &Locks, filename: &WheelFilename, ) -> Result { let mut attempt = Attempt::default(); let mut count = 0usize; // Walk over the directory. for entry in WalkDir::new(&wheel) { let entry = entry?; let path = entry.path(); let relative = path.strip_prefix(&wheel).expect("walkdir starts with root"); let out_path = site_packages.as_ref().join(relative); warn_module_conflict(locks, filename, relative); if entry.file_type().is_dir() { fs::create_dir_all(&out_path)?; continue; } // The `RECORD` file is modified during installation, so we copy it instead of hard-linking. if path.ends_with("RECORD") { synchronized_copy(path, &out_path, locks)?; count += 1; continue; } // Fallback to copying if hardlinks aren't supported for this installation. match attempt { Attempt::Initial => { // Once https://github.com/rust-lang/rust/issues/86442 is stable, use that. attempt = Attempt::Subsequent; if let Err(err) = fs::hard_link(path, &out_path) { // If the file already exists, remove it and try again. if err.kind() == std::io::ErrorKind::AlreadyExists { debug!( "File already exists (initial attempt), overwriting: {}", out_path.display() ); // Removing and recreating would lead to race conditions. let tempdir = tempdir_in(&site_packages)?; let tempfile = tempdir.path().join(entry.file_name()); if fs::hard_link(path, &tempfile).is_ok() { fs_err::rename(&tempfile, &out_path)?; } else { debug!( "Failed to hardlink `{}` to `{}`, attempting to copy files as a fallback", out_path.display(), path.display() ); synchronized_copy(path, &out_path, locks)?; attempt = Attempt::UseCopyFallback; } } else { debug!( "Failed to hardlink `{}` to `{}`, attempting to copy files as a fallback", out_path.display(), path.display() ); synchronized_copy(path, &out_path, locks)?; attempt = Attempt::UseCopyFallback; } } } Attempt::Subsequent => { if let Err(err) = fs::hard_link(path, &out_path) { // If the file already exists, remove it and try again. if err.kind() == std::io::ErrorKind::AlreadyExists { debug!( "File already exists (subsequent attempt), overwriting: {}", out_path.display() ); // Removing and recreating would lead to race conditions. let tempdir = tempdir_in(&site_packages)?; let tempfile = tempdir.path().join(entry.file_name()); fs::hard_link(path, &tempfile)?; fs_err::rename(&tempfile, &out_path)?; } else { return Err(err.into()); } } } Attempt::UseCopyFallback => { synchronized_copy(path, &out_path, locks)?; warn_user_once!( "Failed to hardlink files; falling back to full copy. This may lead to degraded performance.\n If the cache and target directories are on different filesystems, hardlinking may not be supported.\n If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning." ); } } count += 1; } Ok(count) } /// Extract a wheel by symbolically-linking all of its files into site packages. fn symlink_wheel_files( site_packages: impl AsRef, wheel: impl AsRef, locks: &Locks, filename: &WheelFilename, ) -> Result { let mut attempt = Attempt::default(); let mut count = 0usize; // Walk over the directory. for entry in WalkDir::new(&wheel) { let entry = entry?; let path = entry.path(); let relative = path.strip_prefix(&wheel).unwrap(); let out_path = site_packages.as_ref().join(relative); warn_module_conflict(locks, filename, relative); if entry.file_type().is_dir() { fs::create_dir_all(&out_path)?; continue; } // The `RECORD` file is modified during installation, so we copy it instead of symlinking. if path.ends_with("RECORD") { synchronized_copy(path, &out_path, locks)?; count += 1; continue; } // Fallback to copying if symlinks aren't supported for this installation. match attempt { Attempt::Initial => { // Once https://github.com/rust-lang/rust/issues/86442 is stable, use that. attempt = Attempt::Subsequent; if let Err(err) = create_symlink(path, &out_path) { // If the file already exists, remove it and try again. if err.kind() == std::io::ErrorKind::AlreadyExists { debug!( "File already exists (initial attempt), overwriting: {}", out_path.display() ); // Removing and recreating would lead to race conditions. let tempdir = tempdir_in(&site_packages)?; let tempfile = tempdir.path().join(entry.file_name()); if create_symlink(path, &tempfile).is_ok() { fs::rename(&tempfile, &out_path)?; } else { debug!( "Failed to symlink `{}` to `{}`, attempting to copy files as a fallback", out_path.display(), path.display() ); synchronized_copy(path, &out_path, locks)?; attempt = Attempt::UseCopyFallback; } } else { debug!( "Failed to symlink `{}` to `{}`, attempting to copy files as a fallback", out_path.display(), path.display() ); synchronized_copy(path, &out_path, locks)?; attempt = Attempt::UseCopyFallback; } } } Attempt::Subsequent => { if let Err(err) = create_symlink(path, &out_path) { // If the file already exists, remove it and try again. if err.kind() == std::io::ErrorKind::AlreadyExists { debug!( "File already exists (subsequent attempt), overwriting: {}", out_path.display() ); // Removing and recreating would lead to race conditions. let tempdir = tempdir_in(&site_packages)?; let tempfile = tempdir.path().join(entry.file_name()); create_symlink(path, &tempfile)?; fs::rename(&tempfile, &out_path)?; } else { return Err(err.into()); } } } Attempt::UseCopyFallback => { synchronized_copy(path, &out_path, locks)?; warn_user_once!( "Failed to symlink files; falling back to full copy. This may lead to degraded performance.\n If the cache and target directories are on different filesystems, symlinking may not be supported.\n If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning." ); } } count += 1; } Ok(count) } /// Copy from `from` to `to`, ensuring that the parent directory is locked. Avoids simultaneous /// writes to the same file, which can lead to corruption. /// /// See: fn synchronized_copy(from: &Path, to: &Path, locks: &Locks) -> std::io::Result<()> { // Ensure we have a lock for the directory. let dir_lock = { let mut locks_guard = locks.copy_dir_locks.lock().unwrap(); locks_guard .entry(to.parent().unwrap().to_path_buf()) .or_insert_with(|| Arc::new(Mutex::new(()))) .clone() }; // Acquire a lock on the directory. let _dir_guard = dir_lock.lock().unwrap(); // Copy the file, which will also set its permissions. fs::copy(from, to)?; Ok(()) } /// Warn when a module exists in multiple packages. fn warn_module_conflict(locks: &Locks, filename: &WheelFilename, relative: &Path) { // Check for `__init__.py` to account for namespace packages. // TODO(konsti): We need to warn for overlapping namespace packages, too. if relative.components().count() == 2 && relative.components().next_back().unwrap().as_os_str() == "__init__.py" { // Modules must be UTF-8, but we can skip the conversion using OsStr. locks.warn_module_conflict(relative.components().next().unwrap().as_os_str(), filename); } } #[cfg(unix)] fn create_symlink, Q: AsRef>(original: P, link: Q) -> std::io::Result<()> { fs_err::os::unix::fs::symlink(original, link) } #[cfg(windows)] fn create_symlink, Q: AsRef>(original: P, link: Q) -> std::io::Result<()> { if original.as_ref().is_dir() { fs_err::os::windows::fs::symlink_dir(original, link) } else { fs_err::os::windows::fs::symlink_file(original, link) } } uv-0.9.17+ds1/crates/uv-install-wheel/src/record.rs000066400000000000000000000006761520155276700220440ustar00rootroot00000000000000use serde::{Deserialize, Serialize}; /// Line in a RECORD file /// /// /// ```csv /// tqdm/cli.py,sha256=x_c8nmc4Huc-lKEsAXj78ZiyqSJ9hJ71j7vltY67icw,10509 /// tqdm-4.62.3.dist-info/RECORD,, /// ``` #[derive(Deserialize, Serialize, PartialOrd, PartialEq, Ord, Eq)] pub struct RecordEntry { pub path: String, pub hash: Option, #[allow(dead_code)] pub size: Option, } uv-0.9.17+ds1/crates/uv-install-wheel/src/script.rs000066400000000000000000000167371520155276700220770ustar00rootroot00000000000000use configparser::ini::Ini; use regex::Regex; use rustc_hash::FxHashSet; use serde::Serialize; use std::sync::LazyLock; use crate::{Error, wheel}; /// A script defining the name of the runnable entrypoint and the module and function that should be /// run. #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub(crate) struct Script { pub(crate) name: String, pub(crate) module: String, pub(crate) function: String, } impl Script { /// Parses a script definition like `foo.bar:main` or `foomod:main_bar [bar,baz]` /// /// /// /// Extras are supposed to be ignored, which happens if you pass None for extras pub(crate) fn from_value( script_name: &str, value: &str, extras: Option<&[String]>, ) -> Result, Error> { // "Within a value, readers must accept and ignore spaces (including multiple consecutive spaces) before or after the colon, // between the object reference and the left square bracket, between the extra names and the square brackets and colons delimiting them, // and after the right square bracket." // – https://packaging.python.org/en/latest/specifications/entry-points/#file-format static SCRIPT_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^(?P[\w\d_\-.]+)\s*:\s*(?P[\w\d_\-.]+)(?:\s*\[\s*(?P(?:[^,]+,?\s*)+)\])?\s*$").unwrap() }); let captures = SCRIPT_REGEX .captures(value) .ok_or_else(|| Error::InvalidWheel(format!("invalid console script: '{value}'")))?; if let Some(script_extras) = captures.name("extras") { if let Some(extras) = extras { let script_extras = script_extras .as_str() .split(',') .map(|extra| extra.trim().to_string()) .collect::>(); if !script_extras.is_subset(&extras.iter().cloned().collect()) { return Ok(None); } } } Ok(Some(Self { name: script_name.to_string(), module: captures.name("module").unwrap().as_str().to_string(), function: captures.name("function").unwrap().as_str().to_string(), })) } pub(crate) fn import_name(&self) -> &str { self.function .split_once('.') .map_or(&self.function, |(import_name, _)| import_name) } } pub(crate) fn scripts_from_ini( extras: Option<&[String]>, python_minor: u8, ini: String, ) -> Result<(Vec {% endblock %} uv-0.9.17+ds1/docs/.overrides/partials/000077500000000000000000000000001520155276700175665ustar00rootroot00000000000000uv-0.9.17+ds1/docs/.overrides/partials/integrations/000077500000000000000000000000001520155276700222745ustar00rootroot00000000000000uv-0.9.17+ds1/docs/.overrides/partials/integrations/analytics/000077500000000000000000000000001520155276700242635ustar00rootroot00000000000000uv-0.9.17+ds1/docs/.overrides/partials/integrations/analytics/fathom.html000066400000000000000000000001271520155276700264270ustar00rootroot00000000000000 uv-0.9.17+ds1/docs/assets/000077500000000000000000000000001520155276700151715ustar00rootroot00000000000000uv-0.9.17+ds1/docs/assets/favicon.ico000066400000000000000000000163561520155276700173250ustar00rootroot0000000000000000¨6 ¨Þh†(0` >1JggR’ˆQŽ…MtrA/>U¤•JegLonEFPaéÇQ†N{MvsB7Edÿ×FMUb÷ÑC:GP„}=.\ѵFSY?#6NxtFKSJacW²ž      ( @_ãÃV¯U¤•C:FYÁªHY^>0C;HEFPB7Edÿ×B3BHX]GTZSšDALGW\=.B9FLmlX·¢_çÅZɯHV[A0?                                    ( MtrEGQ?);C8FR”‰C;HB7Edÿ×PŒƒN}xJceQ“ˆEEODAL@'9GW\=.I^aFOVO†~GUZ      uv-0.9.17+ds1/docs/assets/github-add-environment.png000066400000000000000000002454251520155276700222650ustar00rootroot00000000000000‰PNG  IHDR'Cž÷uf€IDATx^ìÝw\YØ7ü{Uz{ok[u-[tuÕÕÕQEDP@E±¡bï bÁ† {ņØ6¬¨Ø;âz?Ïç}ïò{æš$fHèêõÇ÷£L93™I&çüræÌ¤|þO0ÆcŒ1ÆcŒ”ÿN`Œ1ÆcŒ1ÆËON0ÆcŒ1Æc¬@q8ÁcŒ1ÆcŒ±ÅácŒ1ÆcŒ1Æ ‡Œ1ÆcŒ1Æ+PN0ÆcŒ1Æc¬@q8ÁcŒ1ÆcŒ±ÅácŒ1ÆcŒ1Æ ‡Œ1ÆcŒ1Æ+PN0ÆcŒ1Æc¬@q8ÁcŒ1ÆcŒ±ÅácŒ1ÆcŒ1Æ ‡Œ1ÆcŒ1Æ+PN0ÆcŒ1Æc¬@N(R¿àÍûD?ƒ)3çÁÕËí;÷Èsº»‹Û¢mÒ¶ih_¤ûÇcŒ1ÆXaPPõfÆËO¹ÕV7(œ  Üø þó‚0möœ8s¯ßÆË–Ë+´-Ú&m›öö%;/:/<}ñÑ7cpêÌyÑõ[1â4érŒ1ÆcìÛ§Yožê?_¬°?yö ïcì›C×7ºÎÑõ.»mu½Ã õ–‚ýáÇdóóííKv^tnˆÿ˜Œõ¡[ЮCg3±ÄÅÌ´¢yÿttĆMÛÄu¤å0ÆcŒ±o‹f½yÏþÃø„„¤$¥|fŒ±o]çèzG×½ì´Õõ'¨k% …!˜P£}¡}¢}“ÎË+oÞÅcð0?˜[•Y±°.‰á~c—ûËcŒ1Æò—ºÞLtª¬Kç3ÆØ·Œ®{tý3´­®w8A]4(ýN/h´O´oÒé¹-Q‘Š€(^²œ,t0TÉ2´t…˜.I·ÃcŒ1ƾnê®ÍL0ƾWtýSßÒ&§‹Þá nAã=H§4Ú'Ú7éôÜtûn,ê7úU2äT£_š!öáSÙöcŒ1ÆØ×‹ê¦GNDʦ3ÆØ÷„®ƒ†´Õõ'hôÍüüR_´O´oÒé¹%t˘YÚÉ‚…Üb]¼4öì;(Û.cŒ1Æû:QÝôÅ«·²éŒ1ö=¡ë !mu½Ã zDˆtZa‘Wû¶|eˆ,LÈ+Ûwí•mŸ1ÆcŒ}}¨njÈ pŒ1ö-¢ë !mu't8qê,Š[ÈB„¼bjYW®Ý”ícŒ1ÆûºäEÝ”1ƾF†\9œÐâþƒ'°+U^ L>{÷‚ϲyúò4 »Ã 0h¹l^Å*?âÅë‚ëxêl&L›-O}в´Ž´ÆcŒ±ïYn×McìkeÈõà -\Ý=eÁyöòMÚ24H¦C—îióhpK'ç^â; #FE·½ðËï-ÒæÿÝÞ×oÅdØõ–ncèðѲýÉk“>aV@ x/Ü[‹çÿµO01@+šGËв´­KeHËeŒ1ÆûåvÝ”1Æ2S˜¯9†ì‡—¯Þ¤t¹Ê²eÉÕè[ˆÿ˜,›®)êr´l©Y·l;äáãg²eó … ÃÇLÆòsñüfãÁÿz#öôCËÒ:´.•ÁEá~ôNŸ»$›ÎcŒ±¼““ºé›w ?v+Ö†büTŒ7 —®Âí{eË~’R>ãlÔUŒ™4»÷–õäÍ ­CëRT–´|ƾ9¹æä5Cö-O‰«7î $t[ŽDßÌØË 3†ì[V4{Ch*[±šlÙœªõÓϲí¾ÞƒdËæêõ@áõ† °áà)ê…zQÐ:´.•AeIËgçÃÇdtv…ïÀŒ1ÆØ·,»uÓý‡£kOY[mÔÄéˆ{ÿQ¶Þ·ŒB÷¾ƒqòÌ…l 2JëкT•%Ï”/¯€‹c_\L•Íc…Ÿ!לäOÿbãÖÝxøô…l^^0dßò$œ˜0mŽìbj¨)³æËÊÕ…–—NËê± 4½Š{/['»>$$ÉÊ×ô.>ï¿xh¼:vÏÿÛ_ cîþýqá.ÆeŽ–¹÷ÿ+×Qö ðËú¦Æ øôó[ÙÀ¤Å"Üþ¤e¾Â¡ŒIxH‘ÍË+„ …‰ þ’ÑGÒÍübFŸ?kü:;IZ–É{ ìp/"Â5 ˆv&Jçg½¬âyú7( ó’Ðwû $ËÖÓΰ×ÿ çÛ£‚M)”o»g“¤óYn3ìüœwñI;i&Þ ßqÒyŒ}O²S7]º]\ϱG¬Ùˆk7cÄ={‰CGNÂwô$q¾—oŽŠN]äuï ºÏEx¼|½‚@½(\N7•AeI§«%><‡Í§Ÿéý½ù­ápâëfÈ5'æþcqù½ûãÁ“¼( Ù·< 'ž¿z+^`¥½!ôµnÓƒ’Cö-3Ú©Tûéç&HHʽ†%¥¸Í[µ•mGmÛÎ=²ur hIãFÄþO<ü_oÜý?ý1lHG4¯Õmý…¿~Ö®MÿÄeFŒpÀýÿ«¼½ƒÊ ²¨LévòÜûKXŽ»9 d¾òp‚s,‘MgÚŽÆŸöÀA;íË&ž›€Z¦ô:,Qux$>ÊÖÓΰ×ÿk:Û)·mÑ Ë_þK3Œaç§à¬Ù°EüNÙ¸U6Oo©‰¸¾{.Üœzaȶ粆‚"ñ9NïX… ÃÃÙÙ »{ ç i˜»ã:^¤h)±`hÝôdäEq^^>¸÷à©l¾ÚÒ•ëÄå|ÇLÎV/µ]{öcÛn²ký}aïèƒñk÷©¦ ¢«¯Wè¶Œœ¼V5*ƒÊ’NWúŒëk}áp^ïïÍo ‡_7C¯9KV(¯%®žñøù+ÙüÜdȾåI8‘ßrkßÚ¶ë( ÔÎ_¼"[>§bî?D#sÙ¶HoÏþ²ås7Ø2ö¿½ÅÛ4.¼ñDóZ­PDz”øõí´£yu,Z eÝÖ¸øÖS\—Ê ²rë\âÃþ([m"’åórä+'^Å}€}WWD]Íéãi“pÐçG3®ŽGògß3¥ˆÆ„Ÿ­ðƒe/lÊ´án¸ÂÑøÓ8h§cYÅc„Œ†÷ðØ«ÿ80†½þ/xµãûblÈe¼J•Îg¹Í°óS0è—\ªø»y ÿ}Ÿ([&KIÏphátqîƒ.Ž®òp"5‘>pp)Á;°ýàq8†¥Ó|Ñ©‹nŒE¢´LÆ €!õ¡I)èÑÛ[ì1ñàÉsÙ|©¹‹–‰åï ;$›g¸/x6 Æ`Cì¿Zæ4)×Ë „›”W¢ÿÓ4š×ÀîO´kÒQo½ÄuÅ2„²rë\è/Õ…qU'4ѯ½û Éù/ ï÷ý¼~($áD™±øÑÄ,o‰; ð[7þ4‡’}±+Óר#œÈ¦ôƯ ~Ÿw_ök5+X_C8±jýf8tw¡©'¤t™L%ÞÁºQ^èØ{6DÂäîZ‰ÏÔs"¯“$×¶OÏ:ÂöC·á÷ž`…€!õ!º “–_¼S[埩ä›êÛ&Fàù§7Ø1Î îÁ7ðäÒL6]œz¡‹ÇhL 9‡Xém©‰¸¾†ùÀ©»›r¹µ‘òå4è,?ãîÖñpôÝŠ[:z}h++ùÙIÌÐÿtÖ¼µÅ “©¼ÿ‚W7aáD?ôtqƒCÏè71[/Åe†&¾¸Œõs'Ãý7:vŽK_ ó_#5æL|Š#ë`Hß¾èäÔ=Í@`x,Þiÿ©É¸wj¦L@?/Oq9çSƒ¸´å>ãÖ†‘èä† qpÉTôîé†N7àšêx||~¡ §¡Ÿ‡:uïﱘ¸å&ÞÓ¾R8ÑÕ Î<ÀáÕ³1À£œú çÐÙX~â9´¼>Vxh{oëCPИ,yP²oN¨Ü{ðX¨Mœ¢ûþ´œš3o‘l{j4.…tùÜDǵ¤'o<2œ Û6êoFeZŠêÙ¶@MÓæ"ú¿æô¿·AÔ;/q]*ƒÊÒÿ\_†Öc´›=×® [XUh„¿oÄeÍ{ãocØhR­Ì̬`jWuÿꃉûž"YyíjÀÔHó¸Y T¿ášŠsÁ¬ÙÜxw«üª%ŠÃ¬Î8œ ãc,ö C§MP¥L ˜X•CÕf½1-üEz¥_ïpâ3 „G›†(ck#ó’(õcsØû„à¼êµˆá„iôÝqëF:¢~ÅR05/…r?wÁðM1ø +3g %Þ¯*nˆ×W6bПU`œáøš¡hyì£JDr$†U·Ä¦ 0꜠[“j°4+>ûRxq2ꈷØ UÐÓôFâ~ô²£Cæ°ó8ˆxÕôø»û1ÙýoÔªP &¦Ö0/U ;ø`êæëx•š„;&¡]µâbc\sL»nÁ[UŠ7—±jtOüöcEXZ‡u¥Æhë„#O%£s'=ÄÎ ÎhPIØ–ETk= «"ÑÆ‚Ê̺ñ—ÖP4o‹€;°{‚#j—±ƒ‰ueÔu-wiËvÒ‡¢åbã©åpû½¬„×b[­ÜÏãEZ%D{8¡½›Š‡GÁ«]”·+#S¡¼ÊѺ÷T¬ˆ|-î“f8ÑjÁlß]<>¦¥Q¡IŒÝ›v®´oû ž®êSáõ˜þ³wïìÅØnÍPIØ—:íG`ÕÕ„Œ¿d%=ÀÚ}¬*¢v;?¬½z“›XëuDŠ—8ºdì›ÔB +[˜•Þ7ü|ñ½¸-E\ú ïSñ˜VèƒÐçªFíÇ(Œmh£œ^ÂA÷è=’Œ˜ðåîú×ýQ(Ϧū¢ö_}1ãàcŠYÆ×zçævŒìÔemla*l¿q·©Ø›‚WWû]C”¶¦—¬ßz-@Ä uå_]†Œ~šŒÓONc¦Ëï(km³ÒõÑzÐzD}Ho€k?¯D˜ „sÛåKØ Ç±ª5uŘͷñNã8eþùÒr\³@ƒiE^¸‚a±|õzqœ¨NÝݱ|Õzq>ýK½'h:ͧåÎ^¼"®'-+Mj<ÎnÙŒ#O>Añá&tÓNh•òk‡¹ÂaÜA<ÉÆëa,·é_úOøÏ—¿pùºlž.>¾cÄuèÉÒy†ÑNÐô3ѹ«/VÜÊøúñÊjôrêb H¥p¢ìû‡Wßɘ¿å09‚àìè ×yçð2í3™‚[[&ÂÑiü–ïÃÁ“g¶iú9»¢çœÓx®ã³«ï±T¼9©î>˜{.Qk¯ ]e)^"úò!L÷è ‡ ;pþêM±çéÝ·t<¾àí¥µè×Ý=F-dž}'pðÀNðŽÙ`Ì:ùVç¶R±X3Ô ÞX½ÿ Ž?Ž›×bÚøå­º¾§<Çž)ý`ßk¶ÑǺhœ=1jïsk}NÅðy°iÿi;y«§ AGÇÂëMRíƒ2œp舠Y>è6x‚Bw û|C¼Î+^žÁtOWtp‡Yk÷bûî]X¹d(¯³ÊpÂÃkÚl ?ƒÃ‡vaޝ'þq.¼ôïÉòŸ¶÷¶¾4Š—oroŒE5Cö-O‰WoãÅuN¼~/+WCöM—3ç.fhðhZ¾R¿$;;¶lß%Ûžš!O,ÉÍp‚nÍP‡tëõ’ ¢‡C;Ìê&rîÐNœFóêÿ3-œoë08œøW¹¢þß0vþ¬ß¶ÃÛ¡¬‰júU5‚°`MÙþ‚žÓC°aË,[4ý;wÀ°}Âû#õ-¢OÁÔv%Q¬œ æ…ŸÄá£'qâ5J”á„iõÞäTvõ1hÊlLYzBYqM<ƒQ4G×as°h턆Ìsý’(jçˆàGª/g=ɤ˜%hic íÆ`þºíØŒ~}ÐÚy)¢U¿6(É2¨Q»6ju‹¹«·`ÝÊ9poREÍÂ/2½1›S±ž‰çáöÝœ=rìÀÀª(ªå}) 'Œlðkoo4´RÝ¢dò|O2¬QƒjX©‚sñv'åÿ-PÆãâ×0¾•l_ˆ:œP¼= ¿Æ%•ëYÀÔVhð+·cZ{0ö¼TU„ÐÁ! `’!t1G± 5PÖ”–—6þäÒŠFÅQ£~=I@fã‡",N¹=ƒŽƒf8aû#*–²ÈøzK£U`Œê—möFl⥙hh©:?tlÕ·“•†}ÈK±R“¾ž%ÊU¯)yMÂ6ÌÁï´úW$mÛNolµm€ÚU­%A’pŒ«xc×uƒ;áôœ‡òuPÙZ¿ó’úÛ<R•!¬kU–æÊ÷Nëfw†*í_ðlg_T 7FV¨1ä„ð~IEô‚¶°¤õ„cÐr‘ú˜~ÄÁAµQ,Ã~«_ 9¢nh¾Öú¨UEþZÍê´Bã²’ó'¼ŸK8mÂc±ž^Æ&5Q§n)IÂ{ßy ª®=ÚΫX]Ý]¸n*ÏgÓ’°¶V}ŽŒË¡õ¢[Ê@%«Ï—ô¸êáà‘“âu†Ðmã§ÎQúþ§ùô/ýMAì%ÌW/KG”–¥âý ŒÏ,œHMÁÛ¸÷xùê%îFG"tÎpté5«¯én”0–Ÿô¯)Lj¢åï?Êú–µé³Šëܸs_6Ï0ºÂ úžÁ4WW¸/¿©q«C Î-ˆ® q„TU8ÑÞÑK®iÔeR“pnétè:kï+à ū£×ÃýÖÆh„½ÿ"vçD8tŽ•w´?æS¿c©À¥¾pšpt„™–¥¸¥ýzÊoëøôë|]ÑqÈFDköîP<Ö±½ÑÁk.èèõ¡xy~Nns@–Kç“w‘AèÑÕç5®]©ïp`º':xg>àtÂ9Ìpí ×e7TßaÊp¾³ ºŽà ë*p~Ù tè>ëïjï +†]\Ð™Ž¡F½WñBxÝ]áµö^æßɬ@é|oë‰WLexxKû.Ï-†ì[ž„“g¤UD²‹.ºÒru¡å¥Ó µkï~I%2ÝÒàÕ²åsËŽÝa²í©í?"[>7ÑqÓN”UöŽpíÜ‹Wt¹tj'NËpB‹Ôçjg ã†3EÝÏјØÐV®»3éY l$“ÝÖ¡ 'Œ„Ê·Í_ • ]O.þØT6)…n[T =É›]afö;¦DkÿR%b8!4”-Ú.Ãmî‘ɱËÐÒÒ µÆ\̲k ¾¨+5õœN7”âS*^®ë¦l<W‡wx’øä¤”•_ êpBõ~-bSö ¿ÉËpðÉ¿5ÊiÙÚâ²Öh<å*Þ)ñäÎ9l^0+/)¿D“S±Ç«¢20±tņøTåþ|¢Æîg\šö»ØH-bZî›å~A\Ô|´²ˆFÖøyêuñK5ùA0Z«‚”b•:aÊŽ38ud3F¶ª  c²n§7…í Ÿ‰¡›Îáò•“r­§ìibT\xÍ•¿Dp4ý?Y¢r·ÅØu §·ŒÆ¯ÅUû\u(~̸læáÄ¿ˆYØ&T¦ÉOþ‰ß!æâNƱ÷Òõ„òÌêÀiaÎE_ÂV¿–°ÿÂþ =¥ª´iÛ¶Fc»õRpÀä]Q¸rùfØWV[£2è±]9Â|òÃUh+žsUuŒݑ8!œ‡–úŸ‡Âg¶ª:Ø¡¡o פW'áׄzØŸ·¿Wà}v?½ÀúÊ}(bÙ ÃÖ S ÌaÙb®h\;’îíÀäiëqðÚS¼JLÆãc“𫸟°ë³_u-Ò|­ºtƒÿÁ«¸tr5œ«§‡&5û 0⢎¡seåg¥ˆu¬{+/ôž7–»ŽËg6Á»q e¦M0á²òÞ_ùy¥_ w¡‡€XÀ¶µ?N½Éϰ½o= ç¬hiw„¾ù¢×çËPôÙ7Å_Û†ò“Î×Dói9Z^ß§eN(Þ œnª:ƒP™vˆm7$=s+@†Ô‡¨‡-OOçÎÓeôÄéâ:4ê¾tžat‡Ô›,2pì=–ã¬úÖÁÄ+˜×Ç=D)¯‡ªp¢Cßµ¸,¹•"ñæzx8ö¨}Ô»@øN>2ºÇЉø \_ÕÞÞE¿®½àfà8’ïÃp—á¾™ù5MgY:‰ä'aêäŠA[4¾¿E_ðúðltr„ k:êI7±˜nˆWã´Œeñ gû¡CŸå8ý>ýxØSб«BÄ^}ÒõTRîaå@Wtž{AU¶*œpŠ¥Ñ’^Š;XîÝ“"4z²d¤ 'ÔçKs]:6®B½à’–×À ïm.[#–ãå3<óžŽ2dßò$œ¸zãüÆOÅÈqÙ3jÂ4œ9IV®.†ì›.{ö+ŽÚÌðŸ'[>·¯Z+ÛžÚÞý¹1Бn™…në0k.Ò¼­#O‰Ï)óª£j#q„ ©ï°Éµ"ŠoŠþk¢ðDë˜Y„&u1ô„~½’ï-B3s[´ ~¥¼(KÉOñ$ö!bÒ<Ç«$áb~eZX£bÇÙØ£½r¬ 'lÐz‰äUbß Å=ht™Ï>c‚Æš 1'¤ó²#nC÷´pB6æ„F8QĪ%¦DeüÎFyrìRüi¡œfRå [y ÷e’ðþè§'$cN¤ÜÁŒßèV37™«iP68+‘6i³±ÂyŒÛÜKÙØ„'iû•xi:ê©pÊÆ_*žß¹‚È —Òœ~&Þ—™ÞP´@)áÜÑ4*Cñ,íT n«;Ƈ á„YKÌJûIˆ!ª_ôMÁD±Áª- ÐÞˆ}ÚÖâk¶D©fƒ°àà]ÄI·ôõÌan¢úu_ð~;ºÙ*÷Ó¬ûvÕkÕ¶mÍÆvÆ`!ሯ²ç‚p ZªŽùÛ­niç¡õ’ôã’xeèu>áøð:Êcbá€%Ï(¤¢2þÅý vbSĦ6~P–›ü`-ìK+ cseO‡"VÍ1åR×õ ¬Âò¦íV©~‘Ó|­6hš6FG*ÎOl #šnTÿ¬y­º|Â1_Õ¾šÿ…¹÷”]…ÓÊ0*!é•ò·;=QÂXõžQ]3´×÷»-GËKçë’U8‘¢ˆÃKWqöÜ9 ÛŠ¹cÂÁi &ì{•gV(R¢hù-;÷ÉæiCºmªƒcOq0Mé|ÃdNü'>F¯CŸ®}1ë´òVãç–¢G×X ¾vªÂ‰Ž4Îä{Eñ&c„Ïq¿õ±Â5ë3‚=ŠZi ”²<–©ñ8à —€³x­£áeY:‰Ä+«áêèŽi'åÇ9ñú:¸;ºaÒQ]·aÁ‡»˜3Ì „FßXy8/Õ!NjvMè¥åX¨8Æõ_©Éˆ9¾³'GOtéî‡n½`O=ÒöYNt›ýïÕ߉*I‘˜ÖÝ=—]×ùc˜r@LOÌ9' 6b<ÀŽó£øúZˆé|o@ý4 ÎÎ}÷!g+ÖdȾåI8‘ßrcßNEž+ŸÚø ![>·Lš2S¶=µÈóQ²ås7]áõœhXZù/ I¨Ç„zZÎÉñôlFº´C½j•`kcSK[›˜g/Oc¦SX £*­à:u.¾Ö¬àfNXtÁŠW’ 4I¸‡þ>øç÷(_º4Ì-‹ÃÔÂElÑf¹²›»4œP¼^Ä1 TLaŒ8¢±×C}Ñ´¼ Š˜–Em?~”¡·‡r@Ì2pÛ#i%ŸÇÈZV°vß—+áÝE瀞Ö!—ú†&­—*¡Ö˜¯»Q¾OÞ(O}ð1Í`-6ȈЀ,÷zÍ=†Ø´óšI8‘¯rÒ.ô‰=r¨AU/‚ºâí'ie$ìDw1XP5þ„JC°½mÆ2~š„Húu^£¡ØxÆ­ô_÷“OcH5Õ1i$>ÞÖ ã Ùè·éƒ­i¯ñ ¯pP‹Êè{ˆÎ…¶€@{#6%á üÛTD1õ-F–(^ßvÝK{Ÿ¦¯g…:®¤W^’ŽÁ»’òئï¡mÛmaû…§¿_/«ƒ4_ôP8šç¡6†œÈÎyHÀº®ò1H20ÿêÛ´„JÛÍÅíU½@èØ þxÕ/jB÷⺠èþgC”+acñ¶ ôòLÚ«*ßš¯µŠÆk^×"U/Óð;§î%ðWf4…1M7k ÿº†iÞÖQ+Ã1Hº>%çB~^5zÄèbd‡n[’ôü|e¢Óç(»—'$f¼FÄ'&‹ÓgÌ]dðà¼Y†RŸâ°F_tp™‹Co ÛcyAÿú=½í±¸<=FôÃGõísºmÚ±W\žn›’Î3\æáDJÊC„ u…“ÿi¼È'ç÷…}¿µ¸¬¾v¨Ã‰‘{Ä4×U¼>ŒQNéáÄí~°¶³ôô-\¹~[ân¿”$«c™psúºLÆæ´ë½n:ËÒN¬BÏ.NȃìÄèµb81ùxç,5 ±aÉT_t¡ž^Ã×ã]§„ï·ã{¡ƒ÷R¸&=‚÷ñ(®gŸqoçtuôÀÀ…a8rù.î_Ù²q+œzzʦúu×à§v|ÎF8A]Æ#fÃ!³.ÖŒå#}ëCjê[¢ç.\ši˜G†ŽN½Ä^·ïåll)¥, #lç$8¸àÐó ˜í®{ ­« 'þñX‰ó’°SÝx³ÿx[Çëptr,|Fõ»½K-Óc™ú;'xÀ#øF&·ëQ…ýåá„âé> srÅÀMÊ[5Ó×ù‚W᳄kÎ,½®ï5ç3žŸ_…¾i·‰¤àô/üãˆcâíš:¤ÜÇJWtµ5¯×ŠôÒ3œPÜÂ’¾=á0ñˆÎk>‡_7ïm=h9³NCöà æV%2T€5:s^¶|N]¾%ÛŽš•m)Ùò¹Ž=þSóišý¥BKT/öVlvFÐÚî¨aÔ\œ–áQ¢OëÐÿQ¢ªF¦­V¿ÐüNÄ&—’Z‚†tîl„³Ðø3k¿ZÕ½Úðp"ù~ þ0WŽó ùX¤¤«3Ñ ³pBËþhõ)GÇÿ Óô_„ó#œ {¹{¸÷I&—]qU âjb÷ð ó³ '4n“øÅÿNZc7½¡* 'ÔRñìÂjô¨¥|’B;l¿´é}S)m̉šÝÒSncÚ/êÛ:üqYúØ3 q›\U!–@»•ªs-0äQ¢š·@غìÀkÕôä‡Áh¥ê>oÝs—ø´ÃŽƒÆm¦¿bâu!û¼«+_»Ù¯˜t•*BÚmXÉþŠÇÕСh(:IO›è:×ËV8¡U½My8¡yì„󠺕ê3Ýâ³-̵ì‡Ì'¦¼Õ¥ˆ¥=‚ë®Ì“ç&£¾xn,QªrUå š&5ÑgêÞÚÔ·XÑQÙ£ˆM',S—§¸‚±õ”·uh'4_k6à #Û ·¶¼Úì[1D±Å_Ëž‹û§íü¼ÛÙG¹œq98oÓwð*]Ÿ¯ì›ê?#ÇO»—«cLÿÒß4Ý1¤Ô²Nˆ ,G_Kž,ÀXAЯ>”îåëwèæê%®7pØhñé6Òù –®o“¢ezz Ì…'u¬Â êqãœ=0aѸ8¥p)Js0Diô HMÀÉ…>èà4¡•å*^Æhçžp™uO ¨Wé>–_wv \ÝpPýô‹,è,+%«¹Âa’¤ñþéBGºÁÞg=®hþ(¢xŠM£3S«¤+pwEŸU·‘Hû2ÎŽ}à·ë©î†¿BõèVÿÈ ÌǘÍðîªå¶máÄçdœYäÝ'bã}íOÝàpâë¦ó½u0A?2äE0A Ù7'4üÝÞA¬€jÓ´EkÙò9Õ¶]GÙvÔ:vî&[>·Ñq{þ_‹p_ÕsâÂOüY·5êX¶ÇŸKþ‰ÊÿÑ ‚»aN ªüÇâ4šWÇ¢Z×ÿ ßzŠëÞÿoo±,ýÎE2¶º–B±ŠCp@ó‚þ>ý«Zf¼­C¶î¬ébãßTOÂHÁ¾þ•P¬dlÍp!ÖN$]ŸFf6øsqúxtA¿½èo˜er[‡üuè–x~"j™–F¯ÝÊ_-ó#œ8yA¼GLŸ.¡úŠßí ±iŸ|v òÚU ;…›šOë(¦=œPÜÁÊž5UOCHo”'ÅlÇäékõoèÜ'=F°c9å†%„ƯØxú„ã#ê*×5©ççpùòY„¾…7©ŸqyF3å&Œ+ Õäp܈KARòG<½{[MÇÊ(å—­xÿ½øä sUï…‡£q5*þ]~TíWVb͆"5ŒÁ}YN9„9NµÄi¼¿ƒ•cr2„ô$…¿§!ôÄŒŸÅ0ÄŽpƒÄü‚'û`ÜÒ}8}ï=„ó”ø< âí'æâ¹{Zá„r0Xåq1®é¥§bpûúI,r©«êÝõyøp|$ª©žÂQÑ1± HLIAÜÓ;8ºÓ¶¨ÖO¼†)¿Ù‰û[´¬ BîœÀÚ4p¥ð¨1û¨”ª‡†øšJºa£øT‘T<>8uÍ•Ç>Ï Új=à¿ç,N^ƒÞõT!‰YSLUÝo¬íü(^x;“pëôÅòó/ð>%ñoŸáRø:L¯q]¿ÏWöyð…—/º»õ…CwwÌ X,þK{ ôEßA†ß©3œHMÄ«·Z*ÖI±ÁÏz/ÅÍcD¿úPFŸ¿¢£uIÿÁ#ÅÞ#ÆN{K¨§ÓgK 1|Gã}Ž;Ÿu8‘’úGæxÁÞ±':úInßPˆé1îž0wS8FƺùcàÔŽ£4ÆHÁ­m“…énpŒµ{`ßÁC]»c†.·dé<–É÷±zh ÚüHç­ R:Ë¢ÆûBoüã46AøÑ£8v‡Ñùï£C1й'œG.Ãú}'qðà^Õ£Deú(QÅËÌð›‡ÀÐ}Ø{üOw8‘òñ†T·‚i}oŸ¸[·/a×O4¬]å³ÑsâýÖhhïƒ1sW`Íֽ…~.Üš”F± }°IÕ3$?‰I3Ä{»¥ÓsBñdJ¨º€«©iáDʧ'î¤lQ㩈©p,M„ËÀ®„²A¥ù´å­BcÉØJ{Dl  Î*ýÃÅçsS™o \Ú}óJiççÃELiQFëãO0* ·½êÁ»Þ#làOÝû•ûgZ£!ª‰!BÖâô†¢lJ Û”–U,ލž‚aÈqÐlô«Òud-‹¶KîªöM[@ ­«Ñ`¦í›XÁHݭ߸"º¬×=ࢸÿyNÐyØÛ¿Ž2ÌÑdNP7º¨ÕðjY¶æ–0¶©Šú]¦`çÝ+˜Ô¸¤ÁáDâMر)*—^‹Pñ7+U¿8ǺkéOîÈëp‚À¤GõQï 鼜ùŒû§Âáç*°0³‚iÉZhè0'é—֬ âÕYÌ÷l‹ê¥ŠÃØ¢4*5q‚ï†hœ˜ü Œ4åÉÏŽaFŸ¨W¥,ÌL-PLX¶ìO£çÔ}¸¥ù«nê[è&UKÃDhä[WnŒ¶ã§o;á.vÌ„vk¢„µ5ŠšÚÁ®Z3t´5ÊJ¼‡-£» N9;[–G-û‰Øû+;Ñ“=²n§7mð§8V l‹*v60¶®ŠŸ»Í@؃Œ_ìú‡ôF¿~šxÏΡG“*07³Eqá3âtQ£»©¶€@[#ö Þœ F¿ŽÍPEx›Ð{¾j4ë‰Qo".ÓÆïæ]8Aî`½¯j•-Së*¨×y ¶…ûãWÕ~ü6'óó Jý€‹¡Óá֦ʗ´®'Ö°,û~wƒ¥‘qHŒY†¿Tš6žžþ8¼O°¤ƒ2Ì*bÞ#O}ÞÓ±mŒð¾(+”cZ%kµCÿàËxtØ•Lò0œ0mŒQÛw`dû°³´†YÙFh?r;®k¼÷už¡ì'‘!ðsù 5+”†©p~Ml« VkŒ ½!Vlõþ|eÝ¿qË.ÜŒ‰•Í#4ægv½6:ÉÔwˆÚ»Sý†¡G7tèâŠN½aàäØõš+άÐÈiÝ””½tí¦8Ð5yøôE†ùb@á« (FMœŽz>¦7{”OÚèè< {¥·O¨Ä½Ou»mîËé±Ô”›e1V˜òÞŽ¾}O\žn¿ÿè¹l>yŸ˜vý‘º}Ë  Ù·< 'ò[nî›}'§´€àצbû®½è7`°,< ik7lÆù‹WŸE7ú³.aͺPôõ$+G'ç^²2òʬ€@,ß81RÐ’zAÐmš Ók™NËÒ:´.•AeIËgùƒAºÀиÒy,÷én(²œJ<;?Š='lñ·ú±¾ß¤ŒÇpÍ'Ç0ƾz¹Y7Õ…>¾cÄmÍ –ÍÏ-Š—àå ç¹çÒí4ùNÐÀº†œÚPT–t:cßC®9¯ßÅ‹cMЭdÒyyÁ}ãpBâPÄñ´ÀÒ¦$ú "N;. Ô¶lß%+Gíäés²å³râ”öçÅç…IŸ0|Ìd1\ Þ4°¥ÔƒÿU’N'´­KePYÒòYþ nžD:å 'rAÊ=l›„ Goâþ«H¤q/ÞÞÄ2gåØEÌ~ä+ßòÀ†ºz_0ƾ¹Y7Í 91mö‚\ [”šˆ;ÏâÈÑ}X4º:У:cµ\“ó!œ3iNžÉyÏP*ƒÊ’Ngì[_לì0dß8œÐÂÕÝ3CX0qŠòB° P$Ôoô«l}©v:ËÖÓ…zdH×Ïk*P¯:Ž4n lIOÞ ^ÚÐоÐ>eç™Ç9uåÚM½Æ›P[¸—®\—MgŒ1Æcß–ÂVofŒ±ü”ݶºÞáQ_h)¡Ñ=$ù9Øm‹¶IÛ¦}0ôÅ2ÆcŒ1– ºÞÌcù)7Úê…„6@]3èÞÜ‚Fߤg—æ5z m‹¶IÛ¦}0ôÅ2ÆcŒ1–_ ªÞÌcù)·Úꇌ1ÆcŒ1Æc¹‰Ã ÆcŒ1ÆcŒ('cŒ1ÆcŒ1V 8œ`Œ1ÆcŒ1ÆXâp‚1ÆcŒ1ÆcŠÃ ÆcŒ1ÆcŒ('cŒ1ÆcŒ1V 8œ`Œ1ÆcŒ1ÆXâp‚1ÆcŒ1ÆcŠÃ ÆcŒ1ÆcŒ('cŒ1ÆcŒ1V 8œ`Œ1ÆcŒ1ÆXâp‚1ÆcŒ1ÆcŠÃ ÆcŒ1ÆcŒ('cŒ1ÆcŒ1V 8œ`Œ1ÆcŒ1ÆXâp‚1ÆcŒ1ÆcŠÃ ÆcŒ1ÆcŒ('cŒ1ÆcŒ1V 8œ`Œ1ÆcŒ1ÆXâp‚1ÆcŒ1ÆcŠÃ ÆcŒ1ÆcŒ('cŒ1ÆcŒ1V 8œ`Œ1ÆcŒ1ÆXâp‚1ÆcŒ1ÆcŠÃ ÆcŒ1ÆcŒ('cŒ1ÆcŒ1V 8œ`Œ1ÆcŒ1ÆXâp‚±v÷Á¬ Ý¿ñÓàìÞí;÷È€¦Ñ~þóƒà=ÔŽ=úÀÑņœKqâ ©²õXÎìÚwXüaÕú­²/î¬Ð:´.•!-÷kŸ¤ÀÓ—odÓ+h|=ü~=xò#ÆNIûuwäø©xòâµl9ƘÜ÷\§aŒå.'¾c‘®À½ï`Y—;)ïa¸pùºl}–=Ô•ÑËg.\½)û’Ö­KePYÒò Rò§±aóNŒž8+×mBìãgæS01rÜT„„n“­ËXAâëá÷mʬù²sM¿èJ—cßEêì ǘ‰3Äï&)¿ñS±qË.%3ñ-×icùÉï5Õ0X²—®ÝÄÛøDý?xÍôèÝ?m¹-;÷ÉÊa†¡_è X[WGCQTVaúµaѲÕ*÷Înýð.>Iœ—˜"Vöh:‡¬0áëá÷¨û c77$¥|ÑÿištYöm Ù¨üÕ>+£&L{VI×ÿÞ}ëuÆXþË·pârô-ñWUétª\¹~[6ý«¿Ž–V¨56 ‰Òy…˜fE|]èv|LJ‘-£F ÊUë6¥-¿}ÏÙ2L?tO%Üüº EeQ™…á~ÍøÄdq_ºõòBä…˘8}®ø÷ÃÇÅJ:˜ n^ƒÄnÓf<|úBVV¡òé1æ·²I‹E¸ýI9-!|ʘT€ÇÝŸöuø®‡Š7ûàùcU4ó¿úU}Wå•»±ºu7¶íÞó—¢13 P<Ÿc'ÏJ[f줙â´Y‹qîÒ5qYZ‡Ö•–÷µQ<Cÿea^²únd-Ë|/¨Çgº+Jø>Õfø˜Éâ2PÐ5@ZÆ÷ê[¯Ó0Æ Fž‡ô‹Ä‚ âÅ&âøÙ|ªÜÑ s·_ÃS…tù¼“xnj™Ò5ÒU‡G⣖e¾êÐüñóW²yjH 5Q\ŽþÍq@‘r“[§×…´*üuËo½NÃ+yNPذh™xAïÝo^ƽ—-óìÅq-CËæ8 *ËûüšÃÖØ¦›£«ÏDLž5Fx£C“j(ç¼OSµ¬—_Y8ñác²Øe™Žµ!qµ'Äu½||³yÿe žßÜŠ>?Z¸ñ„Ý‹ÿ*Ž[n _hTjé—pn¡² Ã/ —®ÊP‰§nÑô§yêñ&hºú¶Ž³QWÅ¿é—KiYÙ¡ ',QÎ~2—­D`à|Œ÷醺%-Pĺ&œM”­“%'tR¼ …C‰¦˜rí³l^aWð×C‰„ô«l #S+”ïwÒùúø‹9ÍK¡Íò—ò Nøn•MûÎ|ÎSÞÞèèÔ ÇO‹×+ê!±ç@^Å}-OÓh-¸|5†}WWôô(–%]>3ñQsð{qs-P3ûqŽ&Ê×ÉܼuEq#+4˜r]þ]ªˆÆ„Ÿ­ðƒe/l’–­xŒ°€Ñð¾{c¿Ÿ´Ñ'œ ¹P¤¾Å© K°p±Ò¼ahfg“&ÞðWO[¸«O½*´½Z¾—: c,ÿåY8A!ÃŒ¹Ê®’®žñìeœl5šGËв´NöŠqwµ#J[¢‚ãr\|/-'Uh åb%ú+ 'vï 1ý‚-§:/ýË8yA6_/ª_ LÚãAnõ`ù Ð OôØ,épÏ®bíD4«Uf6°,ß­¬Âñ§Z–• ² Ã@RTaSî)t¼~'ý—HBu‘U‡t«—zYiYÙ¡ '¬Pwâ• ŸÉøkóð‡µ¬7ᙡá$‡:ü‹ç›ÝPÂü÷¯2œ =º®‡4ÆÄí{eÓ5Ñõ°ÿ¿œ]5¼Þé‰R gS˜”ñÀÖ·òe²’»m¬lµ‡ Ñ7cÄóEaƒtõîÚ±÷ ¦úÏí ;¤õVTì—ÊÐìe‘µls+‡¢ÅÌPĬ:ÏÜ‚]{v`eà ôwømgßB’l,¤¾Ä ‡R(RL{8‘pf,~41ÓN°4Ú‰Sg/ŠÓOŸ»”aY1`¯\ž¾ÇrPhJ:ïJ–°pÝxé¼BJ¯:ÊËG‘˜ëò3,ÍÛcîí²ùÚ–: c,ÿåI8! &²J¤ -C¿hä( H<¡5,Q¬šÂâôX?5BÆ c“akU–åêã·YØvGþëjÒ“ã˜áÞ UK‡±UEÔj?.oF[y8{ÓÝÛ FÙ’0±*[ycî‰×ž€Ÿê/ßã§ÏËæékgØA±ŒùAÁ²yzÑN|ÆãcðhÓelm`d^¥~l{ŸœW-Æô@“jå`ffS»ê¨ûWLÜ÷T<®I1 ñ›Ð o¾èaÆãœ‡`{[7ž‰K)šÓßãìêQèШl-maUé78ŒÙŽèe²Ø¦ìµeÂoü4ñ¹ÞÒ/àt ¸²ÒõÿŒÙ;Îâ\ô Ý:W´F…~ûñ@¶|FT6mCºÝ‚@÷èÒ{„½(G¤¥ei érÙ¡+œHI}ŽÅmm„kƒŽ&Ó4v{”C±ò>Ø—”±Œ§Ç º‰ºoSu­Ïf8!.cåˆU¯ÞáT '~­R¦6­1릪! Ç÷Ä/ÕÊÃ\xÏ—®×W]Áë áɼº¸ýÛ6@)+k˜–¬‹ÞÂçâA(:Yil?ù#zv [t@ÐcÍk¢ð™;¾ýþ¦Ïœ-LKÔ¯=gaï_QS? rå´>#6Â15¶*‡Êà1ÿ4ž¥~ÄÑÉ-QÊ4ã/ÁF?MB$uOÏÅÏN^ÉìzHáݶ"ö°ÎWËñõPM¸N­u*ãúSpúü4üdV×Çi RñèØ °ÿK•€©mETiÔ#÷¼Àã=ÃQ¿„¥ÐXÕøUÞ¼ îÿ«3Hÿø 3<þF­r%ajU;bðòsxª‹ï;Tq1ásѳÅO(ae³Ruð»Û|~¦Ñ€Ïô=#}-ù/îýG8twÏÙÖ]éšR01täxqº&j€jÖE6ï§wÊ R¥åëôéüÿ°ωñ/³qUó»Hæ ÞÞØ‰‰îÂy© \3,J¡L¿Ñkf8bTתäç°ÐãwØ™Hzb˜6˜‹ñ¸±cÚU+žñ½ P_’nà3šf_gÇ(ƒ‘äH «n‰Lê`艗8äƒVu+Á¼8l«ý ç™Gñ0Ã~ÁËsËáùçO°êOÖU›£Ç츳µ¬ÌPÄÎ ;Õ¡Hê[œû&µ„e­adQZxMmàì·‡Ö%·i 'Ô×7Iº<ŸDcOÐ|_áý‘ÙõAoºÂ‰T¡^°u<ìÿDªeaFu£ZÃ#è<^hûžhCß¶°(÷3þòYƒ³6¡³æ÷„>u-=e]§QÖkn × û•P¯mK”µÔ?œ(LuÆXþÊõp‚~iP6èL¨ÑsÆitZ—ÊÐö«Ef>FŽE +Ô™ i˜h•ˆÈé-amR \§bÁšX> ìk•@±²tKã 3>£Ú¡¨m8OY… !ðÚU«ÖFE«Œ¾ä';áZÍVõÜ01x 6®[ŒA­«ÀȪ)&œS>µ  ¨»0SãP:P÷{ ¨»âKÑù¤2úŽ–ÍÓ‹–p")f ZÚðvc0Ývl Æ ¿>hí¼Ñbe(ûÖ„‘í/è9=¶lÁ²E3пs Û¯*Ãp" çf´‚µY5´ˆÕ›·bÙT7Ô±¶B¹[Tû•õ6 áìÞ/£Y@øÈŸaZ2޽‘ÎˈʦmH·[² '¤hÙ¼'Þ`yÛü',š¢Ïàö(Uþ8˜)S×ã4Uwd_F¥þ@ÿµØ°ifxþâ&eÑzqLÚ¾¼±­JXÀ¬f7Œ ÅÚ øuùëþŒ2¦Ù '„ŠlÄ(ÔU;ŽÇ¢ Û±néDüSÍ&µ†"ì -÷OB]QRØÆïbåæíY¾£=Ð~ê9¡ý ¯žÄöQBÙ¦õá±æ8=‰ˆó—š»Ÿ¼’ÙõPsLzBÇ¡#'eË_U’¯A;kkü4é*×0ágkX´_-é]öÏ EM ØÔwư‹0{*|\Ý1ól ><¸‚ðµ>¨ejÆ#v#\8‡_EìÇÿÔN$?ÞžÂw•iÕ𙳫„ëîdO œìPwØQ¼T7~Tïã Q«Òp¼+Ö­ÅÜáQÑÌÅ;­SígVïùë.4ð¡úÜÆ>R>ê˜zIÐßô(Yú?õ èåå#N£Ûwh“þ¦qFè–i¹™Jý€õݨ—ƒÐh·l„¾›btÞ¶iZ”´Ä"ƶ°,n‡b4V“‘%Ê;oÂ=áX¿ßÔ¦’à!-œ8wãXÉçéN³D…z?ÃÆX²¾QI4›ÞÃ#éÞ*´+¥ÜOÍeªÔ¨‚bÅ4Éqg©=l4Ç›R)bѳnl¯+máÄÉHeω³¯È–'P¨{ÐÐ-9(t…Ÿã±½Süá63—mƦÍ!שLLª¡÷žø´ð2)f%þ.mÓŽ¸«V,À?¡l¾'²®kéO¯:Í»gØá×½—]Â]á;§º•þáDaªÓ0ÆòW®‡ë7ï/ØÝÝú"ö±ò‹ßTY u© z¶´t¾n_ð"ĦF¥ÑsWÖ!@òƒ`üem‰J^2$ÐÉ7 SK”è±UõKÏlv…™Ù£’¢ˆÆÄ†Ö°¾¸?Hç©N$? A;[kÔsïÓ–ýŒë­`jÚ£/¤êµMCÐ1•~ùfí56¸U„å?+pãtžmCºÝ‚Pà ūp.g©q[G>…B¿h…Xy?ã}ÝÔ…¿´éðÓ¤3õVv.ƒb"LìÁ“€•Q̪-æÜÖKSbq;¡¡cœÍp"å¦ÿf ó ­1_|äxÔ6µÅ c…ÏP ùT‡qÕˆÃm¾àéªN0¥Ï®æm¹üÙÉ+ô¨H]×CÍpBÍoüT­a;«ÒÍÕK6]BãmQ[á8þ‚‰—é<FÔ”_…sÖ÷4zUhaTg$Âßjï¨ltj¹­CN$!|pmá½Õþ74ß›qjL û2á’ê=§zÿÿ`R ý¦7ˆhY ¡ŠY9aõkÚ}Þ3…òUëÅsJAý­þA…žÊ¡^†nÕ¡iþó‹S@EÓ#e¥åé#îØhÔ¶Põt0²E¹?}0/üQÆÏˆp¬ƒè³-,S´bw,ŽNŽwn¬rAY LjÂçˆÐNý 1‹ÐÌœùV¨?ù*â©HLù,~ÿ%§$bWEñ6’,]±!>U%é“ò}“y8Aûg‡zýÖ"âò-œ ‚z–Êý6n2WÄïQ"†ÖCˆŒJ¡ñ õ8t6;öFMÕkL'°®«²GÑ2îØðüÞ>ATx&Í Çã¬ú\Óy5ô©QôTu êu£-äÔ›ÎpB‹w»à\ÒåG‚8í#ÂúWG1ë¿…†Æï‰ ö¥3|OdY×2€¡ušg‡G ª='Ha©Ó0ÆòWž…=ÜûáÁ“ç²ùY¡uh]*ðpâ_Üjãx$«ž¾àYˆ#ÌLjcÈ é²‰Bƒ ŠÚõÁ±›["¶¹•AÑÂß2–“t}6›iTø1ªŽ,×ãч¼Mó «K X¥¡8(iå'ªDSð#®¦ì‰PwEé|5zTd—½eÓõ¢%œH¼2 -¬Q±ãl쉡ʘdÔwØäZÅŠ7Eÿ5Qx¢¥â«8ñ/Ö:ÁÜ´!üN}Ð8G xuzê˜Ú eÐS$ë±MCúENœ˜‚ÆvõÑ?ì¥lž6…勼0„µ†ÆÝ‡Osû"¶,‚Ç/åPÔ¢!|©Ÿb_á„5Ï”ÞO®À^¡áP¬Âì|­yˆGô¼¶01m‚ ÔHM>ƒ¡BcÁ´Ý*Iþ ¯p€i6É介ÐÔÜÜÁÍëÔ›ð¬` çmBÿâîR{XšTD‹Ñ;p9N[eVG8‘ËŸ¼B×0ê©'N´…ÔÑNÐ5•ž8#®·”Û˜ö«µpš…˪5)Ú ͬÑh†Æ/Õ·çáWaZÃé7uŽO w8!¼_FÔÞ/í×ȇI·ç ×RaÛÓUÛV½ÿ‹V†C>+êóÿ'fÝ¡ó¯Ï{¦p†3UáÄUÇšÓ¸#4M=XoøÑSâßË…u¥åéçîî‹?ÊZ¥ßnaT?:/Eä;ågSñ<íÄ À•‡NŠFÒ1¡ñJ½¬PJ´x“ï/ƪpB>æD Âú©Ã ù˜Y…EËöÅNu}çÓ3¶UÞ’R´Òe&å:&6RöÎ(VÕáÔ;G,;¡.e”=DÒ‰PU¹/Æ¥Q¿W¶]{'Ùß‚³bÍFñ¼Ò3†lK=(Ô"¥ÇKçëÍpB¥¬gºîQ.›|¾?jû,Áó5]2|OdY×2€¡u'cúÊõp"§·uhŽ;aØmª ±QôÚ“U»Tñ—)#óöXôPºÏ¸6³ŒÍ[cýjE_Ìml`ôót\”>êëýtÖ¬ð%쀓•öѸÅ_lú`[LE£ÊÓ±¥ûk¥óõt¡Þ¨‹2 é|B#”S}ÍÓ‹–p‚k×C}Ñ´¼ Š˜–Em?Îø‹’âåiÌtj+cs˜UiשÛpñuúëÐ?œHÅÅ)¿ÀHËùQJoLfµMCèÕRÃóAø§r´šs´Ì—*L] >œœS# XÖì‚qa5™—Oá„I¸í•ü¢öé)µQVöµR‡¦Âõ¤›•ÊùSýB¦QöÁ(­¹}‰„ˆ¡(/í²­!­’›ò{ÆwD%ášVÔ¶.Z÷_€·4+´:‰ϹûÙÉ+™]3ÞÖá­³ޝ‡‚Ĩ)¨k*| Þè˜{¸InÀ ºV0ª7gUáNÂá¡(gl§ÍºÇ:Ð;œß[–(ï£þõUCâ^¸ØZ ¤W¸ržú¶Ž¦óq]:æ@HWáü7Ç õ8*Y¾g =qƒÎ=±ãåëwâ´=û#Äi^>ÃqçÞ#ܸsž†‰Ó—¡[iš¦ëý ä¸Ø8Å?¥b2Ý7‹ß‡‰'£¶øˆO],Ò~1ÏËp¤ÕÜMû~NÂFçââô¢êëeR¼Ê)oéÈØ(þŒKSƒq±ŒcN|üìÝ|çãðÿº ®Ã}0lÀ†»/®Ã]‡»×A[¬PÜÝÝm8”ý@âµ<ÿ{/i›^’6eiàù~>Ïg#¹\Ò$mîž¼÷Þ¥ù¨•Ù9º”IfY«öüSÚ#qL“§/‚Ñ©ûŸòë*æšHhA!N+,n;ÖÈÏ;½1TN„<ljÅ#ФүȚ> œœÜ`ë mJW£—Õ|Nèû]ÚÖic}NÅ¿­elºM“Ðr")mÓ0 óß&ÑË SMˆùbOod²qFaç ~³¤Ž´ƒ:¤0¬ì+é-'NËåDLå„zGªàHW–AËQ'ÖŸôoéC"E騶÷v)²ûÀeÜ1áƨ×äèÉ¿u®36ž’×1fât댊ÞrB×AØî=u ¦…•U älµWcm ‡àöɵÝ®2I¿Vé*cÄAõ†¯árâ>fVŽ]N,½öÒU·ûu^£]{áh öaA†ïSçg‹#ÆM%òW¶D©ôYQaÜaè\¯?Iiò(Ó—ŽÈØh:V¬ßŒu[öâÀ…x¦ó{G9!ýù)ÑÊ =˼½…Éå\`™½-æîR¾ÿ¤ì;‹â›Ô ¨+v £‡ïj¯»3ÒYNÈÏ´Ë iƒ5µ+JÛ©{ßRöœ½¯Uâ|Äó[G±`X+K/mÐ:ä„Çì š hÃå„:‰ó»ó­×ßè 1gÌ™çñä_ý÷PzîêžG=<^_lò Ëõû0h«xÝD9aø°EãË ñÞÒ_|½y¡.'RµÝ«œÐ~ÿ«£§œÐÄð{Æ´ÑžsùšÑ—‹/B Mˆ©ý%ÉÒÕê 1Å:ž<7ü:“—ÿìBŸ_5óPHÛ"“®¿ÇË#ƒS.'àœµ$Jü^N‘ ¨éu⛜°­6_ëTåDM9QežÖéÙßáÌè’:å„HðóX4¤~IëSR¸UÄøsúþvü·¯eÇîý4…ñ#(Ä©°Åáab’Óç.é\otô–oqjB¸Ø¤B–“±hÛQ;{gÏ,G«,ZËFz>'^ìì Úsiÿ¶Vü1~›F„–Ii›†a˜ÿ6ߤœQÿÉ©DŸmF‹ô°Êó'öhŸuA'pG íÇoîTî\¼ÀÊæé`™¢%–Ëëx‰eMRÁ"U¬R¬S½q =Tözˆáu•¼qCggÈô‰–*†°*¯36CFyÉëØ¹ïÎuF%Žr":oaç€Rp°ÕwØ:Ï.ù¡AfGØUö‘Ÿëà«âø[”˜t=v9¡~ª}XÇ?>°³É‰N»”¯}ÜQÞ§òú¸bÜi·žãìÊžø%ÍÏðœ{·t®7œ¤rÚ-±‘7rüù="N*¾T.£%+ÖÉËŠ ¼¨!Ö_CsNèFÚxo–i:`½¢œx¼¬)œ¬¾a9!mì¯j‘)šÃÿ©îí¢¼]¤P±£³ñ¯Îý…õa¯}X‡f¨¯mí%xk=o±µs6Xj•ч 7|x€¾¼~rã*KÏYŠX&ùþ€Ûq–1ùšßo•¸þŠ!ýbDååÊ|õßàíø#“ôÿÚ s—­Â­,þ«òÚ9 ]Ûíòä‰êÃcꡌÑåÄëcèKz¿Tœ‹@åaò:œQxôÅX‡u$¤œˆŠî{Æ´9£9•¨Н¼Nì®X»CGMÀˆ±“±zýV½£7Å©gÅ:t*Ñç¸ùÏKE1÷W¦V„ØQ·+†Á§ßáõ?¾¨¨9¬ã§®{ã,t‚¦á7M9‘oˆúP˜ëE9‘1zÎ ?ŶËW—¯Ïb`ÍaÙûbwôßÐ`ll§>„CYND'(kúW€»¾yœ`Sr .hÿÞïA‡Ÿ´–:üOÏçÄã¥Mà¤ý9¡ŒÛZ†bÜ6MLZN$•m†aþû|³rBD” ^SfEï¨ÜôLg™{ŸÊ3d‹eIJÿº˜óG‡—€•r·]ƒ+qÌï &E¬ì戴ÍÖÆš¤2øú|TKé÷Qb¾Gàœp°É„Kjm\„àü䊰5!¦f’1‡bè{0êØö¤qNî&­;ÉÏõ±Sçt®/û—oÛª}wùXKåõFŘrBŠú¤Th²F½ƒ¨ûmë3øÖr‡u1/õ,Ó—¢–³#ÒwÜkƒîÕ… (fo{BÌësäÉPS×õ‹óÛ‚xïSçzùrýê5m£ó“§8êÛ Ù\2£ìÕØ¼û¶FeÏ¿÷ºX·¸åýþשŠ÷HTıø÷=ÕYND|s©½¬ˆ¡ÙÑñåD øV•0ùºö©ŸcUKiãú›–pwis¤°I…ÒãúFù9–7ÍK—*˜|U{N‡{˜W7,´7:ßbBi=‡Ÿ½8„ž¹¥í 1_ÿ-ÿZf뀵÷âø{«S"¼ÇʼneaãP³5·{ä×@3wâ›9庾âwç[%)ü=£¦´IZ‹´?[4y{ Ó+¥€Eê–XöXúwðQ¹P°ÊÓ; ”ZÁÓ¥V=óœ(Ë 1ysÏ|°r*…á§µ;z=}~µ]a 1*'ŒxϘ2â5oØ¢¼S¹]úÛ:y¦·|V†9óüôâ21'XFœÅJ"n+Îô"&DT.o0¯¶£MÆ4ÈQ©=úO÷ÇÊÍ;±ÞärUO™ª–=ù(?×Ó+kFS¸–Dç¥çðÏ‹wxôWŽoÆô¡a§¦äy}o*ɇ°Ùñä`¬=q‡vlÆ^ùïÙ[éõՌȱɉsãäÉCX¿í†$B9ñ.›;fS¯ß: ~°ǯ\ÁÁåýQ<¹æL#ÑåÄ+ìœ>^ËâìÝW–ÞöõG>y„ˆÖý'ˆQRÚ…¡ßëD-&Dô•ÁûÐ9‹#ìë‹9€b–}~l¨ôÜiÖ5q²k5L½¦ý9ñ~2ÆþœÐå¶–±‰›&vZN$•m†aþû|ÓrBD” âðâýÚÛt®ßNˆëÄ2_WLhòòfÖÉk+é;{e4î6 ÃÆNÀ°A}ТvET&zãì¸W%¸I;ùŤy‹ñפ?£O%:í‚Ö‡ÒÓ½è‘ß É‹¡áÐ9ðY0cº{ GŽ‚Èáæ¬8•èj4Íê ÷B¨Ýk"¦Í‡é“F¢c½Ò(Ñÿ`¬áÒ¦ˆøPÏwÝF­ðÏ=ý§ Õ1…ØÑÃOž½ s½qy‹{—V¢UvGXþžËÏÛÓeP°ZGô?¾ËÖa±ïx4-œ –é[`ÉÝò·4#ª–Gƒî£1Á{–­\Š)}j £½ ;£~NCî·NZX8Eë9»pøôYìY;Í eEòÚ#'D^áð˜rpµvEú²0hª/fÏ™…‘ýÚ¢|Áf˜{ÇÈûL`FŒ› ï…Ët>„å<>a% ÌC`•žKŸèÞF±N±nåýý×£&Ä{K‘bær1qœø÷†­»t–‰:ÎWL‚»ÿð ùÿÿýðxuŒ/'¤¼Ãƒ‘ÇÞi«ŽÂÒãıݘ7 2¥N gëoYN|”O%:³†´Ai›EšÁ¸Ùó0sú$üÙ®6 U›½tj"J¸:À>g=ô™º¾¾Óѧ~adΙOúÛ¥½îwø{Ry8X§E‰^þØqò<Žì^…õFêT©a¡s*Ñ~Èçä‡5ÑqÔlÌòöÆ„‘}áYº0Ú®?÷lé]åZôLjé á·r5|&vCÉ´Np©4'úyxuaŠ9:ÀµhŒ›ç¹>;pñeâÿî|«˜ôïaÈ#̯›–;`ÞQ¢Äj†äÒg”ÇÂGx-íÔÝ\Þ™là^¸9úN˜ƒéÓ'¢_ÛèàwW]nFÏœNÒNd%t›¾ó|ü±ëÖ{=å„ô»rgƒô·Xz_ÿTíÇúÂgþ\ m­>•hî®;tN%9aÜ{ÆÔ‰š#@ñøûRÌhˆ³®Èg¥R.'²kßaõÆQN¤Sœv3*ÖéPvÊ…èß‹ ssP%}Ô|Š8ÔÂ\ùÌ(Ò²o.cÌoî±—ÓÚ}¸¾=Ò*æ–‰z ¿¾œvjÏOE)7Å[Òߟ´ÔIy¶ùzi»ÌÒVzjN+šÌµ"¼.èucªh½ Ó)(½˜ÑWN„<ÅÒ&aéV ]—ÃßW/aßò±ðÈ—™ÒÅ^öÅßÒká.}Näj€~3–`á‚Ùèß°2eɭωx·µ”+žÄ¹M£HBʉ¤²MÃ0ŒiòÍˉ¨ˆY°õ}3!&#Óž!;Qòæöy÷G½RÒ†¹« ,­`ëž¹J7ÅÀ ·c†ý‹É†ü¡N±\Hîä »”¹QÌsü/èN8öòÆ kü;2¹»ÂÊ)rUíÿ‹ç1º„{¬ >‘7÷bRçZ(9-ìíœ`—"; Tí€1ÛïÆ>äÀDYuF•æíŒš*ŽÉnâ6«ÖoÖ¹ÞØ<_ßîÚç:·É‡žÞâå¹%èP½82¥J+éµ²K™ EêÀ‚3šãÓCbëØø-ÏOpvp„¥}J¤ÉW-'ìÁ ­oaƒïîÃØfe‘%¥¬¤eÒþ\íþ:‚Íým¬rB¬3'— GCñqs•—O•«ê÷YŠSbæq#ï3!߈çðèéó:Æÿ6b]bIá1,V<ñÍäµwä¡Ñâßb²9å²"z¨Ë ±±ç¿R},·8¬K¹\B’rBltŸ]ÜU d†“½3ìRåÁ¯FbõÉ…ðpJþmË ‘X3¶=ÊåÏiÇÑÚ)2«NÃý胸s`&Z–Î wiÛ”ùP¦³¯éŠtÊc‰ƒobíÐF(œ9•´Ãè —ÌÅQ£÷R]Ù©µË ‘÷¸sd>zÖûYÒ¤€­´|Æ_P¦ù¬”¿}{«k£Vñ¼Héì KéúŸŠ¡rgo쿯½ž`œžß %³H÷iŸ©KŒÂþàÄÿÝù–1ÕßÃ×7}QÉÙ 9zÖ9V<:A»Ð>³´#RÙG3Òì .oôBóòÕ·R!c‘†µ7jbÁxxd&Í G{W8e®…©ßé-'D^ÜØñm«"wzéõs^§üÕÑvÚAÜÒ~ô¼ÿÕQ–ƾgL1jBÌ+"Fo9qV>”C¼žbBĨeþŠdË(ýmr„•cj¤+P­FlÀy­¡A—W K¥Ÿ‘ÂÉIÚ.I¬Å[`ú͈—ÇØíÕ\þ{`cëçL…P¡ÿ6¹tHŒrB¼Þ·vM„g±ìÒï¹\³–CË™1¯¹ælÉÛ`µ\N¼Å©=áQ"R¹¨·ÇìSåB¡š=1ýÐcÝCI † íbâÀ‘:·û×ÑWN¼£cöcLãßÞUú}²—~׋µÀ¨·°¥KùTÍ1ËJŸg¡uÙüÒ¶¬‹ôY–¥ÚÎÅž•Ýå9'Zi>'âÝÖJ`¾÷m†aL“ÿ¬œ`’V¢Nù*2Úkš¼ñ¥]‰ã-Å·ÙÚÃô—­Þ ³&aY½aZwì™ Y® E¬C¬K¬Sy?¦Ê ã¢ß/"5ë7Ãûu–‰š\N;‰ºÁ÷'ÞâƒIPø÷…“˜èR|a""þ_\¦ïKÆ@BžÀ»¦›\NXfé…Iô4ÂÆäùË`tï;Xþ]ï=`˜|ÖQJˆìÞŸH#&¾qž¯kwÛœ žO"!ùÞ·i†ùïÃrâŽØlÔR=©ˆ8µZ‡n}åÓâ‰ÿº¼e»nòqÊÛ3ÿ.b’'ñü5ß6ˆÛŠu$µ £={!Ÿ¥C£-¾yÔ&­Œ8ŒkÁ’•ò²"âxnå2Œþ°œHüðïᨑ^õ›GŸÑƒCË çÑþy1w\¹/Eó×·@gq¨‡Ü­Ôfžs”të3(úw?ÑGL|Ë„<ÁªVÙaé\sÄ¡ªÊë1ßó6 Ã0ÿ}XNüà“z‰³$ˆo Ûvé-G]»aKù´Zb1 ½øIy;æë"¾;Ư©qq[~»ðã†åÄ· ÿþ¸¹qû4ý#zGTœAìÖÝ:Ë1"šÓkÑLfå{'é¿ê[¤¬ ¯³ßÇߦ¯Cä]”WbåõI!A{¡X•vè5z6æ,ZŽE ç``ã"p±vFöλðà?(‰¸MÃ0Lb…åØ(â˜JñÍœ˜•Zœ6Kœ×[ßÐHq™¸N,#–·áñ˜?vXN0 cÊ<;¹]UBÞÌiá`ïK»äpÏ\å[ŽÅŠKºóv1ß.¯®­C߯•/K:õká©óVBÓ1Ûqí?<´†Û4 Ã$FXN0Œ‰#>”ÅPÆÞ†£A³˜oî¢".׉eøÎ0 Ã0LR ·i†ùš°œ`†a†a†aƤa9Á0 Ã0 Ã0 Ã0ŒIÃr‚a†a†a†a“†åÃ0 Ã0 Ã0 Ã0& Ë †a†a†a†aL– Ã0 Ã0 Ã0 Ø4,'†a†a†a†1iXN0 Ã0 Ã0 Ã0 cÒ°œ`†a†a†aƤa9Á0 Ã0 Ã0 Ã0ŒIÃr‚a†a†a†a“†åÃ0 Ã0 Ã0 Ã0& Ë †a†a†a†aL– Ã0 Ã0 Ã0 Ø4,'†a†a†a†1iXN0 Ã0 c¶¹Ö¼.ÎÍÁ|ǯ±òug†a¾¿üßñÓçaL–¯ÙÄ0 Ã0 “¤rÜ£¼ÎÎ,ó}E¼ÆÊ×a†ùþò•=<Á0 Ã0 Ã0 Ã0 cªüˆˆˆˆˆˆˆˆLˆå™Ë """""""2)–DDDDDDDdR,'ˆˆˆˆˆˆˆÈ¤XN‘I±œ """""""“b9ADDDDDDD&År‚ˆˆˆˆˆˆˆLŠå™Ë """""""2)–DDDDDDDdR,'ˆˆˆˆˆˆˆÈ¤XN‘I±œ """""""“b9ADDDDDDD&År‚ˆˆˆˆˆˆˆLŠå™Ë """""""2)–DDDDDDDdR,'ˆˆˆˆˆˆˆÈ¤XN‘I±œ """""""“b9ADDDDDDD&År‚ˆˆˆˆˆˆˆLŠå™Ë """""""2)–DDDDDDDdR,'ˆˆˆˆˆˆˆÈ¤XN‘I±œ """""""“b9ADDDDDDD&År‚ˆˆˆˆˆˆˆLŠå™Ë """""""2)–DDDDDDDdR,'ˆˆˆˆˆˆˆÈ¤XN‘I±œ """""""“b9ADDDDDDD&eÖåÄã'ÏÐwàpTöð”ÓoðH<z¡\Œˆˆˆˆˆˆˆ’0³.'FŸ]LDeìÄiÊňˆˆˆˆˆˆ( 3ÛrB¥R¡–g Ô¨×rÄÿ‹ËˆˆˆÌFÄø _÷*KQuE>*¯'úáE À«$¬-íð?»{-B¹€ñ¾lA3W{üÏÒŽÍ7ã‹òz""2³)'îÝ€Uk7bÃæí8ñ2¼¦Ì”GJ 1.z™AÃÇÊ—M˜2 ç.\’—··¥ø„ãüˆÂ°rôIJ·Ê눈’žÈ +(_n.’ýn\¬ŸÄ©påZL/âÑüVVóëÅá$øé{‚­2ÀBÚá·ÌÜû>)¯Oˆ/8Ú7/,Ey`“½&ö.?Ë "¢Y”GŽŸÔ9|C¤nãV¼ùOôr7å˔ˉ=~Jk ö ÇýF yåbÈœ:llaŸ" r•¬‹¶#Wàük•òfˆå™—樂P½½…†ÕÔÑ¡ó%ÜŠT.AæNõ:5+Î…ÓÈ;IfgXõj †YsçË#$¶ïÚ‹7ot÷¢Åeâ:±Ì_> °ÿÐQT«ÓMZu”וP‘ÏöbÀïé`iå‚4E }ÿ±ç5zt‚gÅBH½+v½WÞʱœ "ó¢]N8w8„I+/bj™¾í ž&É‚o݆÷º›8ñ*I>@úJ!÷À½TR*'Txî_ÎVÒξmA :¦\ Tx³¾RXÛ!™£´½d‹Ô­°.Q¿¸a9ADô#HòåÄõÀ›òÈQ6(‰y&6m݉Ñ^SälÙ¾ ‘‘ºv†Ž–ס=ÊÂ(a×1¥\jX8þŒ–K®ã½žÏÙðÐ0è¹Ø ±œ "óSNx#õÄU.@”$„c—×bXþž„ʉÈÇð­–ɤ}«_ÆâR¿»‰¡ Â’º©¤u9 M“Ψ\Úñ·J…ÚþA‰¸}Är‚ˆèGäˉwïÞ£fýfr¹°~Ó¶èËE1Ñ£ï Ã7ú &O–eí†-òå šáý‡„ 2TáÅÊ&p·vCq¯« øðŠÀ³£sÑ©J!¤us…]Š\(ÞÌ ;h}+ù³Ê»#ÛŸGqoßd´(“)œ\`Ÿ*~k5 ‡ž+ Õœ[Ü…³ÃÍÑÎ™Š£Îàõ¸¡5kZè¾nHëTK^½Æ©¿Ú¢xæÔ°u-É7¤pÕ;\[ï…ve/KZØÛ§@ªÜ•ÑnîG?U,'ˆÈ¼°œ söê6Z×÷–Gø$•r"òÞ”u%‚3ŠM ÄWìê#ò¾/*8‰u¥FÃU·á_Oö°¯ä‹ºßýK,'ˆˆ~I¾œ>]><|ôX¾LŒ’ÿnѶ‹üÿbE³6äËöì?$/#&Âÿ®R«¡|HH‚¨^cyƒ”°pk„e¯ŒíþUxs°?ò;¹!k­¡˜³b=VøGÍl®°ÉÝ»£†8Êå„ ¬sBîL¿£å(oø-[‚éý<ÉÎîuüñ8úýÎ{•ƒ‹]V”ï9þk×bÁØÈçâ„ôMÖD/'—%Ѷg5¤Jÿ;šþ9cÇ.Ź ÁæN%QºÕLš· ëÖ.Æð:y`c“l Ñ|³Ár‚ˆÌË×—_°t€¯t{\ðâË㈷/°Úÿ0<Ú-Gúª¾°)7)¬AÅѧáðI±#3—ÂJÌiQzz_ŠoOL…Ë‹ÖÀZ^Þ­ŽiJë°ûh[C½ój?è”G †œDî2saQqÖV_öùé}L» ¹=æÁº”´¾²ë1õ‘ò³J…—7oaü¤ø­éb¸•÷µôó¤ò\ƒ*cNÃïò‡8Ÿ³˜ûÝ5òd‰á¸sö*ŽÚ„Ÿë/‚S9ØUYŒÜw ÛªûøGóØté>Ï_‚Ãwî”nºîÒ㲫¼9;ì@÷5q_«Ë~Žå~QµÍ2¤­ì+=þHßl#Î ÀÉàøžoIä\>xþ\‡ì5çöŒœj,EáÞû1jÏ ¼4¸ ÝÇœ°÷†ô쇼€ßìmÈQQwþ“è”ZˆÖ'bß2Lú™—.:+¡šú1ÛW^„Ÿš¯Gõ1'0mß3<úª=êÜœR6VvHæPÓî|ŒÀÉ¥Õëró„ÿ‹ù7If_“n*ŸC¾àáA_ômRy2¥ƒƒ½3RåD¡ê]0aÛ?ø(ÝÏ é~Œ)'To¯bõØÎ¨V$7R»¹ÂÚ!5Òæ­€†ãTt»/ÛÑJŒî`9AD”ä˜E9!ø.\" ¢¨ÆOš!ÿ[œ•#ÊñSgäË&N%ÿ{ßÃò¿çû-^Æhác`'X—œŠØŸ­_ðêÁüsû¶&wðø­f^ŒênÎÈ?øŒÖéå¤ ‹iå`k[CΩïH.'¬í`‘¡1ß3âØÑMhšÒºÒlœ²œ "ó’xåÄ\¸Ž¼gP¹–,”;šX”]‚Úƒ¡½ví$r•V?†,3žÄ½£ñ#[ùÈ벬}»£Vdd9‘¬ÜfÌ RáÍ•¿QNZ^ûqŠa­ö‹xs6 µ¸žŸENé(èutôý–]ƒq·ÞbÝÄUpEˆr=š¸´9Ž=¯•kb?Ï/žA©êêŸW'¥¼‘ªçE\ø¢ÂëKgQ®fìŸS;6õvcIL“¯Còú,¡Ç\Êé{ÃñwÊ[ _ÿÞ={éä÷F‰UN¨ðäÄ1­jøgVǪ “%"cŠ8Ë;çö•çá¡á§0~á—0¼ “¼.÷Æk!¾ËQ¯EÔbçß F^ŽÿqF<ÁÖ^%àb-&ÓÔ«”(ÐyNûÔ‚m<åÄû‹sP+³‹|¸ŠÎz¤Çh•±&Û^ÙYN%Af[NxMV—×£—¹|5@¾LœfTØðˆüïy ý£—1ZèAtÉèÛê~x®ýeTØ È->Ô¢>ì\Pfö}¹tˆ¼3¿Û» ô´yÿï£òz'Úgp€cãuêNM9añS/ˆµ5­Â ¿Ú°µ+ƒÉÿˆ^‚—6€ƒm! <ñ6f}RÞœ†|¶.(ïýX¾ou9ጢ®ë|{£Wø9 ÎëÇ[4Ì,'ˆÈ¼$f9aí¹¿Õ‘v Ë,B‘aÇ1yË-lŽÙ‘5¶œ(³ƒ ¶‡·¼ïÚd ZN;‰áóO¢Ç_7µ“©ú‚ý3WÁ^³SnWgZÏ¿ ÿƒw±éÀ ÌYt•ÎSïK;è‡ÝÔ;ü>ú~KÍG‘Îkäb©át\tþîb󡘻p?~‹Þi÷FÆñ÷ðF9€#Öó¼%kKÏs¥å¨1ý<æï»ƒÍ‡obîü½(\M³S^ÊŦžDíZÒ¿K/DÁÁê×cý‘ÛXºæj7™}îoà©ÎýAKiÝÊó<ˆÇŸ³ÿqÌÜsûÏ=ĶݗУ×RÍóãTýp[ççO„÷FØ<}ñž ®fô„ãÀÜ ’.‹Ê‹x­yÏD¾¼:šÒƲê*4ô½ŠÕ'áð…GØy(ÓçDµ‹`[~#¦ëŒ’1^øù‘Òöƒz§¿†ßó¯šâËñþÈf#­Ë:#Zm Ñ\‚Í­3ªOQšs ŽÇñ;!¾”99´8ìÅÄœÒòÉ‹¢áo,ß²»¶­Ç‚É}á‘7,¬Ü=o.X‰å ”‘V ~ÍC¬\‘©bLX¼Qz½wcãro k]iíía‘ægäL-–c9AD”Ô˜E9!θ!JqÆŽà`õ×2Ûwî•/kÛ¹îܽ›·n£MÇîòe;÷ì——yñâ%ªÖn$_–àÃ:ÂΠ.GØ”š{£EõWölºõ±fn;ä²)'BöDCÍ¿”è¢#갎ߦ)Fe¨ðÚ¿>líJa‚˜+á¸4º¬ô¬K˜2B.'lÒ õv=³ª\Y=­ª–@ö iáìì;XYk0³œ "ó’˜å„¼sYn%:ÿ ûM¯*Ç}Öhvf}Ï7Hk®ø­XGit=§»Ó¤‰sóVjéX޾W´>XŒ-'JÍCj°,ë:«Ÿ<$áÃ…£È¡1áÜþ$¿Õ³ûùéf÷SOÒ˜¬ô"ÔÝùA=²OKôýjŠ€”½.á’|xGlaOo A-õã·¨¼Ëu>CÏså¨;ÂïCàüZ!êþÔÏgÃÝ!P.©zûiî¯ÂVÌ¥X@zMn®Þ¨åQj>~_ô:OAäGluHŽšú¬ØIOŒ÷†FØ4«¤^O\sN¼Ü±b=¥—¡ë߆ÞÍ‘úˆÊŸÇh¡8Ñ??,¥mˆdÉaÙ˽"ˆbaWǬê"swìÕšëãž®H/¶‡¬³¡“ö 7¦ã71_…(&ÒÔ¬kzÞ`¯aFµôòýÈÛ>zˉlk§~,¢˜ÈÛm‚”oh1'ØÎÈc¯¾?–DDIO’/'´'Äܰy{ôå⬆&ÄÔ>cǺM[åËÅ:>|0ü©Cõ‹<Üa‘²56èò)}ÌÝž’vZåÄHgíŠòcàØñ:9¤ÞÈÒ”6ef!ö¡žºåÄÅ‘Ea%}÷Ýx\g}ÇŽŸÂÅGêru9‘ív+7hÂpmz%¸Ú¤F¡¶3°úÀY\ DàµuhŸÍ‘å™-íS‰Æ_xîWî6jï€z#ýX}ßú«©Bn¡~õºìz]Ã3íÁ÷Ï£xYõ:2N~=»WÒŸØ n¦aaÝìÎh?cË ñ8KÍCq¿úïCP}ÂÒ ä‘eWcø =´hÚßÔ[·<ƒ Šý=íûµ(·“îxr¤OÀóóWiŠô¼¨¼ÏØÏsö™Oô?~Õgøˆ1Î}´æ_Ò¦Âåë4…Ðbt>«X(ì zyª.Û?þÆeåË®t U5#œþ¼{”d"½7dF•*Ü^µ¶âþ4‡ï|Ÿ£{V1úÓÉ›o4ø3Cõf#šÊ#‘­ß‰Ø?×—£šC'²Õè߬ÇÙ!¿¨¿€±J޲³ïèdQ¯z¡°¦TÐSN¨^®D]7õõ–Ùºcí6ñÓºêQ,'ˆˆ’ž$_NhN%:pØåUÇÆ-Û1jÜdŒ0 [¶ïÖ{*ÑþÿêT¢‘¸?·ì­ÓÀc±úÐ %e9qk*ŠÛ9£ð¸€¸­0ºœPáÅâ:°·É…žG”¥Clˉðó’ÏIwH˜´’™å™¯D-'¤Ü§•×k‰| í4åB«³¸¤ýG>ò-¦wö•wª-ëÂ=C†Eïèë™/ å„U=iýz÷ì5Þÿƒ†šeÛΗŸ1JaØ¡9Åe²2«1òŸØ k߯m—K¸Ǻ>Ý7y¤Â<4<¨|µŸgô¸`hE¢äÐŒ.)å‹j[?<ä ôÔA¤’§ðE½½±ÇV„]9Žòu>(½:Dïç·,òfuU¿¦Õ÷c[¬ÏDzoF•Àé9L!·ô³çŸ|•CFÁ‡Ý]NÑðÚl3´o žû7€‹8̶0†W>?a8=¨ \<È#4^èy%#naÒobî i—úX¬o™(ð*®^V_9ñi{¤G®:"çÀ3:£m´½ßÜV³,Ë "¢¤&É—_¾„¢qËö¨V§19ŽYsç£ß X°x™Þ"B\¶Ào™¼Ìlïò!!â¶[u@hhÜ;øJªýè–Ó ©«`ÔÑ—:…ƒ²œ@øUŒ.â Ë]°#®¡’F—Ò¢棒‹#Òx.ÇåÐb°œ;&{áÐH3ß…Æ—ó£PÀ–‡u‘ùÒ.'\:Á¬õW0Ç`®a×#åg†Öh¹-˜§sx€–Èðî¡Ùm~ +É{°a³fHþb´?¥Ü5ŠÄÉ9ËåC,Ê®…—ö¼‚Ñå„7Òx=ˆ5颒X6fÙœsžÅ¹“&¼Ú¾]}H‚´C\kç—Xe€öýfžñ$Îu…ž;‚Œš² îå’ÚÏó6,Ô;i¦ ="Bú×>ôE!ìâ1dÑÜŸxÜ1Tx¸~³zBi?4Þ„‹·^H¦÷Õ̽¡3Z!±Þ0ºœÀ—çÖ&j.o¸µØ…¡;ŸáqÂ6]â3„ÅOݰ'ƒIuHÛ1s*»Ë§ µþu"”?³$üò8”ç¶HŽÊóô|Éóe'ZËgÚÁºÄêYGŒPìî˜I=âA§œPŸ}D>“‡U´ÜW{'=ô{¡´=Ë "¢¤(É—Âá£'tߩ׸5nÜŠ xãê6n¥³œÈÑã§´Öh¼÷çg¡ZF'$³I‹¼5: ßè)˜:s&Ž®õ‹ÂÍZ«œ€8•è@pv€c®Úè1Á üb¦×4+W·i>PNȧTAºWd¬Ð£þòÃÂ…>˜8¸#*j‰Åš)ƒå„ê 6´ÌK÷2è³æoܼ{§6LD½Ÿó#szŽœ "ó•˜sNXTÙ‡-Ê}jmb´§áPÕËT“ðFZ¯ûÏâ%ì)úiæH°ít·”{iF—>(±üNQ®íË1ÍiÙòÞëî*Ä” >(ºäu¬ukßï¯Kc_§zÞ¸r¢ê>lU^M«œg¹k¸äW–1KFâô\u$ Ʀì:Lz ¿œøÚ÷†Ñå„$<è>u[ë #¶kPÎ5ì{fp$‰1T¯×¡‘\8"Kï£q>ŽøDÜœŠòÜ HY®FŸ€±ÊŒíŠßSª'ž´-5·”¿3/£ºƒº$°k¸6öp\ó«úr" Çûå‘çÑøŸm ¿¨Å¡ði#9óT¢DDI‘Y”‚5á¿b<ïÄù‹—åC9Dé0tÔøèe +_6fÂTy±¬¸(7¾Føó3X8¨%ÊæÏ GXظÀ!Eä(æý¦bc öבxõ÷ôoXÙÒ¦€­+\3F…6c±åžæÃ4A儸ø®®ƒæe ­»+¬ìS"už hEÏ—`é±Û´¾¼ ½t Ù4Ãõ˯{§[$ œ(»VÏíµ|>¸[=‚CÚi¯±]9É£®°«'¢È?ïE¬ÃMbÝïרrB<ÏŠ£0´h—ë0Ñàq—‡¦ù«UIHtî/ß ('d‘¡¸vèo´ì°vZ%…EY?ü:þ2ŽiN5ž0*-õÔ†QNÇõÅ'F‰c²n=±ý£®Ä~b"ù¢‚fƒk›íñb䄘è2""BŽøq™¾Ã=ˆˆèû’¨åD­¸všÿ¨$xßn¸Ë‡vø¡ÕѨ•EàðLõY!,*nÅ<}ÇÕ'b9ñåÄ~$9±Î˜‘‡‘>zäD°Á‘ñݯÑåDœÏsâ”'þZ¦9Qf Fëž#ÔHÆ>fÄÿÞHh9-Ona¼×Fd*SRØ6؃eOøs©žb~H&íÀ[ ñ .ˆÓ—è“Cû”êÆÄ ¹ž‰õ;ª ZŒjš‘öM6èŸ$5Z'þn œñ¾Q#'ŠaÔ•x~¸/ÛÐÂåQRdÖ儘SŒ”¨Õ yô=ÆNœ¦\Œˆˆ¾CI­œÀû;h.— ÞH5æ.äéµÎáÒ?PqFD,'ÂoAÍ<Ùf=sž!hë6õ·ó¥|á±#öH‹„ÜoÒ)'T¸³jcôœmOê{¡ŒaìcFüï]NÄøôÓ†­ÐŒŠñFò!· ¯ç2$ò¾7Ê;ŠrgñRŸ‚üßú°[sšP«4h°28îÑ971±¤‹\ŠXfé‰ýÚ Äçmh™\=ç„M™ÙqN¸* ˆSÔ§@Õ-'"8©”º¸°ÎˆözFjS/…‡¦a9AD”´˜u9ñôÙsx6k=¯DÓÖñ<è…r1""ú%¹rBÚÚ>^}ö Ëšû±UÚ ½|ÙåC:¢Á^‡Y$b9Ï÷ÐZ³.›öçÇ™D!ï–1~š³u¬ÂÅd ¹ß¤SN¨ËOòuÞÈõ׳ñ¾Œ}̈ÿ½¡UN8Žøwå„,ò-fvUO˜iYývÆõ˜b‰À­iå`ce‡döå0å¶ò&€ê-Ö·H¯žT3Mlˆ÷0ÐHÜû« ìÄá$Ö™Ðz«ÖB´ÎÀ‘,eK¬k]ªXXÓM.9tË àãæ?à.îÃÒ…Æ^‹³| ;1ÙlXN%Ef]NÑ+é•ÒNÒ™Cê~©0€IDATôÒKÐùlNÍ]!ŸÓ²Æ^lP¶Q³œž…-cÕ‰E™•èÇ÷È Ô¨ªÞi?ÓYÅ¢ ¹ß¤TNàó#t©§~>-=vay\gÏ2ÈØÇŒøßZ¯¯]¿ëøWG‰c³¢Úƒ ƾá#®cl1QØÃ®¢îÇõBÆC´uäC"¶ýîx&±T‹|â‡j.êÃ(Ü­Á«èŸ? 'ûkFCX§Amÿ§ßc‘ýPÝU=ÊB_9¡z¾5åû°ƒUþ¡8eð”6Ÿp¬ßÏêûd9AD”ä°œ ""³”Ë „>FêC;rκ†!­Äm¼‘fÜ=õaú$j9!­îößøU3GCË#Ø©oÅO/1£fÔDé…¨±Ew~Š„Üo’*'¤®Ú'Í!):ŸÆþ`Ãþãóg8~_ù Œ}̈ÿ½ù cZ«¯·¬¾ ˃×k¼½ýÇž‡ë]#|z†-}ä‘Ö­Îà¢ò~ ¿8ùåSz¦@õEO ¯?^‘xàSöb„‚Mt=÷,ÑT/±¬~jyÔC2—Z˜¯5_Fø/‘ÏúaËŸšbñm=¿Å¡÷°¼YM¡ ¿œ€*ëZhN5*ýœEÃkT…7'G¡¸¦Ä`9AD”ô°œ ""³SNÌ…s‡C˜´ò"¦Æ‘éÛžài¬}ÔDÜ&&dTŸÊÒªÞ2d;÷¥— ãiÃ#»œÏ¦°|#Üåwéñz¬C3ŸKð?x[ܼ%GPµ‘úð€d¥¼‘nàuèéŸûMZå„Xà5þê½(ú¬–UW¢ú¤3˜¶9+öÝ„ÿ–˘àsõ;-ƒk_TÐ9íª±F¼7"qÚ[=‚F”%nmöcò¾ûØwúÖl»Œe×B¥Ç¯Â½µa[f!rõØ‹>K®bå±8|ù)Žž»UëO¡që…°”ç™òkã>¥lŒ0œ¨)ÌÝK2Q…RÄMLúM3DŽ8fô^½ oÖ·B yž W”žu7æ¹V…à@ŸB°•ɰ‡eªâh2ÔË6îÄŽ­ë±pêÔÿ%-,­R¢@ѰËé+'$·¢zjqêRq?nÈR¥'¼­Çæ»°q¹/F¶¯ŒŸía•©8~É &ôd9AD”Ô°œ ""³¤]N«Æ'q*VG˜; 1ÂO!Ÿ¼S¯¹ßGqHÏÂѽœ¨¾àÄòíÈ×ó#íäæ€Ë†À'ä~“\9!Q}| ß‘+á¬u:N½)å‹Ê›>(Öaìc†Qï Õë{èÔØW]ÅŠ¦QáÁÚMê‰<ãˆEé…(:íÄõbhûr=³‰q¸7]¯g4ñÂ/ŽÁÏb†¥ò=ïd«±¼ß‰?Ò‹âÀÖÅ& öÉ6îaM‡Âp” =±N½wâþñAÈ!æŠ0PNÈ%Èéɨ”ÞI=7…N¤ûÎ\³/>À¢Zbþ –DDI Ë ""2KIµœ@ÄKŒ’ç÷ëñMÊø-Ê wîaÆŒ](Ýl ÜÊûÀ²ì<¤¬¿G„ï¹wqž¾1!÷›Ë µ<¾r£&lGÉ&‹ÕÏAiØWñC¶V›PÊY,<ûou~8c3Œ~oD?…Ϭ]ø­±\ÊùÀ±úäj¿ã.ˆ‘’°8»ÿúŒÜ‚¢ý‚ú±ÚUZ„Ÿšo@IcyÀÇ•÷tE1bÁ:#Zm Q^_p¬_>Íé:ÿt:>a7Í¡¶¿`ðYåOñ wvÍFe#}jØÙ:Ã>U.ªÞ·ßÆGˆù.4§5XN¨E¼<ÿíPé—Háâ+ûTH›·\Œ3/Äí¢N=Êr‚ˆ(©a9ADDDDDDD&År‚ˆˆˆˆˆˆˆLŠå™Ë """""""2)–DDdV”]2Ì""¢ï Ë """""""2)–DDDDDDDdR,'ˆˆˆˆˆˆˆÈ¤XN‘I±œ """""""“b9ADDDDDDD&År‚ˆˆˆˆˆˆˆLŠå™Ë """""""2©D/'*{x2 Ã0Ltˆˆˆˆˆâ“èåQB°œ """""""“b9ADDDDDDD&År‚ˆˆˆˆˆˆˆLŠå™Ë ""2+ʳ0 Ã0 Ã0æ–DDDDDDDdR,'ˆˆˆˆˆˆˆÈ¤XN‘I±œ """""""“b9ADDDDDDD&År‚ˆˆˆˆˆˆˆLŠå™Ë """""""2)–DDDDDDDdR,'ˆˆˆˆˆˆˆÈ¤XN‘I±œ """""""“b9ADDDDDDD&År‚ˆˆˆˆˆˆˆLŠå™Ë """""""2)–DDDDDDDdR,'ˆˆˆˆˆˆˆÈ¤XN‘I±œ """""""“2£rB…—;ÇÀ£–'*{4„ÇÝR)—!"""""""sc>åDä3¬ëßUjµ@mφ¨Rw0Ö<ŽT.EDDDDDDDfÆlʉÈ{ëЩ®'ª¶‡>ÝPÕ£1:.¿‡í…TŸp÷ÐrŒé× žžMP½~´è5«®}R_ú'WÍFßNíP»^cÔhÜ †/Ãé×ê!‘ÁW±nÖ(´kÑ5ê6GÃΣ0}k ÞFÐ}†cþ“Ñ­mkÔ¬ÛuZvC—‘>ØtýTÒ}ßÙï‡!Ý;¢Ž´n&ЦßDÌÙ¡š›‘.3)'Âqݯªy4D“9—ðáÚb´¬í‰ªüµŒ Ow‡§8ì£Vs4í1={tA]ÏáØðL%]ýǦuB5éú*õÚ¡}¿¡èÒáxt]‰[bŸ±°»™Ñ-ÿ…¹óæà϶ꑽÖ= O6CMqûæ1fÖŒ>MÀŠûˆ¸µíêˆÐgŠ7&{B‡m0pç+ðè"""""""ÃÌ£œ½ŠÙ4BåZí1íÜ ,sÛJÿ®Ý ³.~Q/ñvjŒÊÑnÁuhÆJ üã'yä‚êéVôªÛPºMGL:ùNSDàãÇ/Òÿ«rh2êÔòDµŽ q&è^¾|…§'|м¶tY§å¸KzÉIõ®¾8üðÄA%ª°0ˆn#üœ·\ŒTñ‚çžã³¸ƒÈ0„ÆÚADDDDDDDJfQN|:3ÄŽ‹¹8ýY\†K>QÕ£!<§ÅGéÕ»CVÏS.,æ\ÑmÂÎÎA=¹<˜Ž£š>#F„<2£ª‡˜lS7Ušü…Óa@øÃ]Ü¢ªˆËjµ@ËÁ>Xñ¥ôh$ŸobY¿Öòu•k5B½Îãá½ë^sZ """"""¢8%ýrBõ‡&hvúõ¤Jã)8¢’ˉ¡r9Ñs¯ê+'þR— gà˜Î$XÔ].'ª÷Z„gÎáôY­œ¿‡7šc3"ßÝÃáU³Ñ«uSuIQ»&«‰þûV`lÏv¨.^Òmæ]ÅADD”0a8Þ/,-ñSïcê2œˆˆˆè;”äË Uð ñl(íè7…g‡>hß5*ÝàY_\ÞƒwCq¨‡Gc´_sXÇÛ'xòN…ȇе®zdÅäS!šÃ:"ñþñS«Tx{pj‹ò¢Ù$ì}©5ÜAõoCÔmFèçÏ1p†>Ö‘­PEŒÞ˜ua_>áSÔÍTŸp{å`¹ ¨ÖYu#""úz‘xä] 6–vøŸ”dv…1øŒ<¬.†*óª»J×Ûñùfè ˜3‘¸=£¼ô³:£Ä”[±'&"""úŽ$ñrB…ç[Gh&¡œâøhŸpjz;¹¨1`žª"pgí`ÔŠš³ç0ôí×<»¨GR¨^`ÇÈ6êÑbBÌþ#Ы{'Ôn4ÇÅWQŸobqÏæêõµ„Q3|0uÊDôj×]VÞ—6#puAO4ê2Bºn>æxOG÷æPEº¯~[Ÿ#ôÔ,4lõ'Nœ‹Ù¾¾Ù]Œöhˆú^ÇbÎöADD‰ v9! çÊ>¸£}Y—*¼Ú?#çn‰{!x»¦)¬ÜPwż{ð7¶Ì÷„Í÷åyˆˆˆˆ¾I»œˆ|ŒU}šÈ;ùg]€â{1|9ï‹&¢¸¨;+H›i‘oqe³7þìÔµê6BµmÑnÄRœ|©nTî`—ï8tlÕÕk7†GÓnèû×<Òlá©Bnbû¼‰èÖöxH·¯îùZÿ9+.‰Ã6"ñäÐ|üÙ¥êÔk$Ýgsxv‚ñ«. (ˆ¸³ãû÷@CÏ&¨Z» j·è…~3¶á*› "¢D¦,'¤XgAËMÁ1gG2ër" 'þÌ KñsY¹"Eº´°¶r‚[ú °³?¯R¶ßÃÓTÑw%i—DDD:bÊ «\eQ"½ƒ\BXÿ< §¢Žé3TN„?ÉţкfäË– N®pJ—¿5u7µ†ç…DßìŽøŸMô9úÇft@©œé`ï©óUCGßsxúÛG7ǯÙRÃÎ>Òü\ }VÝPÌ3¤ÂûÀÕº2rg–sH‰4y+£Õ„=¸g°]ǵ•#Ñ«s4mPYœìÕ‡¯8ä@‰ºÍмm7ôYpNž šˆˆˆè{Ár‚ˆˆÌŒV9Qh46ήg1¢À*%*̹£ž—ÁP9v£ ;#™æpdVöÑÿo™é¬ÒŒ½ˆ*',‘1A¸XkÒGjü„b¥~ÖŒdˆI2ûÂr6¦uø|i:ʦå‰tµ+œÜÝa)?VGdh¼bПaxàßi¬àìž ÖéQ}þmùôÕDDDDß–DDdfbÊ Ë\ƒqòÝY . .,2þ âP>CåTÚ5#æíÁ…ûÁøúç¦{ ¹(¬ÜQsñKõ¡!Ñ儺ô(Ús5N\½ˆ­CÊÃ%ª°N‹Òƒ7á̵óXß§$äËoøEuù>ÕRjWCx¼—Öý ·üš ¸?›œèqHyÀbŒ°›Þ¨”ÒÉœ*aê®iøÝÉÉÜ*`âó:H…ˆˆˆÈ,'ˆˆÈÌÄ”?õÄP^mn‡ôrÁàŒ¼ƒNã“*~úÊ ]‘|QÞ^ Î(<.@=òB«œ°Hß;¢Ž¡Y‹.êÃ,,sÆI͹=U/£šƒX‡RvØ+Ï¡ ZŒêŽbYGdîs<æ4 ¡Ñ5“Má„‚£¯ ‰{ëz¢x:$o¼/#ßbkÛ¬°J^­üo¸ ‘ùb9ADDfF«œHß{DyUN%RHæZ³n¿ÃêFîÒ¿åħ[Ø4¶*þ’É]`i¥.ÔqÂ/c®ªwüµÊ ›òsc¿݇ŽéÔ‡iØÕ]Ž7Qëý²ÍäÒÂÉÛî’ï/üüHäµ}ØGì8 C÷CqNl© ¹…kÔ>âI®ˆ˜‰ˆˆˆ¾C,'ˆˆÈÌè)'$ŸÏ C~;Q8 mËUXÐ2uìrBõ[Úf×Ìùà‚ô¿5Cç~C0°G-d±1\NØz,æ¤OêrBž€Óv×ÇL~)Ê WE9ñ÷pä‘Ë ¸dÿ¥ÊTP¤êN;ÏQDDDD`9ADDfG9U06¶Î 1zÂ. ü,æ{Ð*'¾ìDë”ê‘Ö%§ PÓ DÞŸƒ2òa‰[N¨žù¡ªæ°Ž,½Æyh ÑŽå™儸æŽ*¸iª¡]Nì@«êÁ¶Ò<<‡jD¾Á©ÑåਙÌ21Ë 1!¦ouÍ¡&n¿£×ºkxñ9áŸ^ãþ…]ð=GßG­€ˆˆˆèÇÆr‚ˆˆÌŒárø„Sƒ Ã:úŸÚ‡u<ÇÒúiå‘bâÌ9‹!ÖÔ°vÊ‚ô©ÕT&j9!œš5§+UÄ¡.G­˜ˆˆˆèÇÆr‚ˆˆÌL\å zµ-2ª åÙ:"ŸÂøÆ%ÎÅ –i‘¥T[L=úFƒÕ7('ä›<<ˆY½â×™àlï+Ç4ÈðK-´¿ÿÄ5&Ñ„å™Ë """""""2)–DDDDDDDdR,'ˆˆˆˆˆˆˆÈ¤XN‘I±œ """""""“b9ADDfI¥R!äÝ;<| £ÆMFÓ6PÙÓa†a†1ðœ ""³#ЉcâÔÙí5‡ŽÇóçAøøñ#Ã0 Ã0 c†a9ADDf%ª˜3a*¶ï܃OŸ>!44 Ã0 Ã0Œ™†å™q(‡1!Š QJ‘ùc9ADDfEÌ1!å`1ADDDôý`9ADDfEL~yøè åÅDDDDdÆXN‘Ygåxõ*Xy1™1–DDdVÄ©¦Ä¤˜DDDDôý`9ADDfE”DDDDô}a9ADDf…åÑ÷Ç,ˉÇOžaÅêõØwà0=~¢¼šˆˆ¾c,'ˆˆˆˆ¾?fUN|üô CFŽ—7Lµ3qÚlDFF*Oš"bVyØ”™…;š‡º¯ÒÚd@»Ý <-Þ‡µhàè„ÜCÎ!\y™_¢oŠåÑ÷ǬʉCGË¥í»ö†ÿòÕòyî£ ñïo%òõe¬Ý• 倻“3¬R"Uö¨úǬø \}޾üâ¥+òåµ¶@hh(Þ¼y‹: [¢c÷¾ð]¸_¾$p‡_ásÀ"4ÈæŠdÖ©‘«j{ô5^ãG {³*È‘æW ¿¦¼IÜ~ˆr"g„KóÍø¢¼êG`Ôëòƒ?GD_åÑ÷Çlʉ[¶Ë¤;÷ìW^…î}É×Ý»ÿ>|ÄÐQ^Ñ#*†ž \ÜxN ~X¸•DŸO ¬!T¡¡:—Åë”y‡žc'Ø>_ƨbÎpüQw¼)'~ôçˆè+°œ """úþ˜M9±vÃyƒtåš Ê«ÐóÏ!òuÏžE_v÷Þ4ÿ£³|ùµ€@­¥‰Ç jÁÑ:Ê̼ex'SK؃}˜Ð¶2r§K[§´ÈT¤zÍ?ÚÓaYND<9Š9ýš£|‘|Hãê »äÙQ¸Þpl¼«U`È;ÁÎøeÔ˜Ú¿fI ûHós-ôô¿Šwª˜EeoaÈ(‘#=R eöRð² —Þj-(?>wäxînÁ€‘ÂÙ©ÛíÄ'Äÿ¸"î.G³Ü)ÌÒÿ‹Žê®xu:áƒ.ÕŠ ½»›tû,ÈW¥ ¦í„XÕŒøÙœÒáŸðt¿êú Ž©QÇÿTˆÀ³#³Ñ®R!¤us•ô3§ÊY µº-ƃGÙ¨ðêÜR iUEód…«ƒ+œ3FÕžËqUû6òý¦AËÍqrnWTÌ— ö®pÍRõGìÀ}Eñô&¶.‡¬)Ý`í”yªÀªË«ÐÌÍp9ÿs$ý|Gç¢SñóIÏqŠ\(ÞÌ ;ľóø^ YôÏóÇf¶ÇoÙÒÀNz¾ÒþRC¶>@ØÇ›X3¤1ŠJï;‡TH_¤Fí~ëP“Ðû»1¦EäH›Ö¶.ÒóV¿7„Õ·ôýtDßË """¢ïÙ”Wå Ò6{àóç˜Ã:Î]¸$_Þ¢m­¥Õ6mÝ)_·~Ó6åUñS½ÂâÚɑ̭ V¼VîåëŠ|²-²9Ã6k ô˜â‡%þó0¦]Y¤²uGþ>½ cˉÀi¨R¸ºŽ™¿5±|FWüšÒö¿OÃõ¨=G¹œpDªl¹±x;Œò]þ¡_\°µÎ€š ïÆìd†Ý‚GFXº@³±p©?f o‰‚Éáò»Î~Ô,'?>ig¼n´Ï›9ªwðñ0mû=y]ñ=.UÈœ>àƒFaWÙ {Á¡ÃÇp5HÜZ…×ûÿD>''¤.Õc}—aÉ‚©è^9lìr¡ÅÚ'ˆîqäŸÍå»õFÑ”ÙQ¦í0Œ?«Âq{.Ê»:#cÕ˜µl=VKÏõÄmP±±7nœÀ!Oüš¡`•Î6Ã+7¬ÅÜ~U‘ÎÆ ¹œŠ½ yN3åÿé 4ÁÀ™‹±ÔßC€ƒur”œr=æ9ýpƒ ¹Ã­(šŽYˆe+cJodÍ’™œ —ñ=GoöG~'7d­5sV¬Ç ßᨙÍ6¹{b·Ö{1¾×Bõó(Œl¿uR?çÞ#Q'—,œK¢f•\H_¦+ÆÏ_Ž%s‡Á#»+,’ׇßÍ+v#~q…]®†6o5Ö®öÇìñýàY±3–=ÿ÷‚è[`9ADDDôý1›rB;qš¼QÚ¡[_ø¯Xƒ‘ã&E¾qúì9åâ˜6Ë[¾îÀá£Ê«âvr;ºøä˜=ƒ>á`Ïܰt®ˆ)ÚßnÀ©ÁEamW #/ivS,'tEà기¶¯Ž¹57”w<í`‘álx¥µ£z~sƒe¶¾8,ïu«¼®%RÚ¤GÿÇ1€¸|Ggd¶IŽòsî©/×<¾ÿY¹£ð ãxïþ§žÇv}³;ê²q£‹8úàPœŠ*C„Ð@L-—–Y{bÔ嚟-™m>´ß¤õ˜¥g{m3ØÛ•ÀØ€x_˜¸©žÂ§ª+¬ Ãå¨!ê9MÓËŸiýða—¤t'Ø”ž‰ÛòƒQá©_8Y§“žÓ§Z/—Çý+Ãå„Ìàst^%\áPfµnüùôPäµuEéYwc=±éy-¢~žŒí°I«Øøt¸²ØØý?{÷Uŵ¯|½+½ ŠbïÝDSL®iÆ»XAÄ‚‚DÄ^P±WìcG;öcAìØ{4V°ôS¾73ç8T‡ûýÖú¯fæL;Ì}î½÷À¤úÑê9’°¿7ʘƒëFU/ţŸÙÊ—>Ã;ˆþ% 'ˆˆˆˆ £ '^¼|…á£'¦‡úµdyd²ŒÖ\ضÒrg—.Ò$™9–z¾e­aÞh)ž¼«U–vC«ÛÀ¢Y0žém+¿=õ-íðe€ú/îïN¯×¸ÀÒòLÖ4Ê¥†§ wÚÝÑ Üû3Ì-¿Æ„+â¶IØåU&źcë …s?-»eÓþ®>¿BN¶qzÛfÁ༲hxËïÌÁ7½¨7U«÷D‰ØUm`e^ ŽªÃõµY5SŸ—Ùå)øÂÚ圧aÏßo> ÑœŠ}Þe`Zy"4™’ú¸v·@3ÈB%;º9Á´úœ’¶MÄ6(äØ [ôî©üú4Ô³|¿pBq7ßYƳo"îͼÑÔ˽ð.c ·0è…Ú ¾ õõØ ÇÑþQƬDSk”í¡3oŠâá"ü$ÿ§…T!HrúU¶U-Ì;ò}–ˆòà """¢‚Ç(‰¿ïþƒ]¼¥víÑ[šâÔ™ó?zÁ!kѼ­»´nаÑÒ„˜¢ö<¥eâkGŸEÇèí1„F»ŸÐh7ûfÖ[† ¨%mA;”é®;o‚(e':;X£˜÷~Õºl†©Â1o@|÷IU+ê+XXÚà?b«Ú OTzÚ`bΔmÝ`kV>RÅ1'Xب0L?Ÿ MŽtÊ,knÓZ£qV\§>?³ogãV&×­óÊ¢á>eÌ‹Àu“8{…®´ãCQÑÜ­CÕ‰ˆúÚªfrmbPpkÃ@|Wº0þcQµZùaá¡ÒœYS æÌJ soŒ:•ËÁÁ¾,mìafn“JƒqD'œÈì¸)ØíU ¦U†á¸¸B¼§¿ ÷ô³I†÷4~Ú¾kB̬îÑáþ(c¦=…ni`Ùú.4×3L÷z”/V£…µ¾˜|M'(R>Y†_­ £ÁüûêJÄ_XŽn_”‚‰ia8Õë„«Nã©á—Bô¯a8ADDDTðäûpB|¨&˜˜6k^¦¯'Âôê5@Ú&`ú\iYÄñ“²þҲ۪֜ž1tÔ(Y¶Ž‰Mï-ñðÑci™R©ÄŽÝHËÚ¸yàå«WzŸz9nÏns³h¹Rk¢ÆÌÈÎaxM[X4^f0D~s&¾¶´ÃWÓndsX‡/׸ÂÚ¼"zìÕîÀ¯Ä“ÅÍaaÐð´‚­[tó9îÌþf–ÿUëHÆže`"-ºc¤Fòª6°l¾RõùLÎO%ç%6¼«6¼Å! ßZÙáóIW †uD·‚¥yM ’ZþȲQ)ÅsÝ650ð˜~L#JÅ=Ë¢C{„è4¨æ.Îuñá„ðÙ­]PÈÉ;õ²Å? ðƒUfûÐ’Å=’ßž…ÿZöhЕƒï"‹ëÉq8‘. w÷C­L‡çý;N<ù>œC ñ¢.]Ñ_e`ÙÊPi[q؇¶9 I˃W¯ÓYþ.ʘ­è\Ö…JµÄŒs¯ß2·A Žû}SÛ¸¢ÝÔLÀ‰á_À̲^&ÄTâÙ2gXX|‰±´š“ʬq-…B OqòÆNX­=!æULªo“*C¡>×;¼Q¼š/ Õ Tâù®ž(g^?/T7F39?ͶÙ>/Ù_õ©­áò˜ú=LkÃQí6uêuLoP4“ 1 ÕY‘ƒNðØ©3½¤Z2¶v)“²ý v$I½*Ù¼g8¡À“åΰ1/÷°X­ß îÌkëwMˆ™Õ=’©& 5©ê‹=±YýÆåà»ÈâzÞ?œ$ïGRv¨ìwÒ ÇÑ¿áQÁ“ïà qމNž½ÞÚkBãî½ûÒ?Z,Z¦³<öù 4oÓ¦ÌÔYþnJÄzE¬ñËòø¢]øOšiÓ&ÿO74þ®?¶¨{"(£wûšÐ¨¯Ð}f#dõ2LòV½J´öÀƒ9z•¨üê4Ô³¶EÅA8põ.î\:ˆ%}¢Z媰µÒoxÚ |íº(ýEWŒZÕ+çcp3Õ«D[®ø'£á)ÿ+ÛW„©Ý'h9tV„®Æ‚±Y½J4³p"ç…xìòª 4’«Ãyì „†®DØù8án*wl4>/lƒbßø`ââ5Y>[ý*Ñjèlð*QÃFµ(q³/¾hÙ£f-ÅêÍ;±iÕ t«ç“2Ý–éP y;órh4qþúû.®D¬ƒ“:¨T©Ôû ëÅ…_rü/:O\Šá:gnêÕ?Gõ"v™ìC[Ö÷èÕáá¨cg ›ê­Ðoêb, ^ŽÀ)ÃЩa=ôÚ¥ _²ý]dq=Ù '—Ãí[7ô0KÖmǶ+0ε.¬-k£¸î4¬Dÿ†DDDDO¾'<|„¸8ý±Y»}ç.’’ ß)ðèñ“÷{k‡ ñÎLëéŒ/*—‚•…5L¬Q¬Ê·hÞsþÒúC}ÚãpÌñiŽÚeœ`a]¥ê´D¯ “ˆÖnýeÒø×'Ä¿ôßÚ0k•¥…-¬K~†Æƒ6àêßKѨ°^óp]øŸz‚#³<ðU…â0³, §Oœ1 ä âõÛéÉcG€~¨Q¶Ö°¯X­†¬Æ_Úï Íäü2dó¼ †5á˜Ø¾J؆¥c-xlˆV÷.P öô hõ5Ê::ÀÜ®,ª7ôÆÔýuÿ ŸE£Z$»±}¿A§¢05³…UñøÚeÖ^}Ë›;d°gœ+ê–>cU÷âóx¸ÙEÄW®¾O8!=>ˆÉ@…¢ö0µ-‹Z-Fbóík˜ñ]‘Lö¡ëm÷èù¹Uêú*—t„¹…=ìË}Ÿ»OÂŽ4÷8›ßEדÝpo"ñ»wsÔ©P–æÖ0+\ÕtÃø]÷t†£ý›N<ù>œ ""ÒÆp‚ˆˆˆ¨àa8ADDF…áQÁÃp‚ˆˆŒ à """¢‚‡á†DDDDà ""2* 'ˆˆˆˆ †DDdTN< 'ˆˆÈ¨0œ """*xN‘QiÙ®“þ"""""2r 'ˆˆÈ¨¸w÷Áë×qú‹‰ˆˆˆÈˆ1œ ""£2~ò ?yF11†DDdT9†‰Sgé/&""""#Æp‚ˆˆŒJ\|<¦Íš?þ<¤¿ŠˆˆˆˆŒà ""2*J¥>–zO0  """*N‘ÑÑb 1¤ç à$™DDDDÆ‹á%1 ‡xˆsPˆ“dŠoñhÔÒ…Åb±X,‹e„Åp‚ˆˆˆˆˆˆˆòà """""""ÊS 'ˆˆˆˆˆˆˆ(O1œ """"""¢<Åp‚ˆˆˆˆˆˆˆòà """""""ÊS 'ˆˆˆˆˆˆˆ(O1œ """"""¢<Åp‚ˆˆˆˆˆˆˆòT¾'’’’±|Õ 1CüǾ»„í‚CÖ"9%EWDDDDDDD”åûpbðð1hÔÒ%Çå?f’þ®ˆˆˆˆˆˆˆ(Ê÷áDÓÖnpéä…‹—¯f»ÚwòD³6¥Ï¿z‡KW®!!!QoÏF@ñó~* óópW¡¿2¯ÈqeÖ/(YÝ[b”ú+ ””ã£ðiézn„¿;DDDDDDF$߇b/ˆÎž½ô¿•¸½ø9ѸÉÓ¥ÿnÙ¾"ŽŸÔÛòíRv{£ˆ©%þÏ¢†NÓ_îÕ–np4·«‡q—dú«ß_^‡ñ‘X½`?îë[†‹ß¾œÖ?+ á„2G–#"Z÷z’ Ae‡Úè÷g‚Îr""""""ú¸ |8qõúMø‡&­: Co½-ßN 'Ìa[ØC¦³X(cÚÞ …anY°Â‰¤}¾(Uy0Žä2J( H.!R>_‹ÖŽß`ÒU¹þ*( Ò…åS>œxÛ²w‘ ë†piWfúâÏ$ý-„Æë“•hf_-ÝaW Â‰xì[ f•2 ' žoêG«ú™†DÆ&55£ÆOAøÑãú« >rLÚÖ(‡½QÂpâ-¤pª!B† Šeyxî~£·…ÿ5†u‰î]Ú–™ ëH{pS½¡F)GXØ–D¹/ÛbÀÒÓˆÑRîaÇØŽø²‚,¬Qê‹·û<‚šÙ† ×±q´;¾®\Ö¶NŸ¶Dßà xþG~"Ç~«ïæàvÜ5¬òm„Jްª5'Ä !ùö΀6 ê¡b‰¢0·-…Jßy ààSHÍsÙe6­ qH‹‰¦¬Q¼ç¤ª÷mjã‚Ð×égñ^DŸXߦ_¢tX­ˆÚ}1ûà#á3RôAIÛVXvïV nƒÏÊ®·8JÕm…AënàM$e.„ G«zUá`ëÛRŸâ)ØvG÷“Òqlœ±äïKîÝUŠ9À̶4ª6ôAà‰X¨ngŽŽÿÅ-¬´®Ó¦ŸŒÅ9™zæeÐãí+È…ëL¸ #ÜðUåR°²´öYŸüÒã÷>VŸ'Qö=ñ-ÚuBcgWDËz(Û‘£'¤mÄm£cbõWý«N¼…*œøS.ŸÁˆÚvpìº:íqùmLÿÖ¥~ûOB]  œ-èRÙ•š£ßÌ`¬ Y‚‰=ÄÆp|2è^jÂå ìêYfBC¸~ÏYX¾vOíï+Ö@År6ºá„ì·, ÓâßÁkú*lØ‚i=¾Có’ø%èTGWU<Ðߥ ŠÖé€þ¦aÒâ#¦UH9ÿᅦˠéZ½›B¦Ã½Ž# m‹ÒùÜ8~“›:¤TGŒ@ø‘£8që”™†J¼<è‡Ú¶¶púÞW8N(V-›…¾ªÂܲ:ºlz’ÞÈ–íåP³vTiî‡ËÖ dÉtýÒ …,ëbèÉdÍN3‘ˆs?¢°y |Öeæ¯Zƒåsýá\£(LJ¶Â¢[÷^uœ²¨Q»:jµ…¹Ák<œ«A!û˜|A§íƒÂ ¡Ñ*¿1_Y‡sHŒ:HÃÙ‘ŸÁ¢º?N¤* É´3ZÝÍ‚¡ÿR ùíÙ¨oi‡/®C.ì-z…3,Í«£„öÀAÚq ¬¬Ýs"{½Ë¤L/ì|ñoÞh*7æü óôã«óZt,Ói< (îÍÃ÷Vöh¼,Z}}Ù'äwæàázêM¯G›±«ÚÀʼUíDÕh/ŒŸeô2¤Äà*¶(âµ7ó‰GÅ}…´öUéO‚‘ˆ^eP¨h7lQ¿XCuñzže„8"ås;Iù8,Ýî쇹rÊ—ë"|§E¾A¯Uç­iDàø©3é=$.\¼,…š`B\WP(•J‹•KEDDôoÉ÷áDèºMR¨ )Í?¨SRRá?f’Î:íÚ´e‡Î~>4œ€â.f7°‡u³G> åV±Ç'c.ÍõL‰¤-è`gƒ2}Âuæ"¤ìDgkóÞ/Íá öÊ0µj‚ Gz3 (îcnC­ 1ôkaùt*½á®¬Û X«§GºÄ¿±szo´ø¦Ê89ÁÚÆÖv(djFK5ù쇩áPƼ\7þí?íøPT4w@ëPÕUƒ¿ºíÖ‹ ÒÎ`X [î¶;‹pB†K¿îSSÃû9®OýfV?aö=Õ:ÕqJ¢ûý½¥áÈ€j0±qÃ:©“HöÉܺNeìqLw©;3+XUhˆ®“Âpñ'ç¤ãБ£Òÿö5o뎦­Ý¤ÿ—Ú (…B‘^bpÍb±Þ¯´Ÿ%†DDôoÊ÷á„è\ä ñ+ý£zÿÃHJJÆ㤟û ޵ÓkõºM¸tåšþ.àޭ燅PàÁïMaeÓóï+øg”±þ“¯‰ë2 '7ÃÕÖ¥û6 '’Uá„“j‚IU8Ñ ¥ÔC‹âü¢N¨~6©Ö+#NàØq½:‰»qâ? „Œ}>Īöåab[í'­Å¾SqõÆM\Û?Ÿ[¾g8q¸?J›Ë&Ãñ#iÇü¤F{ÛuñÒÏú þŒ íº„û4¾^æ!ä¸&…¿"ð¾v8áú{KÃá~UPè}‰\½Nb/íÀŒ^MPÁÖ ¦¥›bÊéxÝ^DïéÀ¡#éÁ­øßØXRÑ/cÑsÞ”tý&?9áÿ~pd±XR ¥gI|¦ÄgK|ÆÄgå6£'Dbð þÃzËöÝè;È_úï~CF 1)ã¯ØññoðÏýé??‰zŠ¡#Çc€ßHiûAÃF§¯ËÝpBøÇpÔJ4+logý…­žå`ùC îHmáL Ù9 ¯i ‹ÆËðDX‡8Ç„¥¾švCÖñl™3,ÌkÃï”^Ÿ~Y$F~b«5¬# ;=K¡P1lVµ³` h(þ™¬lQsÄ9ÐDš?ã=à ÅÝ@|ke‡Ï']5î-4ü-ÍkbÐq­á9j´k(ñ|u{X›WC¿#úqOvt/ “bžØ¦3çCa4 z¨;¬Bù Kš9h]—±Ù 'þë¾å;kб’ ¬š­@ÿ-HÉÑã§pìÄiýÅFIl$Éd2l9±Å]k¡PÓ(Ô® L;•‡i× ,ëCKx–ÄgJ|¶ÄgL|ÖÄgŽå&£ 'ÖmÜ¢êšÜ¦cz‰„Ý¿`Ÿ ßÚZ£x·Ð¼ #~]õ\ºçdp®¹t†ÿÔ{-üžYý™¸ÉÑD:4=&6ßËeaÒ¶ŒaÊÅb}´Ÿ1ñYŸ9ö  "¢Üd4áÄÝ{÷Óƒ±ç„~0!'ÍÔŒ©».k ±×Åû0'Äbcg1µÄìÛ`ÅSÍÿƒÎ,œ–Fï†wµÂ0¯Ð}f#dõ2LòV½J´öÀƒZ¯Á6¯j05/‡ïzÍÄÒÐ5X<­Vûµ«Ú¼Jti«ò0±(‹¯=ÆaöÒ•XúûŒîÕõZâ†tª™Éâ\¶°¬ãƒÇnàïÛ‘Øè…/jÖ@žJD­j‡ÂfEQ«ót¬XŠ%»®KÃP ÷­DÜ±Ñø¼° Š}ド‹× dùlõ+6«¡³þ+6³Ùh7”‚K³£ˆ¹ê¸ÇüµX8"ýU¢¿ßÊèâ!Ç¢ÊW®ˆj­†cvp(–ÏŠ&•íQ¨h#̺šq|ù­9øÆÆÿõÅìõX¹ò îÊ3;×\¸NÙeLiñ :ž‚ÀàMغm‚†;£¼•¾˜t œ“(ƒfn‰g/b¤¿æ2˜`±þŸ5ñ™Ÿ=Í\DDD›Ñ„×oÜ’&ÃÌÊ©3礙èÅPbÄØÉ9œÄíD×’6ppÝ€Œ¹&3'DiÃ1ǧ9j—q‚…uq”ªÓ½‚N"Zÿ/âÉw°y¤ >+WLhè:¢än˜xàŽ ÿVÚá„´í=ìžÑ ¿Ö©{[˜Ù•F¥ú®è¿ì^Iç”Y€ ¡Dü…ø­aM8XÙÀ̾"ê¶w/`—ŽZá„@öÛ†9£º“Ì…cÔtIYî[ØÓ+0 Õ×(ë(n_Õzcêþ‡: ìl7Ú³¢ŒÃåucàR¿íìaU¼&êwœ€Í·tÃ*U8Q}vœÅRŸ_QÉÑ^8çÒ¨ú³/‚μÔë­Œk«ûà‡ÊÂwd%ÜûïpF–Õ¹~äëTÆâðŒîhP» [ÛÀÄJøù´zÌ9‚(ýߢÿašIùĮ忖º›ë7 X,Vî•øÌ‰Ïžfx ""úØŒ.œÈŽˆc'Ó{Yˆµÿ`¸þ&TÀiÂoýp€ˆŒ’¦×DZZš4QŸ4ÇD& (‹•;%>sâ³'>ƒì=ADD¹¡@†¢#GOH¯]·i«4F’þ·dÙsˆŒ’f®‰””éMœü’Åú—KxæÄgO|9÷å†NÐÿ6†D‹fHGRR’ôºCƒ†‹ÅÊõŸ=ñä›;ˆˆ(70œ ‰áQÁ¡=ßà +ïJ?œ`@ADDà ""Ê״ÉÄÄD†,V•øì‰Ï à ""Ê 'ˆˆ(_ÓL†)6ˆN°XyTâ³'>ƒâ³ÈI1‰ˆècc8ADDùà +à ""ÊM 'ˆˆ(_c8Ábåb8ADD¹‰áåk6œðø_oÀ±˜×Hyò 9Ö ‹ŸÅàL˜3,õ·geZf½¼[ðžår1œ "¢ÜÄp‚ˆˆòµÜ 'Ì{5G—­[pèa^¦Ê„c$"&æ*þ<6üklÿ1Ëvr0îÊÓp÷Ü,xõƒ×’N¨Ò«V½|ƒËÛÛÀ*“ϰ ËÌ×ë^ÅãÂV:ºU UfG·1ï÷˜áå&†DD”¯åV8aë?ÛbS…ýÇãöÕ-Ü6 £7/Äï§OãïøëSÅà3¯*ãë· K‹€OŸŠºë<*ÂÌ`{Ö[‹÷̰<`èíD þê£Ý†DD”›NQ¾–+á„· f?Iƒ"ù æÍªoÐKÁ̳ªÁ²[UÑôP4ä ahå©¿ŽÅúð²4SSN‘Ñ`8ADDùÚÇ'*¡|p”‰8¶ö§l °èQÇÎàV\"RR_ãуCXÜ¥<´¶óøƒn&ãÞ>7T™>ënßÇóÔ4$%<Ä©SñkßJÒvCúâ÷ë÷ñZ¡Õ°S&`}`5˜zúbsšïi§ŽTD±1#°äú]ÄÍ„¸¿qè ?þ;p v¤¾ÄÊ™UUÛuo‹YOåH>?:ç_µ7^Dšì8úôW÷Òðî‡m©¯2»ÊM›c‘ ‹Ç®¥_ª²uñße+°çÁS¼JKEüËkص«j÷0¼7úe3È£OœÃßñ‰HÎïÎÍMð›ü%Ì5Ûdó>9Îß犗XX[ï•P)ä$’å÷0kBÕµ÷ìÖ®Vêï²2¾Þ~ Iw¦ fÏÆð:|ÿ$¦ )j tW}¾ôıXr墒RœëW7`è´o`­}éŠǯV¯Çá¨çH”¥âõ‹+ؾ½*KûR•õôuxšz>CZ¢çÁ“¸ý&i©/pëjº ¯ ëÝ1îd$þIHAjÊsܸ¼îC«é^Wvîy¶Î©"JÏ ÁåD9t¢Ù9 ¬º·ï[ 'ˆˆ(71œ "¢|í£‡Ÿ£Ç…$(“÷¡k/½!™”yÿÞX+CÊóãX¸Á^KÇ`|Ä_ˆ‘'ãÚQ\PHî4¤EßÁ­——±vÛ8x/¿}ÇðH®ÀË CQ^ØÖÌçG|ÐÃ"_BžŽ‘Ó;â×|Ú»b¦á„…ß(üùFäèC˜:^+¦`þ¥ûˆŠº‹§ò'„ð‘ƒ!ˆLxŠcÇaüæYèä_YX__‡E"Nþ GþœÏ ~ðÙ²×Säˆ:Ó_ºý{”~¯úùb­p¯âïÆÄàþp_<A7b K½‚Iãjäè>™þÖ¡qr?àz dà¿bc<‹p"%f'‚Î?Å‹Çû´uÆ­rGiŠ(>-×S刾³AX6^ÁÓðûµ(¤Êc]PýŒE½ßGOþFÔã}˜¹v8º/‹©çï!Q™„ÓÁB½­NÈŸãúãû¸|z. ßÍÛq%YÔ¨ü‹ 'Ñ_XÞ{«x/Hº3ŸvÓ\W6ïy6ÏÉn@K4Z²7å2Dîë…FS„ß±É-PÁKë>¾G1œ "¢ÜÄp‚ˆˆòµN¨ñi÷fh5³ªhôç#ÈSÏaøpí9(jáû]7‘&¿I£Ä}…ôF7±lƧZ]ék¡Ù¡'Â>Â3= Qëx³IwX‡A8Q -ŸBžr~CÕÇ«Û÷èsåpo>$œî­ü>‚çÔËhwØáø#Y†k»Ú¢°Ö>j­DŠü¦Õ:ª†÷ÝGZÂAôèŸñz3/¬z)ÇëóQRldgû>ÕBËpqèËtðÑ:¿AãqTø]¸¸¥‰*È"œšÏx}=_h÷<èÖÈöh1¾×^îù+†ÝL„üy(šh–«ï‘"~<új…XÝ[`òC9Ro º:4 á÷4îÒpTHo*ãó-×&œÇ³“¾ªk—–WAý·…ßçS0@uŸ²}ÏspNæ㜜Ã:ˆˆÈx0œ "¢|í£‡ž±ä•©×F£Ü[zH%4øg?“#åŠ?Êèmk1t*ÎÈeøkscU#YÝèV¼\Æ:óHTD©åG„Fæø§ Ù 'º·ÇÜèÌŽ_%–Fò…J$]¡w*ÂiÉA$ â™ëÀá·ZéUlâ"\—§áHˆVïíêÞ3¢äHˆôC9­Ï9üV^‘I¿þâµæà>ÙM Å#ù+lœÿ©z›J¨ºæ,Rd—0j˜vƒ=“pBñó&ë°:§ÅÞa½4Ç.)ÜÏ$Å,˜¬¡Ô÷(þLoÑÙ¶ÚžxÙ³¥ø^=´CN6s·!N™€°ùºoÌ(´Iò[˜,[9¸ç98'†DDdlNQ¾öÑÉîí0ç™iw§¡ö»zNôèÍ©r<9ØIw>±<{b}²±ÝUë4ÃþžŠZ:û­ˆâK n­uvÃ éøŠLo=m ¢NÇÙ«=·…jÛ/·Þ€LÿKH—YÃ^]¹nIͺ±ªLÞŽöZáD¶î“g,ˆ•ãåé>(&õºh¿Û©H¹=9½‡@–á„ì0¼}u‡íXOY'Šdl R1Ñ*«I+p_‘‚]‹ÕAH–÷¨Z}YÌrü¨N(^#t¶nb3{ ^ÊŸ (@÷Í/6ów#I~SLjיƒ{žƒsb8ADDƆáåk=œðø ½®¤@™¸ n=3Y¯]=ú n¢v6L½TáDtxWp"Uh8WÓë`ÐèÎQ8‘y8b5%²9çD°Ë™† úŒmëm¿ ™ü*ç¹¢á$½jz öRõ苭½Š=?¿|Î?Nø%´†ud÷>5Ø{²Ä½èÔ«"̇à¤,á!ßÌ aN¤@7­á bYO E”2[ƒjœ¿Õä`)œØñ»zNƒýjÊ0P…Zß…ºTáÄãŒÞšåzáD¶ïyΉá†DD”¯}ôpBh Ö\‰Teö,Ëbˆ‚¦º·ÆL¡ÁŸru”ÁUãO†ó›~ÕÖ‘ÝFw¶Â‰î.˜#Gò¥aºoÊaÁÄë ëh…éQ¶‘u'ìZ<…<[áDE”ZŽ$Åcè6´ßYÝÛcŽ8åêHƒ{¥S9ºO`9v!nÈãô9ª¯=‡”ÔôÒ\‡X×’u8!†§´ç«Ð:véåG¬xˆy“´‡udv ƒ€ 'rpÏspN 'ˆˆÈØ0œ "¢|íã‡`ÖÛë_) ˆ;ÿquÞÒxSMò(K½€1#µ»ì×Bƒ=·&¿i0!föÝÙ 'ºÖF›c±'G¿Á vS/á~ú9Úá„Ç÷,_öp¾ÔzÍ¥©W{ÌŽ’Ù '„üÀÑ8”"dzóƒQé]Ã^tJ=y¨ì¦MÐcA§rtŸ„êÞ “¦!öÄXŒ¼ž‚ø¿† ´öç ®%ëp´ۯu7²¨åøé7­åž0âvR&bfv ƒ€ 'rpÏspNæƒ&à˜ì-ÃpÞ£NQnb8ADDùZn„ÒˆÓ–"2Ih`Écq!rfl™‘›æbÆÁ8p'®êÆ«YoG é'±h£?<—ŒÂ8õ«D¯ïïbð*Ñì5º³NT€•ÿ$D$*øt?¦®ˆ.‹Ç"àô <ˆ}‚×: âʨµö”¯pbï@ü<ª1¾šì‹ÑgïáÙ›8(²NH¯µÜx¯”©x|c&†ú£çб¹+ îB>Y¿~U|•hhŒÐpM¼…m{§¢ïŠáè³v.žû 'vº¨®)G÷Iu]uî %. OÒâ°õ÷ÏuÃ$ƒkyK8!£èäE¸˜"GìÝM¿j0Öߺ‡§I©Â½ODôÓsÛ3u½2Ù^«¬úwÄÀ‡qñù+$ÊåHJˆÂÅK0|êת†Žî“ª¤7£È„߃7º¯•ÊàZÞNˆU %'ŒÀ‚ ×ñ81©©/pû挚ñ­î$“ûÕ”aðááD…ìÝóœ“ÄŒ‡u÷žâ,ñ/£ŸöëhߣNQnúèáD£–.,‹Åb¥×‡ÊÍp˜ËVhø¾¾óõ¾,Vnà ""ÊM=œ ""ú˜NdRŸ¡MDä)‡àý–a,ÖÇ,†DD”›NQ¾ö¿NXÏ\‰Ó—7aÖ¦±ð^ÔƤS7ðZ)Ã?;ÃQgh‹•{Åp‚ˆˆrà ""Ê×þ×à ‹Á=1íÔ)\‰}‰™rYž>9…Ð]QQû­,V.à ""ÊM 'ˆˆ(_û_'X¬üR 'ˆˆ(71œ "¢|M;œHLLÄÿ5`8ÁbåE‰Ïžø 2œ "¢ÜÀp‚ˆˆò5±$–&œ0ùÉ ¦Ê4œX,V.–ð̉Ϟ&œÐ<—DDD à ""Ê÷4áDRRJº~‚BíÊ6žX,V®•øÌ‰Ïžø j ""¢‰áå{Úá„×ì(Ô´„Aã‰Åbå^‰Ïœøì1œ "¢ÜÂp‚ˆˆò=ͼ)))øçñw© “¶ì=Ábý%>kâ3'>{â3Èù&ˆˆ(70œ "¢|ON¤¦¦âÍ›7Xsh3,[”e@Ábår‰Ï˜ø¬‰Ïœøì‰Ï à ""Ê 'ˆˆ(ßÓž399¯^½ÂšƒaÒ_sÅîæÒœ$“Åú8%[â3&>kâ3'>{œ “ˆˆr à ""2 šÞš¹'ÄÆÒ­{wà9kJ¸Ô–Þ$ð?8²X¬©ŽÒ³$>Sâ³%>c⳦™k‚½&ˆˆ(·0œ ""£¡ (ÒÒÒ¤ÆR||<^¾|‰ØØXÄÄÄ ::šÅb}`‰Ï’øL‰Ï–øŒ‰ÏšøÌ1˜ "¢ÜÄp‚ˆˆŒ†¦;¹¦…89ŸØpÇÂÇÅűX¬Tâ3%>[â3¦Ýc‚áå†DDdT´ ¹\.5œÄIúÄRâ¸x‹õ~%>CšçI|¶ÄgŒÁýN‘QÒ4–4A…&¬`±XVšçIû#""Êm 'ˆˆÈ¨i7 X,ÖÇ-""¢ à """""""ÊS 'ˆˆˆˆˆˆˆ(O1œ """"""¢<Åp‚ˆˆˆˆˆˆˆòà """""""ÊS 'ˆˆˆˆˆˆˆ(O1œ """"""¢<Åp‚ˆˆˆˆˆˆˆòÔÿ|8¡¸¿¾m\Шíìz¡Ô_?È®a¡g4rî„É'Òô×’ZÊåPøtì ×aÛpG®¿–ˆˆˆˆˆˆò+£ '”I±fûz£U›Žhéî/¿Ú~±)0œÈŒNnCèŽ ˆÉ·Dùçw†aý‰'Ph-N<û;Ü[»¢¹O(®Ê´VQ¾fá„òÍ,Ð[º ±sg¸þÖ]:£©³+ÚNŠÀ«h@+n1ŽpÂë_ '’#1ÛÃMz¬Ê—|Ù­5ðjíŠÖ³ÏA÷n(‘–øIù𜉈ˆˆˆˆ(kFN¤âʲ~hêì‚Æ'cÓí¨"Þ<¾‚+SÕÛ)‘x7‹&ø¡“›;š»xÂÃoVˆBJú¾”H¸½3†ø U»Îpñ‚å{C1È œHÆý#¡?ÈíÚwD ·^è°GÓeH{kçNBé3Í:ü†žã‚ñç?Éê-丸ØMœ{"ðÄuì ŽîhæÒ¿MX‡S1㔯®`ÝT?¸¹¸ÃÙÓ6îÁ¼Þno'dQ8¿þ}à&\³vÝÐyà ¬8ñLÝ€WâÍÁ©hÞÒM}×â–úpÊøpŒj+\ë¾#Gêƒ#˜;¤š ÷»QKu©×IÛ¿¾íAáíÑÍÛv«ÏXÌ »€˜ô@@ë:]ÅÖÙ#йƒ»tûÎÜkÏïã qèæ®¾ö‰p>ý¾+ñúšp­F¢‡§§ÔC¦U—ð›·×âÅm”xv*C=Ý¥ Js~»,Æyáøi'æ ¥pÞMûxÁ¹;Úx ƈß÷áêkMŸ‹ì/DDDDDD”;ò8‘v ®B#Ô ý¶Détã×&¸~nÂvÎnhûÛ Ô­ÅŸ½1þp¬h(_ŸD@Wq_®hÚ±|õ‡‹$ˆ ñôpB‰èƒÓá*|¶qÇ!ŒùSüÐNø¹I÷ œˆË¢wEâÌõöD·!˜4{>Æö÷¼BC¹Ç \”2 u#X8vóvBc½Ë` ò ÷vªói5îžK‡†£=T½DÚzÁkÐ0xˆaCñZÞN(_áДßàê;£g,ÀÔ±ÐF tÚÅšÄvv .ïÀL_á<…ϺÄÈÙA˜1oŽF '—r +ûwÎÍÍ:À€a~po¯ºçÝDBukt¯³e÷¡4Xü.Ä Áή]Тó@ :í[«®Ý-0IêËH»½>}ÑgôLL›3 ý=:JÇsq ñJ9nïYõ9xÇô¹A˜¹$â%„Êxœ ê#}¿ÛýŸ¡£àÝE 6\Ѳÿ:\“r£l~/DDDDDD”kò}8¡ŒÝ?±gC«^Xx%«¿b§!2è7U£Ó7žH›¥áîúaRôIÏÕ¸.SâåþÉRãµqÇìy¦j°¿Œ˜ ípB~Á¾b/….ðßù1±Ï{ýÝ¥!%c¿Q÷Ü0¤«¢¥RYì~ ýmü°æ¾¸\ÓŽßiIcQ”x¶s,š‹çä¾gdâ0“­è#]¯¦x-Kþt†º¼#œ( Õ¹)•P¤ýƒÐþÂu´tǰ½/…åÙ 'T SŽÎB‹–b°¢=¬C‰ø#3¥¡q§™8¨îíti9ºµ–µ‰MOô®³K ŽŠ‰…2Ggxª×IØ-Md‘†k+ú¡©°¬Y¿0ÜKOPßFázäH86G:f“îËqQ:"ª¾kýaúá„2z·î[+_Ì=¯êq#T]Ä{Ù£Š÷7{ß åžüNˆ L©±î›u8¡|ŠM~bCÜ ¾¦÷®Ý\î­Äàa"v¿ á¾R#´ù˜éóT(ãa¤Ø8W‡bc}´ø³Ö°ŒrC¿Í™õÞP"áïCçw71ÄÐúLz£?£Ü|üau/á/.…›Øv Äq¡¥vvÚŠ?»â„f‰â 6 ~û°åëëØ2o¼4Ü¢™Øc$ýœ;bÐŽ˜NÈq#¸ŸêüGÿ‰ô‘i‘˜é®jìHç¦uÂñFÚNÖû¡™°¬™ßªFè¸Cx­ÙQÂŒn§NV5Ö{`Üæ³8}ö¼VEâjTÆ éR¯`4aetš°—náæå0U¨bN¸¢åôÓéjÙ¥ep×'Îh‰ù8™Þ ~†0¿ŽY‡Êx™&öLßTˆ­§¯áÆÍ³XÔO¼'™„½Öà¦&œÐôLÉF8q]îd+œÐ¾Nîo¦ 'Fþ¡~»ŠaX"¿»>ÒðO Yó×ïàÚÎiªûñáDÚÙ Õg³N¼ý{!"""""¢Ü“ÿà åõW4ñšƒ?iMo™úžÅ ÍÜ4¡ÁÚ3ëa¿©†u¼Ø7-ÄgçÙ—Ò %Îýw)ÀР븃½Ä^ nè¹ê64ÓYŠdqqHÈ${wH“j¶ê‡¥êV¿üú¥™„Î3²n+4ŸkÕó/¨fbP¾<‚qß2¬C3¥¥úo}ª gR¯aaZá|t–jX‹û\M7R"îØ´{(h…©B#ßYÜÎc "Ó§Ä›ˆYªy,ÜgààKÕHº´B=¬c6=ÖÖ¡}Ù '’ŽÌB‘ÆîA8-}P‰çÂw&sz8¡&L9é2Ôôà eô^ 玆u$J÷ cXGg½aoÿ^ˆˆˆˆˆˆ(÷äÿpB :ˆ1];¨æ,hÝ}«ÇohÓÆíæFJ Jùã}.ýß í|FÁϯ/Ú¶öÂè?£U ÓßI5ÑaËnÃ0t”?º¸w’æО3æð,¸ ŸmììÏј=o&Ž ×sp4³†jÊ_˜%N´)»ËøÕذq%F÷ì‚&âðІb/-þ]T×êÒýGG/®h!ÎU8¡ŒÃŸ“ÄW­ ×å;+ÃÂ0¤p]Â5h…Š';1@l¬;w„ûðù˜7ozv·Ñ 'äw7 gk±÷ˆ;:¡~£±\lí§ÞÁêAU=4ĉ#G Gi. áºçžU•Éì:³NÈo¯§t\o Y¼ëWÍOGÕðŽŒpB‰è]ãT!S/üæ?.ÁÑ$ÃpBì%¹¨¿jBÌö>ð1>ª 1[ô^ËRö“Ùùfò½Q®1ŠpB”}çMDÏnhÞªš‰¯ 4 WÚ¦kÞ¸yûŽþ&Ù&…æàs@¯ŸpƒkÙ¤ÒD¤é®Ê/N‘±0ªpB4cÎ)x¸xùªþªtç"/HÛÌ]°XUŽdN Û=Šã?v]¦™ò"aÚÛ–‚çž$Dœ‚¶Ÿ—‡µZ‡Ä@©ŒÇÕÍSУeÔ®XVVŽ(^£z,<ƒJÍ>eˆû¬êÏÄ…[Û1ºÃw(ïès»²¨þko,:ÿ雪¥ü³S¼š¡vY'XÙ8¡dµoÑjÊ1ÄANXT@Ïm±vh[Ô-[ VÅQú³6ðÛx ú±NÊý?0¥Û/¨ZÒæ¶¥Qí'Ì9í‘+©Â6»ü,mcfQveêâ;Wl¸-ÓÚŠˆˆˆˆˆˆ(gŒ6œ8x8B (2+ÿ1“¤mÄI4?D–á„â>†i øKÓ.à {üÔg ê«‚^£11`6\7ˆÃvŸoðƒÇHL_²a›VbLëš07¯Ï]qêÐAN˜9ÔA­*UÑÀg~Yƒ¥3úà»R6(T®'v¾Êˆ'7 Cy[˜”øGÌF`àtŒðí¯¥· ‡&œ(ª5k F똳rÖ®˜Žî_•@!ËÏ1ütÆ€EÔVt­l»O»`ü²Øºý®S»o0î¬:ÆHû c?³‡euWŒ^²›6„`~À¸üÒ ¡Qú± Qöm8ñ®òî=ii6æBN”ƒÇ¦GxúôžF=Á½k'°~Ls”´­î[¢2zHá„%þcQÞ»¢uzd*n܋٠Lßp¨¢U8ajj‡º£#‘ñ%ž¯wΣ zìÓ ‰8ЧLlÀÄ ú} T¤pÂÌ 6ᮘV¨)î/ÂO6¶¨1âT¹J ŽùfÅÛcå㌳V¾Þ¯ò6°o¿±Jásãg+{4^úÌ ч0Úp¢E»Nâ?Ö †žˆàÕëœòáSUê¾­#ã­boŸÐºC#¤p VÍV [ dç1¢–-lºìPOª©',¾ÂøKºÃ$ä7gâkK{´ y© ÒNÁ¯š ,š,ÏòXªp¢0~YôD7(I;‰ÁUlQÄk¯ê¸²sðÏ£ÝjD½yƒ7éUíŠÂ¤\Ó“äô«l«Z˜wä‘VxBDDDDDDôaŒ6œØ0\ÕG§z[GI´˜¶ûþøS¨}ؾif l‰Š¶Ž¨é†ûš^ R8aƒªCO࿆2—7L‚G“ú¨R¦$ìì`i]¦bφÎÛuà ë6~®›8Èÿž‹o,íÑ,8VN$n†‹­5J÷9¬îuaH5!f tÛ¥Ò¤Á°¶(Üm·ê¸I[àjk¥ÀdÔì»c›´¡ñ–£Û¥`bZNõ:aĪÓxjp±DDDDDDD9c´áÄÛ&ÄüX²œs2Üœù#,ÌÊÃ{Ÿºÿ„NØ¢ÆÈóêái¸:çWØ›;ás¯¹Øpè,.^»WÃà]ÙÆ0œ°qAèkdN„ DzF™>š!!†²|[‡~8!ìK :еBøñ8¦WÇOÝ‚NV¢ˆÃÍ}‹0йì̬áðµ?öÇdÑ}ƒˆˆˆˆˆˆ(Œ*œ¸~ã–ôzP1œÈêU¢SÖá„Ðøßï‹âÚÃ&² 'd‘YÛæßâoíñiGзÂ{†iÇ1°² ,š®ÀÓ,rl‡Ù"’¹4<Þݵ,íPoêuiN""""""¢÷a4áDLL,Z¶ï¤3é¥P䦬É7ˆ\¦fåàµ÷='ÒŽa@eXw>•!%r<êXd2¬#;á„°§Ý=*ÀÄöG\É|nl‡HÂáþ5`bý_Œ<­}†Ù¼=JÙ¡²ßIá,DDDDDDDÙd4áÄö]{¥@bùª5HNNF§î>hҪÿ‘ãmTsN8áÇ!˱lÅJ¡‚±hÞT r­3kØ7˜…‹šl «pBù [º–ƒI‘´ñnÝ»‰S[¦¡í§Ÿ Bé÷ì9!-[æ%­aZæ'ô˜„EKbÊpo´±_Ú_öà ñU¢ÛЭŠ ýí‡ÎÆïµ. œŒ~®?âûQªàAñp9ܾuCß ó°dÝvlÛ¸ã\ëÂÚ²6ú‡'hˆˆˆˆˆˆ(GŒ&œØ±{ŸNÌ \ˆÈ —àÚÙ ]?Ê[9²bð¶S+˜XC‰š?ÃmÔz\z­5"«pB Œ=Ž™]¾GY;áóN(_¿¦y„CªÁî=à QÂ-Óé'T-é3 ­ú:/½"?'á„´øIæ÷kƒÏ+•‚µ¥-¬ŠUÅç-|1óðSÕ°•7‘øÝ»9êT(Ksk˜.‹j ºaü®{:û!""""""Ê)£ 'ÄaÍÛºë ë?y†þfDDDDDDDddŒ&œ]½vÃGOÄÿ±˜3õ7!"""""""#cTá< 'ˆˆˆˆˆˆˆ(O1œ """"""¢<Åp‚ˆˆˆˆˆˆˆòà """""""ÊS 'ˆˆˆˆˆˆˆ(O1œ """"""¢<Åp‚ˆˆˆˆˆˆˆòà """""""ÊS 'ˆˆˆˆˆˆˆ(O1œ """"""¢<Åp‚ˆˆˆˆˆˆˆòà """""""ÊS 'ˆˆˆˆˆˆˆ(O1œ """"""¢<Åp‚ˆˆˆˆˆˆˆòà """""""ÊS 'ˆˆˆˆˆˆˆ(O1œ """"""¢<Åp‚ˆˆˆˆˆˆˆòà """""""ÊS 'ˆˆˆˆˆˆˆ(OE8¡T*±c÷>z"†ø5(¿ã°nãÈd2ýQ>gáDÈš hÔÒå5täx¤¥¥éœˆˆˆˆˆˆˆò1£'ÄbøpèÈQ\¼|5Ó4ltz@‘’’ª¿ """""""ʧŒ"œ‡nˆÁÃÓgÑú«Ò‰Ä¿‘Òvâÿý €B~¾´Ãÿ™X¾¥lQcäyp ч)0á„è£Ê8½v!æÎ[ ª9ð}Qk˜×óÁLͲy r" ýÏQŽm8qòôYiù©3çµ¶’SR0dÄ8i{q¨Ç{ÚRÃÑ»œ l:oGŠþ:"""""""ú FNŒ -3qšÖ–*⤘âÜâúÃF#))Y“œÉ*œPÆãêæ)èѲjW, ++G¯Ñ=žÁ ¥ö†€âÙQÌìÞ•Š¥C%Ôm=®_Çœ†Et†‡¤Þÿ»üŒª%afQveêâ;Wl¸Í$DDDDDDT0m8q┪çÄÙóim™A (†š>Äナ¬Â Äa»Ï7øÁc$¦/Y°M+1¦uM˜›W‚ç®8¤çÉabýb(äð%ÜÆ/Áªeðm„r뢚£]F8‘öÆ~fËꮽd6mÁü€!pù¥B£ôÒ""""""¢Â( ?õ0'QOõW½UjjjzŠ~ƒý¥!ï%Ëp"qÛà^Ìeú†C5 D‰Ø5`o^mC£´æ¨HÁ¥€ïaaš1±¦âÑbüleÆKŸeDDDDDDDœQ„Ë‚C¥€aÔø)Ér6¼AìA¡yièºMú«³''á„ì*ûzneNgé{w‹ÚçKªÄÉßQž4õ n]V9^Í¡ ž6»uœ§ÊïUVÓ®C5zÚ|Í›1^+¾!O÷\j¾Ú²š&Ï ‡ ' (lƒ‰-ÛvXÎzF±…aB¯mPÿê(ƒŸœ–¾ÁV©WõBz9¹Ÿ\}3)WñÖšº{Š*ûx+gäȉÛ;5²~qåÉœZrñÍ l…j©Ë¢“OÞ%å°á„áþýûjѺ½P´j×YK~[a†F­ß#&þe¡×ýõ…§ò÷b= À‹Ë¡Ã C@À}Óê3 0*^GLü«BtnRy%sÍ Ú‹-+eðqøp¤ɿÌR÷ÞuèðQk³Ý…ÞøEuÞ-¯¯: Ò(ÿéš>c²†´)¯l>žò,ÐS;˜³x=áD‚p@?·¨¬rgQR9¹ùÉ/óÛúüëQÚpÕvR^<„À®'€]N»"œvE8ìŠpØá°+ `W„À®'€]N»"œvE8ìŠpØá°+ `W„À®'€]N»r˜pbמ}jÔ¬•Š–¬¨OKTxª*YþK]½zÍz)€8D8qüä©8…¶µgß~ëå@âáÄ a£Ì aÙòUÖ¦Gê7h8áÀ!‰Ví:›AÃ¥ËW¬M”`ɳú±¯\ Õ‰k#/‡'Z¶íd—p"`q}%uv×KNFy(±{2%ÍœOÿû²³¦ì½¥Pë Oƒp€hžÛpbÆìùæ9ç/\²6=53œpI¥šÐðGiè~j߸œr%÷TâdE5è` õ”'#œ šç6œ0\¿qÃz(NÌpÂ5³¾Zþ Úñ;›~P.7/½Új³âONø=xð@í»ôÒêu¬M1¬Z³Þì{çÎ]kðŸz®Ã‰êQá„–ªv yTž£{á´¸nZ9¥ûZ¿Yºnj£W]“ªòœ€ð 'îÔŒUU K:yz$Wª×K¨é„ÝúËfîȃÓËÔ­ú'Êš&¹\Ü|å“þ ½_±­¦ zØ À Íe¿(WÍÜáhíúMÖæ(kÖm4û}¯°å2ìì¹ '"ÿEðŸü¥ûQáDÈé*äé¥,-7EŒœø‡áDÐ1ýT"ƒœS¾¯º}'iú õ©÷~Øk§QáGdF»Ô)o¹g¯¨£§kæt ëÙR 7Òä‹Ï´ú€çÔÎÝ{U¬L}Vª’6mÙfm6mF£/`oÏe8±ô÷•f£*×lðÔçY…‡Usæ9]ºtYÏ×®åÕìý´rJUR£ŽGöüGáÄ_óë*•[VÕûõæÃE6C/ib™ÔrÊÐXËî†vî'}â‘DEÇ\~¶…8¼P¶nßiFmÛ±+êxd0a”ÑHž»pbá¯Ë¢‚‰Èªøe]¿pÑÚõ‰¢ïÖQÎ~zµLÍ=rǶç?'´¤~9¥o¤…7nëöíȺ¥CƒŠÈÕ-¿:ï ’î¯U³,^òx­¦†®9§û¶/±Ø°ykÔ‰Ý{ö™!Ed0a´ …C…Æ4 ãgÛjýC]¾rÕì7ѳ_ÙʵԼÕæÏºö6ÿ[®jm]¼tÙråÇ ß­#¾è³DK—ý®%ÓZém/oeo¹AÑ—ûáDÈy(â=±-×j±Þ˜<ª¿wS­|a¯ãì«Tù«©Ý¤-ºç9¼HV®Yg~/[UŸ—®lþl‡ 'U»öì3û 5Nå«Õщ“§5pèH³mϾýš1g¾ù/…‡³\ùñb®9qKËe—“Ï'p$rJ‡ÙóÑáÄúVzù±áÄ9 /ì+§l 5qíF­ß`©;uâ–ÍDŽ[:¼t”Z”Ì#Oùh«ß®2ÑÀ£-_¹&êûÒøHh*œúã3l°­³ç.Dõ ÑÍ¿n™?ò£yÎî½DµÇUÌpÂX s¬Š&õRª*3u9*x ¥õÓË)Mc-µ„·gW—óc ÝÓÂ:i•8EMÍþÛöÌ' ÔùÅÍôš»ò÷>(Û¨¬Ömجõ·X ‚C„m:t3ƒ†ÓgÎY›©d81ªâYÄNH÷µ©Í›rvË£o×E®;¤òÉÙó3 ?k³?hè--¬û²?6œÕY5•Â5•>|P½žÎýßT/­²´ŠÜ5ÇãáÄèñþfÐ0dÄè#'¬>†àß9a½>òHõšÞ«ÖC‹öþ¬²>ÉNîŸÔâ~T$Ï+Jâå-Ÿtz¥`E5»]7¸½S#ëWžÌ©åîê)ß ÊV¨–º,:·Ñ$0N®ß¸¡Ÿ§Í”ÿÔ¬¹ ëïÛ·Íþñ1rüû&œˆ«øXsüûžÛpbó֪ݰiÔî aznà à'€]N»"œvE8ìŠpØá°+ `W„À®'€]N»"œvE8ìŠpØá°+ `W„À®'€]N»"œvåáD`` V®Y§>‡©aÓïTºb •®TS_5k©~ƒ†kõº ²ž ‡ '¶nߥêuëÓ[µ4Õ®=û¬§€ơ ÿ©3¢Â‡Ê5hìÄÉÚûÇݾsÇ,ãç1~VÅêõ¢úÍš»Ðz€8L8aLLž6S<°v‰ð@ü§FõŸ»àWk—$X (¬4Ù¿Òœ«¡ÖFž{NlÙ¶Ã Š—©¢­ÛwZ›iͺú¬T%-YQû¶6ÇÉ_óë*¥‹ÞêuPÁÖÆ§xp®úÏ9d9?H{z¾§$kê—Ë„€O‚'îÝ»oNá0‰¸‘~[±Ú<·n£æÏ¾HfèuM©Z.îÞr~½³vZ;< mk÷†|¿œ¯k“BB.xA%øpbÑ’ßÌp¡uû®Ö&s‰ã'OYGªM¾3¯±aóVkóS ¹0AŸû&SÑuõŠÇkj±>f¼ðD÷÷ªËÛ>òŠ5œ€øaLykߥ—¹kÑ“¬Z³Þì{çÎ]kðŸJðáDû.=Í`aýÆ-Ö&s cÚÆ£Ç›#,eþ¢%æ5 emz !:9¬ˆÜ=?ÕÐÃËÕ8“·Òµ\w¬Ý„Þ>¨jêƒåã•T~óèÝj?iç‘)ª–#¹9¹ë¥¨òS™©ËQ±³S>9{UÐä¿l/vK»ýÛ¨Tþ¬òóö“wÚ×õaÍ^šwÌö!"ü\‚ýµûÈ|u¨ô¾2%÷“«Oe/òµFíøKdëÒÚaªWäM¥ñó•³Gr¥ÌöJ6™¨]±Ý ‡týÆ }Q®š9míúMÖæ(Æ´7£Ñ÷ÊÕkÖfà?•àÉÈ)÷bŽ7°]$ÓØ¡cùÊ5Ö.¦K—¯˜}5oemz²àCêùŽÜ?©Ó!÷µ¢É«rN]GsmƒCà! -šV‰=²¨Pý®ê?lˆº}ßHe[ÌÒÅ¿NhËÊQª”ÁKîŸöÒïkÖjõšõú㊱úDláÄ]mïù‘|]S+oõ®6iŠÆ n«’9’É)M):9=%ü\¿ß×VáÊ#uøYÑ íܽWÅÊT1ÃÛM[¶Y›ÍcF›ÑÇè Ø[‚'Š—­ªJÕë[›lÉÈjÕ®³FX•¬P]åªÔ¶~¢ ]]”Û-© <§°ßï¯n¡Ln©TvÊu›Q ¡º>½š’¹¦Öç?P¬+[nÒw¯zÅ2­#f8rf´Šøz)Sý%ºa³EȹÉ*“ÚKÉ«ÌÔ5óxĹÎ>z£ÃN=;ö~~©ª¤®éUoiø«Ý›YMîÕýIð"0Öè1£¶íØu<2˜0êYÖñþ >œ(U±º*T«k=lŠ-œhýC—XÉòÕê¨tÅÖÃO õß½&gï/4êœM‡Öë›,^ò*6^ç#…=úkaí´Jœ¼–æ35bóÔáD¨®ù—•‡kµXo]yó®ÖM¯ÄÉÂ^ÇœŠq®ÛÛê²7z$|¸¿ ¸'Q ÿ?Í%h_/åóôQÆ’}ôëñÛ6Á €ç•±ÎNä‰Ý{ö™!Ed0ñ¬kðÿ†N»l¡CppÌñ· 'ŒÑÆân± 2ûÔkÜÂÚôxw—ë«L^r}¯‡Ö;®cÇ:¨É5_VbÏÂ|2"9¯…}圷»öÄ:lBq'‚´·[9{|®‘H”`ìýž\<>Ö@óµ#Îõ,£ ×£Ç ÁÇë]÷$*6áZD #Ó[èýt¾Jä–F¯•j¥WžÑ½hgxÞ¬\³Îüþ3F¡}^º²ù³q HH|8ѳï`ó/Ó»öì³6E-ˆ9rÌ„Ç.ˆ¹uû.ó}³6=Ö_óë*¥‹í"–6å죷z”™„œÓp#œx³G¼„{ºä—³Çg±†Ìp¢ˆ†œ¶ 'l¦„DõŒN„ ½{Zk&tT…7ÓÊÙ9¹rÖ›­31sÏc=žÈ ÷Qkóö”àɫ֚¡îÕo°µÉÜJôÔé3ÖÃ1tîÑ×¼ÆÚ ^¹>†ÐëšR!µ'ÿB™«Y³mjæPUÎâ%ç×;k‡9óâ®æÖH¥Ä)jkÎmë…"áDÖ§ 'BuýçòòtͦfkDë)ÝÑ‚Úé䔢޿™›vXÏ}èQáD”ëZס¼Üb›>ày³nÃæXw=‚N9{UÐ俬m Æz Æ´†qÄ„•ñŸkPÀ^"œø}嚨)‘5Áªµ[¼ X\_IÝõ’[}¿%ÐÚåæœZJîbô˯Î{ƒ¬ÍÏŽpø× B¾HkL<‰ñYŸ `Næ.X¬M¾Sz_›áD§î}­]âN¸$—·¯·^n±^Ö†Ðkš\>•Ü|“ËÕppÆcÇŠëÞ 5¨÷¶ª¯¿®PkÛ¿ÈøL={p˜p"R`PªÕþÊ (Ž;amŽWf8áù?U(÷ª\27Õï÷¬=¤Ð U,Ij•¨ZR>„€C0€4¶Ô|¼ mÿžœK¤U¢锸ôËò«YHõ ùc*ãOð Mÿ¹—&Ÿþ—_ÇÂøLXöàpá„aÉo+Ìp¢J͆ê3`¨6mÙfí¢-Ûv¨ÿàú¡sOmÞºÃÚüTÌpÂãêéßR¯ºgRÅ·-=BtjDQy¦®­ÉcªÈ=–ig–«wÝO•#mr¹y§QÆ·Êê›1[tÕ6œÔ‚NUôVæTróH®´ù*©óâQ,IÌpâÎAÍèPU²¤“gXßT¯—PÓ »õWÔ?±ÆNëÒÚaªWäM¥ñó•sØy)³} ’M&j×È>À‹áó>_jÉžUÖÃáá„{ÓîZqñ¬N^<¬-ÛÖ×ßå”[ývZKXéèŒÏÄøl€ÿšC†ƈ ë³æ.ŒjŸ·pI´6cëQc;Ò¸ 'ÞW¯}[Õ.—’ט¯h‚ªï{~JÛ`™.L®#œ¹0GÕ³øÈí•âjÖ‚&ùV·z)¥[Råþv¥þŒ BohQÜrqM¯‚ hÜÔiú©÷×úàåz9£Wôp"è˜~*‘AÎ)ßWݾ“4}†¿úÔ{_I]Ó¨ðˆ# õ˜áDðñõqeø¬†Nž­éaï¥Ï÷µU¸òHޏ6ð‚Hß4ÿSìÐNx´ªƒ6á`ÐÉÊ[&¯ï 4ÿìNèœ]E—׿…M”¿Æ«òlØUŒ?ˆ!W´fnsjK^å²+CóÚj»á¤î‡Uïo_Væ‘-߸»T5¾Ì¢ÿ-<£ÀßU³ò+ª°îa;gç©u×ÂJ_ñeyU}O « ·Â¾D¬VïèÓ%—£¦€좬¥2ê5ÿ=ß ÆÛ§ÿ•{GßüñèõsŒÏÄøl€ÿšC†ƒ†2C‡Õë6èÊÕk1‚ŠÈ:á¢9jÂøÙaWááDAu? =]ß–Kòªšvýá ðà?z*¯g65]}Oú—“[´pâžV5Ï!'ŸÂêÈöaàŽ6·Ë/÷·ÕyOxßàÃýUÀÃK™,{X„¹½¦¥²ººG 'þš_W©Ü²ªÞ¯7ÎE½¤‰eRË)Cc-»kˆNÜ›YMîÆ½DÆÎO{8|v¨Þ.›OMöF†Y”¾I½Ù¹¯~Ù·[ÛOÓíÐÚ5½¸ü|¥‘ÏêÚßµý÷¦ÊZ®Z¸£S ËË«fK-½yÕPý¹®¡’Wª® Æ—€%œ½¹X_ÖÎ¥&.Õþ?ÿÒ•ó«Ô±MN¥è2MgBÿÖ¬>9ä×gaDx¤m“>RÚºo+i³Úcþ‘ÕUuäûå·Zò„™"O÷ÙñË!ÉÁÃ2‡sæ›S:¬¡DdíØµG“§Í4îÙw°õ2ONìVð¡~zÛ#¥Jú_µí‡¼rËÞV„Æ '·ªuv/¹› Ë–í‚TAw½Õó ‚îve|I¹»fWóµ¢w Ü YlGNhIý rJßH oÜÖíÛ‘uK‡‘kÔëÇ '‚öõR>Oe,ÙG¿¿ýŸ.²$4O÷n 'BtåìJõî’Oî ;i,˜áDf97è ÕQAC˜€•ªW=»Ê®úÓ&D<«!m^Vú‘ëðçt•¨ôºªoú;¢íº&wÏ®}†”щŸ[J^{k»Í¬±û;[+m¹òq-X—ÿRÞ5¿× # Þ§ŽM^Sùù£ôY¥bê}ÖH'îé×Áy”´÷Ý|x‰X=ÝgÄ/‡ 'Ž=#ˆøeæÜ¨ö‹—Fkû¬T¥0­#<œPÈ ,”DžÅÆë¼ñ °^-^M¢Üw‡=ÂÄNÜ›£J>^Jßdµ,‘Cع õ¥Ÿ§RÔÿ-¬-È•áìñ™Fœ³,DrZƒÿg³ fÈy(â«—œÜc/×j±Þx:‰NÁÆ‘é-ô~:_%rK£×JµÒ+ÏDV¼ â2­Ã¹dF¹•Í,×RÆ¢˜9”³sw;1üÀ '^V²ÁË£íæ|~„ –É Ïʹ”¢êÃò-›N=çéNèMÍì“KÉzÍ“1+ôÆT«˜Wõ¶G,-œx …³Ë©LV%·¹VаvçÒ«Ãñ …\­Ê}¤öÇ‚|j Þ¬TM㮇}´~UÌ=­ íjÙ «>_vå±Á$Ó:`/N,[¾Ê Œ1[¶í¤_—-·vÑoa}Œ¶ö]zš‹c>‹há„Btfäçòð*ªa§Ct÷÷&Jïù®z˜Ó$b 'îÎVEoO¥k²*f8q?<œHõÕr›p¢˜~4S!ç4¼°m8þ»S¶†š¸v£Öo°ÔÆ:aÌA5œz÷´ÖLè¨ o¦•³srå¬7[g˜éLœÄlÒYKÎב‹týå‹N¼¢t?®‹öç<øì0(›_õ7žÕ¹k£Õù¿ï›Áííß)]åššt3HgWwýNZ9ÌN,]îmGj·åZç®_Õ-ã+'ø€:7Í¢çŸÔþ™ÅäÛÞ_çBƒuhfqù´› “§‡ãÍSõJÄIDATêíò¥4ð¢u%ÞèXöâpáDPPªÖjh†û¶6Ç«èáDØsÈʼn*æ›Dï Ø¥¹u2ÊýÃ!:fþ]?–p"h»Úäô–[ѱº`Öa¬1áû2§u\[Rn®¹Ôj³e¡º ú!··Í´Ž{ZX'­§¨©Ù£Ác÷èp"JÈu­ëPH^n‘£-€G\¶µ®9Í# ÝÿMµªeV‘‹ÔÆâÁ&}S/›Š-Ý®Ám^Õë?ï{Ø7Z8¬Ã³ŠËý˦šwçQã‚´~lA%é>\ÝÛe×Ç‹ÎËxËæâ•««×ôJòi¹þÄ£±•(ìÅa‰i3çšÓ3JWªi]zô³v‰wÖpB¡×ôsÙTrÉñ®ò&M¦Â£Î™±† ІV¹åì]H=ÿ°ð}GÛä“‹{þ‡ bì£üî^ÊÚ|µl3‡û;:+›í‚˜¡º1«¦R¸¦ÒǃFFÝS„2ò“ŽÊá–J5>úJÀóèð…ãJûu>ëa‹N„ýéÜñK1ùTüT –nÔ+—töÂZ¾r”¦ˆüŽÒŽŸÿ§$ß–Ö[Š«ï9›äÀ² fÈ…ªZ3“2t¤yÇNëܵ3:pp‰F.Y£óyÅÝíß*mõ·•©r9 ½ñ†ƒw«]ã\ÊX;‡²ŽÝª'ÅÆgb|6ÀÍ!‰ßW®‰±ÆÄŒÙó­Ýâ]ŒpÂ!f|©¤ÎîJ”¤ŒÆ_ŠüWÌØÂ‰°£W«~6_¹fþ\MúMÿÏcÕ½~øV¢¹Z¬°ÙJôªæÕÍ&g׌z¿Q™ã³ìÅ!‰Í[·G›ÒQ´dEmܼÍÚ âÌXÒx8GP÷lÜ;‹`ÀÞ"œ0;sŒøi¼zô¤}ûZ›à™£Œi _ƒâùbÜ«qÏŒ˜@Bà0áü›ŒõŒ!+Œ-5—ìYõ\M÷0îŸ'ãÞŒ{4î•5&PN€ ãݘæðyŸ/•¾i~stÁóPƽ÷dÜ¡ `W„À®'€]N»"œvE8ìŠpØá°+ `W„À®'€]N»"œvE8ìŠpØá°+ `W„À®>œxðà=¦½P@Àk3Hà6œøûöm :RÅËTѧ%*˜U¢|5-X¼ÌÚ$`NÁDz_›Dƒ&ßiä˜‰êØ­·Š–¬hÛ¸y›õ@9d81|Ô83„5v¢BCC£Žï?xØ<^¹F}Ùœ‘€…œÕÐ}åZh¨N„X-îÌTy/oåøa‡ìrw¯Ÿ»Ónû¼>à¹äáDÕZ õY©Jº`Öä_féì¹óf[—ýÌ€âÐᣖ³ââ–fTI¥Ä)êhÞKSÐ6}ŸÃK‰’Tÿµ‡Áˆ)ôš&”L*§ÌßhåÓ.A8xÁ9d8QªbuU¨V×üùÔ™³fQ¥fC3¨ðŸ:Ãü}מ}–³â"Dç*&7לúnc`´–àý½•ÏÝC‰\Ò¨êì[ÑÚto©ê¤ö’_µ¹²´<áàçáD£f­ÌâÄÉÓæï]{õ7ï?ÕüýÆŸ7m»?“àý½ô¦»ÞéwDÁQGCtjX¹%ÿH¾™D©ëÿ¦»6çmë n~útÌ%YÆT¯WW—±34còp5ÿ$³œ}ÞUçm'œ2d×˯Q£¾ã5eê$õkRXiܼ•©ö|]Ž GÌpÀKiræQ¦|5ÔaÄdM›2V*å•—K2½?ð°Í(»ÚÞó#ùº¦VÞê]5lÒÜV%s$“SšRuäaPraŽª‡½O·WŠ«Yÿ šä?ZÝê}¤”nI•ûÛ•ú3ÚëÛ„Ži\¹Wä’¦¤†ˆHQw©SÞ$rÏ^QFO×ÌéþÖ³¥*n¤ÉŸz< àâá„aÄOãÍ0ÂX Óhޤ0ŽmÞºÃÒûY„êÒ¸’òpË£6[Ãäï-k¬ÔîyõÃŽ°ßÿš­JÉ|”¯Çð@ `¥eðRòÚ‹¾†f€6´Ì-—”å5ñüÃá¡-UÝL^JRþ™ëiF„/¹½®ëlWß¼£õ­òÊÙýuÛ9˜á€»§ùR¿\±yÐÜ«ÎozËõá:ñR!gF«ˆ¯—2Õ_¢6]CÎMV™Ôaï³ÊÌð××=­jžCN>…ÕÿíЋ;ÚÜ.¿\ÜßVç=A†m8rY‹å•{’÷ÔnÝ_QÓXBÎý¤O<’¨è˜ËO?µðBs˜pbͺ:pèˆùó¥ËWÌ)åªÖÖýûç=lÜ¼Í 'Œã_”«¦’ª«U»Îš5oa´-GŸVðáþ*àTˆµ¾eN¹¼ÜB«ÂC¯h\‰dQ Yíé®<îÉôŤkáåAÛÕö5oy•ûYoßÖí¨º¢Iå’É)cs­0¦„DŽœ0¦ˆX¦emï¨n¾*üÓÅðkšá€‡|ªÌÑßÑzhA­TrÎÞN›Ík„êšYy¸æP‹ÈaQîjaÝôJœ¬–æYHàVµÎî%·bŽÐˆ|t  ºû譞؈pâ.[´µÏ'Jâ™Kµæœ·­æþZ53¦Ã¼VSCל“í¬bãáÄèñþfèб[oó÷ž}›¿Ïœ³ÀÒSjÛ±»Ùf­>†Z»>YàfµÌæ-ïò¿èÏÀ}êœ×ÇfdDøâ˜®^%ôÓ… ULnî©ß±ˆGõ{sTÑÛC/9¹ÇZ‰’ÔÖ<#äˆ'\Þ¨#Ñžò¥ÐËãUÔÓ[¹:FL£ˆ˜Öñjë-Š9hqÝ´r~õ{m0‚´·[9{|®笫lë`ï÷äâñ±+p†½ÏJ>^Jßdµbì~°P_úy*EýßÂÛ"ä,^JÙ<<ä’¯›v÷M¨þÞ=Nµò¥•“³¯R實v“¶è’5# B‚'6lÚb† u5×ù uòÔó÷ŠÕëéÁƒÓºyó/ý¶bµîÞ»gŽªØ´e›êÝÂ<Ç}7wµ¨nz9gl®ßý¨ì›S7|•±ñ}›ÓG©üþpCàfµÊæ%·ÏÆé±ë@F„‰#§yØÚúƒ²¹&Q‘hÓ:ž&œÕõŸËËÓ5›š=ÜN$Â-¨NN)êhž±iGØ=´Éé-·¢cuÁ:­ÃœÖ⣷û²™Öá¥,-Öé^èŸZÒ(—œ=ó«ÍÆÛÑOŒ&Pç7Óka×Éß;bz6|8alZ¥fCóg# 0BcáËÀ  Íž·(Æô k>sÎ<×X‡¢tŶ—~*!'†è=zíœr1º!@+›¾*·\ù•3¬OÙ©GUD-4éùŽ~Øò˜‡÷È1]sªñò[6 ·µæÛ×åìþ®º°]ói‰°ËžŸ¨âI½”®Ö‚ˆ…/#ŽŸ¤’)½”¬r䂘ÚÐ*·œ½ ©ç¶s4îhc›|rqÏû‚˜26(Y¥f9}äúÚ·Z~ó1 ÌýßT/­²´Úd™Ž€„•ª×W‰òÕÌÝ8Œ@bÔØ‰ÚàÙÖ«_øÚš·R˶bTÿÁ#Ì©¢B8‹XØò%'o½Ö~§%{^_X_)\ÜõRä4 !ç©Ö«>Jœì-•o=P#ÇOÔ¨!=Ô¬âGú }ăºN$‘Gּʒé#Õë1F“&OTŸÆŸ(µ±•hŠÚ˜#á„qlïÀ¢JêšJyªvÑ0ÿ©?¤]ÔV¢#<Œ B¯,Výl¾rÍü¹šô› ÿŸÇª{ýð­DsµXñè­DÃü½¾r{zëåú¿Ê˜µrvœ*¿WYM»Õèió5oÆxu®ø†<Ýs©ùjÛÝH—àÉ!#F›ÁÂèqþÑŽï?xXEKVT©ŠÕÍàâQBBB4hø(ó†ühm~ ÷µ¬a&%vͪ¦«c¬þ¨Ð?§«¼Ÿ‡œßèª]Öä"Là…µÖ¬ŒÞ|%mغ·Iä‘2§ VéªÙGŒùÑž_­A_W®ô©äæ™Rió”P£›tÅv¤H,ᄱ.Ç–ŽåîšEUg†ÝÓíY¿¸òdNvÌS.¾”­P-uYtR1?= œ8á’@Ⴑ‡ÿÔ6r¬¹M¨qlÊôÙÑú¯Û°Ùì?rÌDs—†M¿3ûS:Œµ'@Â’àà ÃÑã'T«AÓëI 5V¡¡Ñ×:رkDØö«Vû+íýã@´~ apˆpÂ`ì̱yë-^ò»¹ǹó¬]¢ÜÐú[4gþb8yÚÚ ‡ 'Àó‰pØá°+ `W„À®'€]N»"œvE8ìŠpØá°+ `W„À®'€]N»"œvE8ìŠpØá°+ `W„À®*œ Òêu4dÄhµlÛɬ þSuýÆ ]½v]c'NŽ:>ôÇ1Z»a“BBB¬— ˆÃ„ôu‹ïõi‰ 1ªrú*_­NŒãFµhÝ^ÖË€Âa‰ÑãýͰÁ1mæ\íܽW¿­X­]{G»õÑï+טmƨ‰ßw0#*Q蟋U?ÛËú ï™G‚´³S>9{UÐä¿,pPNS3JWª©2•kê~@@´¶ýE…GŸˆÖvÿþ}•®XC•ª×v<®BþÜ«i]ë«È›Y•ÔÛGΞ)”òÕ‚ú¬N7M?pÇÚ=Þ„^Ÿ§êR)_·ñN„^Õš±´öJ¨µ»qˆpÂXOšµlgmÒ®=û¢Â‰=ûö[›õõ7áSAþþû¶µé©Ü?0^å³$Q"—TÊþY}}×¥zõ줦Պ*kêê¸ë_ž2ª‡QÂ? 'B¯OUéäïªûþ`kvãáÄŸ7oFMé°š:}vT8aL÷°ú¦ÕfÛÍ¿nY›žìÎFµÎí«Ä~ïêÛ%d!B<ˆqìßõO‰]ŸY]É= NϱaßKí»ô2~’UkÖ›}ïܹkmþSNDNë(W¥¶nß ŸFaüüø‰SæTÈpÂh?yêLÔyW®^S©ŠÕÍÅ2ã.DçÇ–”—Krr4bZÅãžY®Þu?UŽ´ÉåæFß*«oÆlÑUÛ CBÎjèÇ~ÊÜbµNýÞSåò½,ow_ùd, ÒmçêÈ}›¾wfª¼—·rü°ãñÓ:Boi·•ÊŸU~Þ~òNûº>¬ÙKóŽE>pÜѺ.)¥›‡^rr*çÜ´ýin €Ã0v/ú¢\5-YQk×o²6GY³n£ÙÇèk|Wöäá„¡â—uÍâ³R•ôí÷¢…ÃG5·üÝh3ú}ßÝ<â,ôº&–J¦D~U4õÏ'¯ÑraŽªgñ‘Û+ÅÕ¬ÿMò­nõŒ@ ©r»RQ—0à _9gyS9³|¢½ÆhÒÏãÕ³A!¥põV†šsu)²ïS…wµ½çGòuM­¼Õ»jؤ)7¸­JæH&§4¥4êˆqf .ý±NóÛ|(·×UoÒj­^³Vk¶ŸÒßO¾5ÆX¸X™*æwà¦-Û¬Íæ1£Íècôì-Á‡AAêÚ«¿2£'ÊV®BÔkÜ"Ú¿ ®Z»Au¾j-¤0Ä4~6®‡a[õ}/¹¼ÓOŸ8 âžV5Ï!'ŸÂêÈv¢Çmn—_.îo«óžˆ×Ž'^r}]-ÖÙ,¦zK¿}SNnÔe_Dß§'BÎŒV_/eª¿D7l‚†s“U&µ—’W™©kæñP]PJnîLë^[·ï4£¶íØu<2˜0Êè$ >œ;arTØÐú‡.Öæ'2Ö©ˆ<âä_¬Íö`•gð”ë§ctáI£ ·ªuv/¹› Ë–¾ÁGª »ÞêyPf$N8ei©µ–+7·UVW_uAæL'†¡ºæ_V®9Ôb½uõ‹»ZX7½'«¥9fB8¼h6lÞ5Bb÷ž}fHLm@B‘àà ۑ¶á„1 ¢[ïZòÛŠ¨c‹—ü®î½škTD² '*T«uü‰·¨Uv/¹¼;@‡Ÿô,oŽ*ùx)}“Õz`m X¨/ý<•¢þoámá„ËûƒtÔrÝÐKãTÔÓ[¹:î#žNio·röø\#ÎÙ.laÖÁÞïÉÅãc îÎVEoO¥k²*f8q?<œHõÕòèáD,¡Gè…1*ìá­Üq'ötÉ/gÏb '˜áD 9M8¼È–¯\õhü $4N\º|%ü˜Íö¢‘A„Ñf=çp"ìáþèÀÿÉÕ%µJLŒ˜fñ(AÛÕ&§·ÜŠŽ1$øpp÷ÑÛ}E›Ö‘8cS-ˆÞ7p}+½ìšDŸŽ¾$ó2O 'Buýçòòtͦfk¬±È-¨NN)êhž¹iG¨®N/¬u6kýÆ-ÖÃ@‚àɶ»G ¶Ó:ÕÈúeæÜ¨c‹–ü¦FÍZE[øÒ6œ° 2žFèÕ¹ú2ƒ§§-¡~Ûÿ  b  ­rËÙ»zþa›8ÜÑÆ6ùäâž?–1³ë«ßl†d„ÞÐüº¯ÊÉã}õ:<1œ»Üù‰*žÔKéj-ˆXø2âøÙI*™ÒKÉ*G.ˆ)ý=­RÄúÖ ûIðáÄÍ¿n©QóVf¸`¬a¬4o»¦Ä£}6lÚ¢rUk›ç6øú[ݸñ§µÛ„êÚŠvÊŸÔS‰Ü3)_¹&jÛ½Ÿúôé¡¶Mj©èûÍ5çW«~6_¹fþ\MúMÿÏcÕ½~øV¢¹Z¬ˆ±•¨SÆÊœ©êt­IþcÕµvA%uñÑ+ ~}2 K*_–´òpó”“Gr¥xõ=o8^»lJž_­A_W®ô©äæ™Rió”P£›tÅ6ˆ'Ü>ýQ›wW™73ËËÝW>™ÞQÙ‹t"ÚÀ‹§ 'dnCºoZGU(˜CÉ}’È#eN¬ÒU³˜ó9lÜן›èÃ,aï/ìÒ¼ßS[ã°»*ÿ‡'"#Œ­E«×mm‰ØªDùj9fÂ3Œ–ø—E„®…†éÄ“€ðÜs¨pÂÖ¹ó´`ñ2 5N»õV§î}õãèñš=o‘9õãî½{ÖS†¨pb(áràpÂaN áÄp€h'€]N»"œvE8ìŠpØá°+ `W„À®'€]%˜pâÖí{:åO8{EGO_¢(Š¢@ßÉÆw³ñ ü[ìN<ÔÙK×cü…˜¢(ŠJXe|WßÙ@|³k8aü%—‘EQŽSÆw6â›]à FLPE9^ßÝ@|²[8aÌ_¶þ…—¢(ŠrŒb ħx'öŸþ[…ZmRâb‹Íÿ¿ÇÆX`Íú—]Š¢(Ê1ÊøâK¼‡‘ÁDd¿Ç†µ&(Š¢·Œïp ¾Ä{8aLDVl¬Ñ¥(Š¢«€øB8AQE=Sñ…p‚¢(Šz¦â áEQõLÄ—Nüqø´ú­¯¿m§¯[´}¶ ;wÀ°1úãè™×§(Š¢ž­€ø’àÉÆß´Ñ§%*ÄK5oÕ!Æõ)Š¢¨g+ ¾$øpâ³R•U¶rm-^¾îU™*µôyéÊ1®ŸPkϬVÊ›*©¼_)«žkÎÇh§(вwñ%Á‡ƈ‡Ê5Ä8×2®a\ËzüÑuAëz|"'w½K¹¼Û[kNZω¯º >mç”*3Žé(E%¼â‹C…›wî×媆U5mÝ}0Ú1ë£lû9V8qIGv.RçÆ T³Õ-=³zX‡7öUA/¹|Ø_ëÿÅÿ'EE/ ¾8T8±cïa5lÖZ•k6Ô®ýÇ¢³æy6ýþQ8ášY%û.ÒÌùK¢jöŠ?t Æ9Ô_çõûïÊÅÙp‚¢þãâ‹C…ÿ¤þY8‘Cu瞥OX_¬º™½ÂúdSYû4³Gm½›-½<Ü“È7Ó»*Ñf–6œ0úžÑÔúÙäd\Ï­€Z®´YGâÔ .‘R‰ÂÚù•ÖÀ]´¥ÿgrµ¼ö¡U”ÇÍ]‰SÖÒ¸ÃG5»g ½•)¥\¼J¨ßÞðkùc•ú]Vo¾’NžIä“6·Þ©øƒF¬:nó¾ÏhTãõ|ôz›5Z3µ“JÈ®$^>rO‘SªôÕÌ=£úG¾n¢äUôãÖêY³ˆ²¤L*Wï4ÊT ŠZMÿCw.PÛJ*s ¿°÷“V/¿W[ÝÕ‘hŸ×­þ¹«*}ô†R'õ ;?­2¾UN\¯½ÏòÞŽïÖ¤¶e•Å×Ã2²ÅGowÙ®ÃÆçq`­6 ÿ<<ܽåâ›Nó•RÍŽSõû!Û÷FQT\ ˆ/ >œø¬T¥x[ÓX\ÓzýGWà '/¥É‘[>.–) ÎÉôVûu:Ö÷àÂoõŠ[øÃsÞ6™ÇÌkì§/’Øžò+;Q;N_|l8ñ’[n}QåJñZ‰üªé§£F0±@ÞHn†Öi(‰“}¨–‹NF¼ïÈÀC.™óèeoëÃ}Øûøl¸ÖEŒBˆz]—tÊ’-[¯í÷¦òåJãu2ÔÖ¸ý‘ŸÕy­PJ)]Ã_+‘[2yûx‡Ÿã’ZïvY1åéßÛáÍ}UÐ=úk†WD8qꀆ–Éõ~9{„UÄ{{õÍ<ËÿOŠ¢žº€ø’àÉM[Ř®ñ¬õUóÖ1®ÿèzÜš>Ê×q‹ù/óɰãÎ~Ê^m¨¦,[§YÃë)»gøƒµóô«1zâø*5Ëåv,ìÁ»@w­ˆxøßùS¥ð Á%ƒÊO0F8H~<ì~ªL ïGöNTÉ”žá¯÷^{ÍÜs!ì󨣑UsÊ)ì='NQQC÷¯—÷vQ‡,ÕWÙŒÏÔ].ôÕšãçtèøy>vŸÇfªR*ã5Ã>«újõѳڳm½¦ í¤oGn~QõLÄ—N”®\Óñðu‹¶ÿ¨Š–ª¤rUëĸþ£+îáDâ”_ê§çŸÜ£Îú„OWOþæ¿ÒŸ×Ò¶Ì!‘G!µßö€~ú˜~,ŸÆ|˜wÊPO“ÌÅ/ŸNxÈ9Ç×úÙ|˜x½Ô2_øë9ej¬)G#ïÃv:É»jmnKj¸æT½¨Q!ç´è»|}óªÙÒs1^×óóµÕxð;¾j]¥4Gox*Y¥)QS3öŽ«¢$æ…$*2ähıªáÇœSªÔ˜QïûÐÒ6ÊnŽÊȬ/1ÞGÜÞÛÑã¿«QöˆpºæÄñ•jòZx["ïœ*üÍOš³ã#`(ŠŠsñ%Á‡ƈû¯9a]s™~ݱ½§M8aìà±:êáø”†”LN¤®­ S¯ë©·<<ÂÒ“¨`÷:t`²Êš# ¼”±þBí7Ï}R8á­l߬Œþ/ÿÇfªJjã:îrýxˆ6GƵ¶ø<üZ.TÙÉð0HäUZƒ"óž?¿g·¾¦’£Ür«ÑÂÈ×xR8á£7ÚmŒN„Oa{ÿ¢M¶áDäµ\2©ÚTK8áWM£¢FYØÜs¬áDô×µ 'Þí±û±áĪ΄_×9©Òäþ@o½c­Oôõ4#ð‰Û{{|8~ÿ{ÖLQ«Ê(MTHá!·\-5óˆµ/EQq) ¾8T8±bݶ¨õ#ÖlÜãXlÙ/Á„aË“§1µ#I)Õ®>åÂ)W[-<þ°OœÃ‰›ô}þÈi_irÔƒ÷M©›5b:Ä{j³6ú´ŽÈÅ4cÜs¼†—´wlùÓ:\R«ÄOáS=b¯¸½·£Ç—«qd8ñA¿¨E#ÎáÄ%ÙýÓÿÛ»覫þãGh“6IÓA¡¬²ÊFá¡"**S¢ˆ …A¡ì%† e¶d/EPq0E@d*ÊFÜÈìÊ矤+MKikù§•÷ëœïá$¹¹¿¡§§÷Ó{ïOMMö~K«Xqûw½­ª>t»K؃pâøi}<ü^ù:÷u(¬;ºÎТõŸhÑäÿêç£6M2†Ð‡nbf%¸Þq³NÙ»@-“6§4Vè¨ñëöiß±SÚ¿¯Ö/ž¦F,Õg°½s;zlGÊ^‹4ՠ囵~ÝJÍßðÿ°IúŒÔK?׎ƒ§uô§ŸôáKõetÜ#û}MÝÏ‚¢¨œ[òU8ñOê…”c‰-ŽÁt‰£Çjz«ÄM0Ÿ0ݧ¡Û\÷@ÈI8a¯CŸh@ÝtútžC@]õ}×ñ$GÛì×;nv Gß[g¶W)_÷Gƒ&–÷£ôq‚“£ÇOjų՜OüHíÏOåzoÔ¡ïW¨]Ò>·y›ååcNz ˆcYÇ@­dYEý£rKž'Úuî‘kOëhßµgºþ¯_73œ8§½QUØù¤ “L÷¿á¶!‡á„½ŽئiýÛ«v…Rò3ûËT¤ŠÂ[ÐäG“BGe/¸Þq³N8ê´v¾;U=¹We‹‘Ñh‘Á¿”Âê=¡g§oÑ^ç½ËÞ¹9ëÀ&k¯B ÈË'X…+Þ¯Ž3¿ÖácÿÓ[}Úªv¥Òò3YTÐ'@þ¡5uoû±Z¸ËåûE娀ܒçÉÕï¢f´K·—DvëáÇ:8—¸÷OQE嬀ܒçà Gí;ô£ÖòyŽkç۴ÿðñtýREQ9/ ·ä‹p‚¢(ŠÊ{äÂ Š¢(*GäÂ Š¢(*GäÂ Š¢(*Gä–\'êü"M0áx‘c'ϧûE—¢(ŠÊåøä–\'¾;þwJ@áø×ñ:#§Ïÿ‘î—]Š¢(*”ãg8[r=œÈª ¯¤ûe—¢(ŠÊåøä…'Ïý–î^Š¢(*o—ãg7›<N\‹‰eï Š¢¨|TŽŸÙŽŸÝ@nòh8áàø%—EQy¿?« &p3x<œHæX¿ìØ`™EQy§?“?›Ùc7Sž 'À­‰pxáð( àQ„À£'€GN"œE8<ŠpxT¾ 'l6›¢ßY¦GÛuVãm²T޶‹–®tï ä!ù&œøø³-釬ÖÖm_¸wòˆ|N,Z²Â4|øñgî]×?u~Çñ]7å›p"zñrgаñ“Mî]—£­ã;Žïæ}qÚ="\Þ–6Zô—ûgü{NdAÂû´dtw5ªYAA~Vy› «Hù:júÔ-;pɽyNnM„7põÀ|µ PCˆ*5í®þ£&jÂøz®CU(Z[ÿ‰uÿJNnM„™¹´C/ÞuõÂgäCØbbÒ½—s„€[áÄu%èôÜY Áª?õ¨âÜ?Î@ì‰OôÊÓU¹x°|üŠ©Ô­ôüœ]ú%Á½åE}·xš×(#?“¿üËÞ«Ž“6kó¸z2¸‡¶?õuÔ‹j^^–YKÝ­G†®ÒáË.m.Ò²¡ítWXq™|ýä[(L·7ìªQœ–óÐ 'õæƒ*Óo³~úx¼ /+?_{_µõÈ5:rÕ¥/ÙôÛ× õRç‡U«J9˜íÇ,®¦}ßÑ~·,¶‹µ|DgÝ[¹”¬– –ª®ºfiOLr‹xû|¦žiRSÅä\Iww˜ N¸D:¶¿ôÕüþjn¿¾³Ÿ Öâ*sg„ºMÙ®_m©Íÿ^ù&œXºb3h˜¹P{¿ý.K5{~´ó;Žïf›í7Eµ,¤Ohñ7%'œY­NaVù”k®>¯GjAôlév¿Šøéö>Sj :ý¸Š¬*Ùh€&G/Uô[cÕ±V9• +-¯4áÄížð€ü}ËéÁ¾Ó½b…æŽí¤jþ~*ñÄrv&õñ³åx—:ÒÒ•Ë4wÚ8õz´¹úp!éŽpÂ_Þa5U%¬þ;aŽ,œ¯ñÿ­¯ÂF?…v^£s.çw&²ƒj4é©aS"µdõ ÍÐTÅíí* Ú©kÉÍbéÍ&ÅUЦúÝGëõiS5fPOµê·2©/›þÜô¢n÷ T¹ˆ—5cñ*-ž5\‡ÈX¹¯>rÞ›Î/í Â>%U¯×E­X¥Es¦ê¥nj>vWê±ÿjù&œ8~┚D´u† Ù©¦-×ÉSgÜ»»±Ø/5¨²E†»_ÓÁx÷Ý]Ѧ¾•åem¨×¹.ô¸¤CkÉà{—FîMš{³S*Úû­1L»\f?Ø~]£%̺Í%œH8¥fVÝ>ôK¥6בIÈǧ¦^úÚÞgÜ·UÓ*kÇwígqIáÄmÆ;Ôïs—é¶ ÚølyùÔÖ¨o3™b;«·›ÈPsœö9›ÙôÛ²*d,ª‡fËxVIüAM¨ sý):äÒàê®—UÕ'@÷½ù££Ïz‡ÉP®¿¶äÞú@>“o ‡={¿Õð15`Ȉ,ÕÈq¯jÿCîÝdMÌ&õ 5ËØxŽÎÜhâDì—z±’E>Í"õ³[Ûø£“TÇת;Ç”#ãˆ?ôªjÙ_‡'½NuIkž,ª)á„M¿/l-³OM Þñ—.^¼˜R~1LÕ|üõà[§•`ûC+;…Ê+¨®z.øZç3ä'…^a´ÕíóØCTÁ识oŸI\’¡}ؽ¤¼S¾EëºWÁà.Zý·{ÛD Ǧê“¿î›tX\Îýâ¨{I³,íVê¢ýˆÇg=,?c¨î²Zûÿ¼a øÊWáÄÿ«Ø]XÉ"CÝ7tøFcæ+«õ¸Õ¢’½7+e»…d×Ö©c Y…»ot~³éy•4©Ír×M#âôõ°š.bÆiïè»äíå«Û2,«j½’pØ~Ý®WÛT—Õ`’©ÌzrìJíýÝ夓 Ã=“uÔíZlçæ©‰ÙOÕ†ïIš‘ _¾ŒÒ öMT=¬”‚äk Áh’Wò ‡„ÓšÑÐ_Þÿ«ä !îb6õµ_§û9§VJZ†µPi?“ VUÃg&kÝ‘¿u£<ðï‘çÉŸNœTŸCÓ-×Èn=?ð¥ì-ï°ý¬9ÍU øI­LÚºáº.¯R[?³JôÞ”>œ¸šN„<óIR8á´Òã+ÝaÄiÏÈ;Ó„{FÖ’·oõ_³]Û¶ïp«Úsʵ8ýºï=½Ö³©ÊØúÞ%Ò„]Iƒüäp"ƒ ÅvfŽšüt{R8qiÇpÕð³¨Hý~š¾f«þ÷íA:üfµ ‘wJ8qJÓáDÍq×'>ë£â†=8ö³ Î}‡v8ŸæI'×ÎþO‹Ç>­:%ýUÀ\I­ædÏ ¸Eäùp¢ÃS=õÐ#íÒ-ÙÈn9öžèÚã9÷î3¯£“ÑPT-¢2[ò`÷•Wñ“O“¹é–€Ä~]µ}­ºkâ¡Äe':—uÜýúQ·e×ôQÒ*˜NØôKÔ#2+©ïÖt‘G¦®|ÿŽž(g‘©Ù|uœOR8Q°ÔsúÄmÄ»m ÊÔxö9ûcì窂­ý‹ë…\ÖÊöÁ©3'ì¯×<¢‚…»jõE—f.⾡»ËWÆp»ÎÌÙ.ìÓ¤f%åeï{Íuúü»äùpÂ1ë¡ãS=ÝßÎ6G޾²ÃöËu 5«`ñzí«¿2YjpMÛÞ.o¿ú¿ßuôI;‡Ëà[ËeCÌíz¡‚E>µÇk¯KSÛ_«G˜%톘'樑¿EEÛ¼£™ŒðÓŸ×_ZØ*H†:¯'ΔHÙ³’žÙè2 Äö»Þ}º¼¼L÷hÂ!GëZÓ©ˆ¼Bûè×<äïOÔ³œÅ%œ°éçèÖò7SóyÇ3âökôVyUè¥ ™=4ÝG :6åAÍ-5/³ïþ5òU8qîçójѺƒ"ZwÔ/¿üšæ=÷eŽrm—“pÂ1rþõÓ¡ªdVßÒ ¬·†Œ}M'ŽÓÞ]Ôäž¾)BÚίW÷Šþ2–yH½_‹Tô¹Û=ñQ¢Õú}êò(ÑxýÙJ… þ*Õt &E-Ö‚·Æ©kÝ*ªz{…ô}µ  mÐK£¦GjÞ¼·5qh5®ù¤¢ÎÛœOë˜ðpC=Ñ‚¦F®ÐšµË4cp„J›>v_âÒ‰ä 1KUV™ÒõõÔØÙZ=W£»ÖQÁªrÿÝ Ä A'çFÈj,¥Æc6蛎iÿÖ%Ò´ºÊ•+žº¬Ãáê^¿7D-•Õ¸ÏDMŸ=G“Ç T‡v¯iGR€ñç¦Áªn5ËR©¥ú¼2Ks#çiê„Aêð@-õ|ß‘ÌÄè³A Õè©—4aæB-_»VÑSž×}ÅýðÐËtº àß"_…¿ÿþ‡ú|Ižî¥¿.$ÎH~Ï=˜p”k»œ…‰.¿A{D(<¬¸L>fy™‚U¸|=5ï1_߸Ì~ˆ=½Y“Ÿi®j%Cäc.¢âÕ[¨çŒ/tÞ}jíoí·—’Ù×_þeëëÉi»tf}û¹†‰m÷/£Ž÷WW± y› +¤Jµ¼Lß]u|þ«6½ÖUõ«•‘¿Ùb?7ûqïh¦n“·èlòq“ ŸÆ3µsýX=Z³Œ,öãZKß­VÃÞ×1×Éq§´ad[Õ(YHÞÆ@ªÐPÝg}­“«:+(,í#?m|£Èmu—ý¾˜}ýd.VC ž[¡R®7A¿}µ@/¶½OaÅ‚eô P@©p5è:VïýäœÒ¡ëFè±zÕTÄß*/£ýó2w«yßyúò7fMÀ­"_„º>ãþv¶µïÒ#ÇáD¾—NëOc6 ÏÉóáDò¬ˆ~/¾œn“ˬ–ãIÎ> sïþÖN¼I8Èsò|8qêô™\y”¨#¤8{îg÷îo „€<,χÈ„€<Œpxáð( àQ„À£'€GN"œE8<Špxáð( àQ„À£'€GN"œE8<Špxáð( àQ„À£ò|8qåÊUÍ[ðŽ ©CFä¬ìߌ^¬«×®¹w<,χýWãmr¥† ëÞ=ð°<N<ôH;µéð´ö~û³ƒ#hXýîzçkÇ¿ÉÁCr›Œªu‡§ÔìÑ'Ü»–çà GðÐñ©ž)¯_›<Ýùž#pppüëxíx?3Ž>í²ãÚúî òöÕm^öò6ÉÛ¢¢•îÓ#ÏÏÖ¶ŸãÝ›€ÈáDÛNÝþñÌ G9 ' !ªßo–Þž=GÓ'W¿öõâk’±J_}ô»Íý+ ›òE8‘›•ÎpÂXFÏ|ãònœ~Šj­Â†Ý;õ˜\>Ù—/‰Gï¬èÅËÿQ9úÈpÂîâjµ 4Éúä:]u¾aÓo_/ÔKV­*å`µd¸šö}Gû/¥ýªbOhý¨öº³Lˆ|-!*]§£FpL_¼\S¦úoê˜kÚqé –k¯Úa%d6+äŽz.rþr™°sü#éÔ@ŠËàão?n ÝÓvˆ–Km@–/ ×='r*Ç{NdN\}_O2»„ :ÙA5šôÔ°)‘Z²z…fhªâF?U´S)0µ]ЧýjÈh(¡ÚÝ_Õìè…š>º›î ­ªÊet 'â¾×¬¡ò.rž~u–-ÖÄn÷ØÏ§˜Î8"gôûFü'@¾•ÚjØìeZ±,ZÓÆP›†=µè,KNùáD&®N\ÚÒ_åºwJ&Ë:lgõvÓjŽÓ¾¤I ?N×}‹Êôب?]²ƒ‹[ª¢ošpâ¯wŸVˆOuÛð§RšÚÎ)êÑ¢ò í¥.Ûû;5K Lj2ççÔ6ä3„™H 'BÕiéO:uò„¾ß¿Sï¾5@÷—ô“WÙîZûKf‘@Œ>ì^RÞa´5ÖñÚ¦_¢‘¯±²^Øî|#Uì.½XÉâN\ÓÝCåU²§Öý~Q/&ךÜHFŸZéH<®nUŸ0‹LU;ëÍ-§’fq¿Nd"Í£D“Ë;H¥ÔÒÃW\Z&è—/£4¨}U+¥À€ ùZd0šäU®¿¶8³ˆ8í}—¼MiÆ)·ù §4½¡j8‘pZ3ù§=®k+«ß6G§6ý½gžº„——·¿BjuÐлtÎ-û /#œÈDâ£D‹©ÙøuZ¿a£6nþRß¹˜n)Ç¥ÃUÃÏ¢"õûiúš­úß·uèðwšÕ&DÞ.áÄÞ¤pbæi·lçôv“—p"1¬ðªØCQ[whÛv·Ú±[Ç.¸ÌÚH¸ Ã¾­~Õe5˜X{ˆ6f:«€¼ƒp"×Ûs"­}Ô#T[+:M pY+ۻ̜°éüüù«iànSâökT¸ŸË²Ž+Z÷Tq,ÜY«þNÛ4s±:½¾ªúZU땃Šwÿ€<ˆp"Y '®jM§"ò í£4ÍþþD=ËY\ )þè$Õ1YT¾Ïf¹f1ûÆ)Üdr 'lú}eg6†èÁ)SŸö‘W7ª[q«Â~!Vwò‰Ld-œHÐɹ²K©ñ˜ úæ‡cÚ¿u‰†4­®r劻,ë°³ý¡ ÏT•ÁÞöž^“¹t™æ½ÞO *TUÙR®bÊù(Ñ9-KËË'Tµ;Ô¤9QšóÖd ëÙJµZLÕ¡xû‘OÎS»zíôÜè75{É»Z»|¾F¶­!³o5õÝ|)ùÈÓò|8ÑüÑ'Ô¦ÃÓŠÏù"…¸¸8µî𔳯ìÈZ8awJF¶U’…äm T¡ Õ}Ö×:¹ª³‚Â\ ‡kÇ´vØãªYºˆ|ÌETòÎvþÞN½éÜsbZj8ápõG­­§U/§‹Ÿ Ö*W§­úÎý*ñQ¤wë­îÍU½LQùÍ2ø‡ªbý.õþÙ›m€åùpbÜ«“3r£&NšæÞ}ÞT¯Ö³Ê§Y¤~fKÀ-&χW¯]Ó¼ïhÀ9¯¡#½x¹bbn0ÂCbMÖ=~~ª0èKö‰Ürò|8ñ¯ÿ½¦µm¨6}ÆjòÜÅZº|¹æOꯆeýU°ðÚù}Η®_Nü²]Ðηz«EÝê*V(PÞF?™ŠTÖ] Ò‚½ÄŠÀ­ˆpxáð( àQ„À£'€GN"œE8<Špxáð( àQ„À£'€GN"œE8<Špxáð( àQ„À£'€Gå‹pâ«Ý{´pÉ E/^ž£r|w÷ž}îÝ€< χÓßž§Æ-ÚäJ͉\èÞ=pC¶?Ö«{Ųº÷Õ½Šsÿðåùp¢EëjÙ¶SºÙÙ­ˆ6œýäDÂû´dtw5ªYAA~Vy› «Hù:júÔ-;pɽy.HÐOÌÔ¢ÝÝ?ÀM•ñ}·ý¶VBC>f7áÜy>œpÌxèøTO÷·³Íч£¯ìºz`¾Z‡¨€!D•švWÿQ5aü=ס‰*­­áßĺåŸKøQ“î+¬Æs~–Íý3Ü<™Ýw›-ý{€\A8‘™K;ôâíþ*XW/|pFî1„-&&Ý{¹!îø,5ö ÈxŒ›†ûž‘ï‰å«ÞM·—DFÕã¹þ.½ä$œHÐé¹²‚UêÑL翦õO—W‰gµ1&í'±_ VycÚ­¾–ò^Ìñ4¦SU(,ƒ¿¬%kèž¶C´ìh¬Î¿ÿ‚j[TÀËW·%—©¡¦ü”øeÛ퉬–µ*(Ð/P~ÅïÐ}'hí÷—Súwã“Þ*æ×RsíѼÞ©RH ~¡ªÜä-:tY1'6j쓨\á@ýK©jóAZþ}ê9¦§Ý#Âå[{¢vY«A×T?«|‚+éî¯è£S.w(á¤Þ|0H©K?¾§AÍk(Ø nèJbßñ¶z=t§JÊ·PYUkÒK“>=%×Ûç¼K„fÿ°O‘Ï6Vyû¹üJ¨ÂÏhêŽ_•tGR\³ß× ]:ï«ÑÞ®âƒÏhò¶_\Ú%^ƒéžÉ:zá€ôj¬rÁ2U}IëßÍä¾_Z¡Ö?U~éë4ÿÜøxñ:·ušº5ª©bþò6«HÅ{Ñ;JßÜŒÕ@Oå»pâä©Ó¡ÅÜèïèÙ'b¿ÑˆÿÈ·R[ ›½L+–EkÚøjÓ°§µéÊÉ=Ú´ðYUñ±ªÖÀwµiËVmþ|¯N]u|ù²¾¿üEõŸN£5mÁ;š7eˆ"*’W±–zûHêÐÙ9°÷)¥jÕ«©F{{Ûèw4{\WU4ËX5Bͪ•VxçqšaÖ˜Nªæo¶Ú'é`|Jnö†€;T¥b55y~’fG/ÒŒ‘OªzY>Õ곿’î“3œPÀ£/¨{ÕbªÐ¬·†E“Öÿd®ÛôǧUÍÏO!÷öÒØY‹´`îz®q}+©ÓŠ3)ƒûÄkUåj•Tõ±—5%òEN{Y•‚T0 ¾ÆíqÞ”ÄCž]£'ì²ÞÑI£æ.×òEÓÕ·Ay[ëjäÿ#‘äkð)ßY}Û”W¡ê«ïè‰ösØ¢'2¹ï„Y9^ü3õ`€U¡MëÍE«´,z¶&ꪆíÞÒáëÞg¸õä»p"§²NÄ~©A•-2ÜýZ&ödY'NÍRS€šd²t þðëªí›~yA‰ÙjäoQéîèw—N-Ò£E- ~b…~Mzß9°7˜äß"J§Rþ”§oÇÞ-ƒ—YE;­Mik?K}5,\Þ¦¦š~Ò}>B²Ä½··¿îšpÀe9‹Mç–´WC½}*1Xp†þºÍ;HáC¶ëO׋ˆß¯ÑwZe¨ñ²vºNöˆ9¤7–W¹¾ú4éýäk°6›§.§çô›U¨ýj%æF×´}Àí2i­¨Ó© m}¨§K[ÐziÒµ&]ƒýúNѾ´“M®{ßÓ‡Y;Þ•dò­£±nø?ÜÒò]8qùÊíýö»tuäè.ßJ/ÛáDÌ&õ 5ËØxŽÎ\/EH‘õpBW·ªO˜E¦ªõæ–SJýÛªŒÉ6ýÝJ&ceõÛæ¾ÓÅe­{º¤ ê¢ÕIËöî}ØÏô½§äo(¢v«Ò®+¸¼¢½L>wjÄÞë-`IØûÖÕ8÷Áöå êRÔ,ë«å|ÎER8Q0¤«Ö\HÛ4þûɪëkU­W*m/öë[ð¨ýú*êùϯ/ùšÌu l¿)2"H^¥Ÿ×&ÇýŽûJCªúÉòØB½xQSê¼sÎå›ie;œˆÝ¥•,2Ô}# Sð³N؇¨ï™§.áööÞþ ©ÕACìÒ9—¼!ãArœö©-oÓCš‘:"I¼¾ROÓƒšôcâgν±˜º¬O;¿¶¾»‚|*ªïÖ´ǵÕdò ×Ë»oN˜[)ÊuÚ†CüA­e•ñÁ™:î8|R8a¨7IGÜî]ÌæçUÒ~?Ú®H^j‘*vû‹*k Ô#‹äkèºÁ=HˆÕ–ç+ÊËÒNKiÈ•ÕjëgJÝ+­ tÕZgÉ×ð¨"Kd|ß•>œÈòñ®éȲ~º§„¿ øSÕ–5ó³Iûn’å»pâdùjE/^ž¦6~ºY6[úg²l‡¶Ÿ5§Y  ?©•nýO/“pbÛ@û€;톘N tøÃ·Õ/¢º¬³kÑÆ_Ï?ãArœöŽªå\z‘Q8qÀN4ÒTg:<°/©n¥=¡Äp¢RÊì„”÷³N<¢ù©ëAÅïרp¿tᄱþ›:ævª1›úª„!HmV¸­©Pò½ T«%;_'^Cˆ:¯KNlêS^“ÉË+ÕÆÏ¬Â­ghóöÚæVÛwQb‘t –6Zô—[—ºÞ}Wúp"ËÇKd»|\["‡«MÍâòöV•n«tâ†Ü:ò]8‘SÙ'ìþ£“ÑPT-¢R7iÌXŒ>ì^R^ÅzéC·pââªN²zgN¤ˆÕéõ}TÕe©Cƃd›~[ØZfcEõÙâv]Ò{]KÈ«ðSZëº_ÃÍ'|îÔð=nm.½§Ž…Íòï¸VÎÅ"™„ ǦªžÉªšc¿K·¬ã|dKù«è…í®Ë:üõÀŒ“iï¿ígÍn(¯rýµÅÑ4v§V´È§é<½~>¥\ '²|<7 ¿éóaõeñÉhiܺ'2aûe:†šU°x ½öÕ_™ì§¯JšÝ6”´]к§Ëª`¦á„ÝÕêVܪ°_87šL8>CõMé÷eH8¥æA•èòžËf–Ž,`"ŠXT¨Û†˜7#œð¶ªÆðÝ.{e$èdT+‚ÕlÞÙÄ{”I8¡øCz¥n€¼« ÒçÎ *’ÄÔ«õ e¸!¦¹Á }ïrZ1ßP=?³ŠtY§ÄI-W´©oey™ïÖK»\;u—y8q½ûž.œÈòñÒ‹ûj¸*ûd4n]y>œhÕ®‹Z¶í¤Ý{ö¥Û3«õõ7{Ѧ“³¯ì±é×O‡ªVY|K+ü±Þ2ö5Mœ8NCzwQ“{újuâ ]ûßÝn²¨ÄÃãµzç~}»{³ ‹P™¢ÅåopyZÇÉyjW¯žý¦f/yWk—Ï×ȶ5dö­¦¾›“6©ŒÝ¥+û©`‰&ê?s±E-Õ¶³ŽQþ5í›ÔDAÆUo?JÓ¢kþÔ¡)}ëHjàp³Â ƒ¥‚Ê–­¤Ÿ{C³.Ôô—Û©²ã1¤5‡i{ò8=³pÂ~O/l¦šþ®ûŒÆÌzGÑó&%=J´¢:¦{”hQ•+«Š-kRä"Í›ò¢š†¨`¡Æzã»ÔkK8»V]Ê[íïß©Ö/NÒ[ó£ôöÔqêÓö~Ýûrbès£pâº÷=]8‘µã]^ÕKá-žÕËoÌÑÂUë´bÁkêR+D^%»heÒ@>'-Y‘nã˜֊Õï¹wŸ%—¿ß ‰="VÜ>€7ËˬÂåë©yùú&åàWtxÙ`=üŸ²²š¬2…TU½ãôþ¾…je-”:sâân½Õ½¹ª—)*_£YÿPU¬ßE£ÞÿQ©K·à¿zK)@Ö²jæÑ¤¿åÛ.èÛ%ÃÕ¦Ne[d*REuž­UGÒîáp³Â ok'-Ü»Rý›ÕPa??ùª¤:O¾®OϸÌ5È4œpHЯ»æëù–µ(£5T•è®W6žtyDir8Q^½ßûŸæ<ÓHå‚äm)¡ ziÆ—¤›É{f«¦õyT5Ë—Ù×O¦ÂTóá^z}Ó¹¤ÀãáÄõî{á„ÃŽwh‰ž‹¨«2!…äm°^¤²j·yY‹¿ãÉà*χ_íÞ“nÌìÔÂ%+´oÿ÷n‘-.ûnš;’–în àß%_„È n4ë ÷]oöàß…pYD8¸9'E„€›ƒpxáð( àQ„À£'€GN"œE8<ŠpxÔÿ25…g¼êõÉIEND®B`‚uv-0.9.17+ds1/docs/assets/logo-letter.svg000066400000000000000000000010241520155276700201440ustar00rootroot00000000000000 uv-0.9.17+ds1/docs/assets/pypi-add-trusted-publisher.png000066400000000000000000003126341520155276700231020ustar00rootroot00000000000000‰PNG  IHDRx¹KÚ‡_ IDATx^ì˜d5ö·.ƒ»»»»» îîÎâîÎâîî-,î,îƒÎÀ×oøÒÿÛ5UÝUMwOUõ›çÙ‡îÜÜäMr;¿œ““!øg˜$  H@€$  H á ¡Àkø>´€$  H@€žA€$  H@@“Pà5IGÚ H@€$  H@ <Ç€$  H@€$ &! Àk’Ž´€$  H@€xŽ H@€$  H@MB@×$i3$  H@€$  (ð€$  H@€š„€¯I:ÒfH@€$  H@¨(ðþûßÿJG€$  H@€ê”Àì³Ï>HÍÚxs]ò^6¥÷UëÛ#–ê}¶Å€$  H@@Yo¾ùf(ðxp(ð¸ó¬º$  H@€º˜€¯‹ötq ¼ž&îû$  H@€$P¿xõÛ7UÕLW&3I@€$  H WPà5x7+ð¼­¾$  H@€º€¯ aŽ¢xƒƒºï”€$  H@@}PàÕg¿T]+^Õ¨Ì( H@€$ ¦' Àkð.Và5xZ} H@€$  t!^ÂE)ðuß) H@€$ ú$ À«Ï~©ºV ¼ªQ™Q€$  H@MO@×à]¬Àkð´ú€$  H@èB ¼.„98ŠRà ê¾S€$  H@õI@WŸýRu­xU£2£$  H@€šž€¯Á»X×àhõ%  H@€$Ð…x]sp¥ÀÔ}§$  H@€ê“€¯>û¥êZ)ðªFeF H@€$  4=^ƒw±¯Á;ÐêK@€$  H  (ðºæà(J78¨ûN H@€$  Ô'^}öKÕµRàUÊŒ€$  H@hz ¼ïb^ƒw Õ—€$  H@@Pàu!ÌÁQ”opP÷€$  H@¨O ¼úì—ªk¥À«•%  H@€$Ðôx ÞÅ ¼ï@«/ H@€$ .$ ÀëB˜ƒ£(Þà î;%  H@€$PŸVàÍ:Á¨qÿN ÇÅO¾»ßüâ`§{þzsÄóO#ìvsüüû=VŸzx/¾øb¬´ÒJ±ÁÄ‘GÙcmÏ/úÇ?þ—^zi|öÙg1üðÃ÷øû{ó ¯¹æšØzë­ã†nˆ¥–Zª7£ètÛþùçXd‘Eb¤‘FŠÿûß1ÄCtº,”€$  H@u'ðFf¨Øq‘)cí9&ŠiÇ)-v^ùä»8ç±wâÂ'ß?ÿŸ-1õØñ¯ŽŸÿ(Ö¼ð?­=yÆZ³Å¢S3ý¯6½;×Ä£E¿=—ˆ=[Äàñ÷¿9HÏóÎÓÖœ5>ù¡xô_Ö<2šQàýøãqî¹çÆM7Ýo½õVüùçŸ1í´ÓÆf›mm´QëBô¡‡ŠUVY%V^yå$´rÚ}÷Ýã±Ç‹'Ÿ|² Ïÿþ÷¿±Øb‹Åá‡;í´SͬKhF÷ÑGÅ©§ž÷Ýw_ðÿ®sÏ=wì¼óαð ÿmf]U@oxo¼qÜrË-i¼2n;“ú÷ïSN9e\rÉ%±êª«¶ñí·ßÆL3Í”úøÕW_¡‡º3Å·ûÌü^xašŸo¼ñF 7Üp1Ûl³ÅV[m}ûöípÎV[!¾›nºiüïÿ‹1dzÚÇÌ' H@@¨+7Éè#Ä=Û/Ó;r<ùîWñèÛ_Æ¿þs´ˆ³¾3ާ<ôVìzã ­ÍŸh´âóïŽ_fÙqå&sÇ,-Ö=Þß%|ðA¬¾úêi1ˆ°˜þùcÄGŒçŸ>î¾ûîØn»íâè£n} "dì±ÇŽa‡¶õg[l±E¼üòË ¼»âÅ?üË.»lÌ8ãŒñûï¿?î¹çâÊ+¯ŒW\±ÆR»'{ox ˆ©¦š*&šh¢Àچź3V66IæœsÎA=óõ×_ÇC£Ž:j·t_|qÌ;ï¼iƒà·ß~K›¯¼òJ›–Js¶ÚJ]tÑE±Ë.»(ðªf> H@@7¨7ôCÄv_æ˜ci<ù9î/¿ü2åÝ{ï½Ó\À[ºÑÁf›$Ü%qiä }³ùæ›'—JW1}öÙ©LÊš~úéÓ¯üñøç?ÿÏ<óLróä]‡vXŒ5ÖXé÷äÍ?Ã’W.u4g;š‹l •sÉDÐÞxãé•Í4&m [_ H@è]êFàݶõ±ü ãÆ¸ûÝ_þØv1Y®KJÞ˜#sO:zœÝ"¼HÛ^óßôß×?û>ÞùêÇVwÔ½¯Çy¿3H‘Ï3IºÂ mD[­oóù&‹ÏZ\F9Ãwÿ_Äcö‰íž"üò{Ìpä}Uµ«Öá×ouÖIî[¸”!:J¥﫯¾ŠgŸ}6-èIìê“psCPT+ðöØc$ ¨–,,gœqF <òH« Aà!ŠÆgœ$êXñî»ï&qÒ§OŸxê©§ªjGGíìîß#J'Ÿ|ò˜d’IRûªM»í¶[\pÁIHp¶+ m‡‚![gX`/·Ür霮xœ§„ .Ÿœ¡Ä²”"±¼Ã;Ä +¬ˆgúÌ3Ïœú•>@d—x¿üòKzæµ×^K‘ö 8Ÿ…E˜þl”ôÍ7ߤ¶tÐAÁ8cããÁL ‹‚øwÞ‰¥—^:¹Õ"˜w.\jÉÿÒK/%ÑWÆ5ãt¨¡†ŠÅ_<¡( çl,"qÊ s™¾¦KSGs¶£¹ˆË*cqG;qyä‘“è›}öÙ£™ÆD£Œ]ë) H@½—@ݼwY.¹6NÛb-Ëi¸¡‡ŒÑG¦Mï|úý/éߥ/gziß¿"ùUrÑ쨫‹AVjxDÑ\éœÇãö—?m}ÍsM—oÂU‹/cŽrÞyðßR«;?«f.–xÍ4&ê}ÌZ? H@êFàÝÑb©[vúÊgðŽh v²KГFxo´lüøÛÀ˜¹än¾®‚Ý!ð8ËÃyšJgð8›…e »"X„²°Ýpà Óu Xª°La=!©=‡õŽëò™¥®`Þ]e 80°žbM«t`ˆŠ,ð8§…Ð('ðöÚk¯déÄ2ˆ¸kOàÐK^x\€»(ÁUr?/±ÄIXO<ñÄ­JeL0ÁÉ•‘X.²xFwñü»åf‹Y¥r°h„¹‚¥9ke¥T‹À£ ,¹XTé[¸"ø>øà4ÞIX²¹+‘s0/Mm,±•–¹Ä•(l0O*Yðª‹å^3‰¿;¦|^€$ÐêFàm6ï¤qásÆ~·½Gß÷ú m¯V཰ÏRÑâÙ%×$ì¾ÄÔqüª3Ç\ÇÝÏ|ðçÏÚ‹¢YjÁcÄaâó£úÆÍ-n¡Å Ù»ªs»CàåsG,& àQšªxˆ‡tæ Þ'Ÿ|’¬YœY*Z)jÕ <¬¸šr‘sñö®bßåd—ËJ¡øKA?Y4)½¶×;,M§Aâ&Çy.Âõ–;‚ßàÖ—#Ò÷XŠŽ‚‹"3ø_i*ç¢ÉÙ=Îg>üp×»$wóJeâI8 WLœEeÃKÞé§ŸžÄVo~†Å³RºõÖ[Ó3ÕœÁ£Œ|Žaˆ{%}XÏd³Å¢^t³¬…s8{‰+/îÌåæl-s1o*”ºá6˘¨…­y%  H@ƒ‹@Ý<ªôÛc‰˜zì‘bÝ‹ÿ·¼øI&§®1kücÑ);´àý«åª„Ù[.FgßÛ£pÿy§®IXgö‰âêÍæI9Ïyì¯È›ã<\<°Ó"é2öb@–|MÂä‡Üï¶DíÌéÄÕf‰]Ÿ*Ö¸àɸñùAÏþýÝŽïG@¢ûa9`ñWz©v^ÄudÁ#ø!Ê!XJNÕDÑÌy¸vKFNˆ=\ÊÊYðà,§ýöÛ/-¸9s´òÊ+ÿ]Ô=ò<¬8 Gˆ{¬7Å+ËÆ@he ^ok .‚9!„9ƒUÈÙ ƒØ¥orÊ3‰Ä¹æšk¦#Œô{)²¡®XIÙ²T,/—•²8zj'^òÙgŸÅtÓM;í´Sºú£4ÁñÌk$gàÙô+sb“!ÄÉ›ùЉb™¥AVø÷rn’¾ À B›³°9aC´c-åŒ)cËq¾âŠ+’0¤_°ÒåÄîDœÅõ™vp>³Üœ­e.2 æƒ pNÍ0&:1Œ|D€$0XÔÀ£õÓ´ˆ»;¶]0¦»O<üVÿx¬åº-‹œyZ®?XfºqãÕO¿Žº/ªdeߥ§£Vš1.ë÷~<ôfÿøøÛŸâ®W?ë”À­%‚燇¯Ðb…ú3Î|ôé¿›Í7i<üvÿX»Eü• ¼ æš$]‰pÅÓïÇ-÷Þ-:õX)buÕóþ kÞÕ©;uÄ=WMvâ±ìàVÇœȚœ9Ê!öÉ[)È .z,Ž HA¸Œqf,/±¬•‡ <ÜÝX¬²xEÔpwcÁ T*ð<ù#ÜÒ}ôÑ–qÊy£FJ,Ö‰rˆÐ¦þX2YŒã ˆ‹*añ‹î`¬9œ¯C˜s^s[ˆDGœ…Âú‰)½&¡ôl"5òöœCãL&!÷q$ziiY`®·ÞzI`">qÉÃÒÈÙ?~ΕÄ ¹n¡Þ®­ldÀ|®¹æ¤º°áÜZŽÜÊF}\‡±†1ΗœY„!üéKîªãYþÍ]‚Är—"Œˆ‰••ó—¥. Xï(ƒò±rW80W°>r^“ºrww""ú±ÖÒÜÉ| ɳ¤rs– >ÕÎE®Ý`ŽÓÏDeܰÉÐ c¢ÞǬõ“€$  du%ð¨¢j‡–»ãÖžc¢˜|Œ>-Á†ˆ7[ÄÒuÏ}§>øV øu`ª{%7â0CÅ™kÏ}g¿ÅUsˆ8ó‘·ã€–ËÓ;sÑ9ïYrš±ãÄÕgI–Å·ûÿ®;xåÓïâåý–DàM3ÎH±Ë /Ä «ÍóM6fºï’§Þ‹#î~-~k‡Ý‘ºKàQW\ÑX"” ˆ-\¸‹4ÒH©I• X²,ìyÑ‚5 ¼J

?üðÃT—^3°8Õ{B "~©¥¬©{v[¤/rؘëÕwß}—ÄÁYU9±9‚e™{Z48VxGú¾A˜3®K›lxàz‹ÄòH"ú%}ê†CSw6CêDáÌwñQn¥9[í\¤ ÎiòÝ`ÜpnS6ú˜¨÷1ký$  H@™@Ý <»¦6Ý)ðj«‰¹›…®·Xl9Ÿ…Ï ñÀy4„:Â1m’€$  H@¨/ ¼úêšk£À«™t@QÜj‹÷ÝåG¸ä÷;,UXýL€$  H@õE@W_ýQsmx5#óàʹ3Î2E•@/ßÿ}Ä÷;Ü òR ê!T H@€$ ú  À«~èt-xFçƒíà ASˆüH´FÄu¸“³œ 4I@€$  Ô^ýõIM5RàÕ„ËÌ€$  H@hj ¼ï^^ƒw Õ—€$  H@@Pàu!ÌÁQ”opP÷€$  H@¨O ¼úì—ªk¥À«•%  H@€$Ðôx ÞÅ ¼ï@«/ H@€$ .$ ÀëB˜ƒ£(Þà î;%  H@€$PŸxõÙ/U×JW5*3J@€$  H é (ð¼‹x ÞV_€$  H@]H@×…0GQ ¼ÁAÝwJ@€$  H > (ðê³_ª®•¯jTf”€$  H@@ÓPà5x+ð¼­¾$  H@€º€¯ aŽ¢xƒƒºï”€$  H@@}PàÕg¿T]+^Õ¨Ì( H@€$ ¦' Àkð.Và5xZ} H@€$  t!^ÂE)ðuß) H@€$ ú$ À«Ï~©ºV ¼ªQ™Q€$  H@MO@×à]¬Àkð´ú€$  H@èB ¼.„98ŠRà ê¾S€$  H@õI@WŸýRu­xU£2£$  H@€šž€¯Á»X×àhõ%  H@€$Ð…x]sp¥ÀÔ}§$  H@€ê“@§^}6ÅZI@€$  H@Àì³Ï>„!øg94ÿýï£Üb”€$  H@€$ ÁK ’^Sà Þ~ñí€$  H@€j& À«™H@€$  H@¨O ¼úìk% H@€$  H f ¼š‘ù€$  H@€$ ú$ À«Ï~±V€$  H@€j& À«™H@€$  H@¨O ¼úìk% H@€$  H f ¼š‘ù€$  H@€$ ú$ À«Ï~±V€$  H@€j& À«™H@€$  H@¨O ¼úìk% H@€$  H f ¼š‘ù€$  H@€$ ú$ À«Ï~±V€$  H@€j& À«™H@€$  H@¨O ¼úìk% H@€$  H f ¼š‘ù€$  H@€$ ú$ À«Ï~±V€$  H@€j& À«™H@€$  H@¨O ¼úìk% H@€$  H f ¼š‘ù€$  H@€$ ú$ À«Ï~±V€$  H@€j& À«™H@€$  H@¨O ¼úìk% H@€$  H f ¼š‘ù€$  H@€$ ú$ À«Ï~±V€$  H@€j& À«™H@€$  H@¨O ¼úìk% H@€$  H f ¼š‘ù€$  H@€$ ú$ À«Ï~±V€$  H@€j& À«™H@€$  H@¨O ¼ûåúë¯ .¸ N9唘fšij|º¹²ñűñÆÇZk­›o¾ys5®¥5wß}wœsÎ9±ûî»ÇB -4ØÛãQF%N>ùäÁ^—Jøßÿþûì³O,µÔR±õÖ[Öz~üñDZÅ[Ä +¬ÿøÇ?k]|¹šÀVWý7.üÏ{ñÃq+Çðà Ùì͵}€êš@] ¼7Þx#vÞyç6‡n¸o¼ñbî¹çN‚oO&ÞÿÑ®·Í6ÛÄ,³Ì;ì°CUCzÓO?ýtðÿSÓO?}¬±Æ1묳¶–qÙe—Å•W^Ûm·]¬¼òÊ­?¯ô¾ŽÆÆ‘G?þxÜqÇUÕ³4SW¼?þø#6Ø`ƒøæ›oâC‰yç·Suyøá‡ã裎«¯¾:FuÔÖ2ž{î¹Øwß}“ Þÿý;UvW=Ô¿ÿØh£bµÕVëPlþòË/qçwÆ¿ÿýï@=ôÐ1ùä“ÇŠ+®‹,²HWU©ËÊ9øàƒãù矛o¾¹µÌ<þòFi¤gœqbþùçO"wŒ1ƨêý?üðCÜpà ñØcÅ'Ÿ|’æók½õÖ‹©¦šªª21S­ß‘Flcgê\-^gèúŒ$ î!ÐoÑEyæ™'þüóÏøî»ïâ•W^I‹‰&š(Î8ãŒf˜aº‡N™R;ZÄ÷XEêàEõ"ð, ,°@UïÙgŸ „ÖÏ?ÿœÆ‹øáÍ7ߌ<0•Eb¼}þùç1î¸ã¶¡]é}zx´zQ) IDATs¿ýö‹ &˜ ¦›nºØsÏ=;5’îºë®8õÔSxƸm´Ñzt^–kį¿þ«¬²J²2#N*¥o¿ý6:è `Ci†fˆ™fš)~ûí·øÏþ“ÄÞ’K.»í¶[ 9dýX%ÚxŒO„ßÊ×_=žxâ‰@ì!¸gžyævû›ö2>>û쳘m¶ÙbÚi§Móƒ2>ýôÓ8餓bê©§îÔ˜©÷‡jùŽÔ{[º²~ÕrQàu%uË’€$ð÷4„ÀÃÍjÍ5×lÓÒk¯½6.ºè¢ä†ì©tã7Æyç§‹æÿ_ȳx^{íµc³Í6ë©.ä=X×–]vÙ‹Öí·ß>úôé‡zhw9!æ°®Í7ß|1ÔPCµÛ–Jïë ‡µŒEvgî¸Qb¹c,_uÕU1ì°ÃÖ\\ž¥¼š êæønl¹å–±ÜrËU|ÂûÑGMã§oß¾­ù°vÂë¾ûî‹­¶Ú*V_}õn®mõÅ·'ðJÝÇßzë­´qÁÇå¸hq-¾Q‹++"1X´îò»‡z(‰Ý!†¢úŠ6PÎj¿# Ô¤.©jµ\²Àûñø•c¸¡ëg3¤K Xˆ$ #аé]vÙ% FN÷Þ{or[úðÓKÒÒK/ë®»në¢W¬{î¹'-VÞÿý`Ç®6‹»±Ç»M÷½÷Þ{qñÅÇ /¼5,þÇü¸üòËÛx÷ßwÜq©ÿú׿âºë®K.q3Î8c:Ï…uãšk®‰Ûo¿=Y æšk®ØqÇcÄGlóþ_|1YH^{íµTÜ7ÜpØtÒIÛäÃu—8ÚzÚi§ÅË/¿œ\[—Yf™”¯#&åÆ,R¶Xµ ÄïÁU+§lÁ[ýõcŠ)¦Hb¦c5VZ(—[Ãþ¶Ûn‹>ø ¹×b-à CN”s饗ƹçžOø`|õÕWi!º°°-cŹóÈ#B+ê$“L’ÚÕ1§/¿ü2qÀ-”1:Ùd“%·í…^¸5u†M%WTæø¶Ûn›žÁrUš˜Ÿˆ;Üq×ÍÞô _|ñ´ÑÄœa„Òx.×ôÕW\‘,j¸<2žžE±E™ô.¥gŸ}v<óÌ31üð纑·Ñœjx<“]jù~`‘)—˜›ÚM6Ù$}3«IÕ|£ò˜`Seä‘GN}†èsÌ1Ó·›Í™ÒoJ5yy&+Æ –X¼:ÖYgAÜjéGÜNɇ%’o0ã¼|ÏkùŽt4îªý~¿1Ì5æ:–qÚÁX¦Ÿð2È)ÏjòægªùÞçÜ-·Ü’81Fóµp) ¼£î}=.|²å<Þ¯¿ÇÂSŽç¬3[Œ7Êðm†Ô#o÷Ãî~=žzï«aØ¡b…Æ‹cVž1Æi¸Ö|Óq_¬1ë±é¼“ƶ×<¿óeœÝRÖf-ÿ&]ÔræïÔ‡ÞŽ7>ÊßtÞIbߥ§¡‡lÎMˆjæ¤y$  @ açe3{íµWZd‘Xñ ¡ƒ`ñ†˜à÷»îºkʃK'ЖXb‰$Z°êÜtÓMia€xÈ‹M݈1ÒJ+­”bœyyê©§ÒNx{AVòLáG}”þPR—x ¹=a1Að±˜{õÕWãÉ'ŸL‹æâ2™)§œ2¬ÀÝŒÀ×_ÿüç?Û,tYr6×UþËâ`ùå—O®…Õ0) ,¬yõƒ#ï†+._çŸ~«Î ·ÑG=->Y¨±€£î´ á† È ¾´ëì³ÏžZôϳ°äYRµ $ؾýöÛé, xÎK‘p³ÃJWLy“0a T“J^GïËÅwÑ:˜ßÅ{Y´E[­!C[Sˆ_„¼ÿôY{©_¿~É×JÜìb7êPLˆ{˜Ò/Œ#„ c·?\9»HpaäY~Ï‚™2Ë <Ḃ2§è#Äc•ÍRa•™#”è[æ2î´·Þzkzç"³ÈB 2gl0æ(ä%±‘€(Îÿ.å“ÇZñåø±Ù@¾bæí¢nÌÏöú"Ï'6xø}ÿý÷i>1Þù†E#í@@°éÄ<æ{C#T~9Õ*ð÷ˆÎ/WÿŒ Æ'm­ælsµß¨<&µ\Æ.m'ˆâùl_-ymŒ+¾­«®ºjšÛ¸`ÛŸó'‘OÆã ¶ŒÆãœoWµßÊëhÜUûý¢¬<ÞÙ\d1>ò·žqpÔQGµž ®%/eWû½Íå²Ù’­´ü=„S-\x—<õ^Ì3é1ôPCÄŠ-‚íÅ¿+žù0Vžiü¸iËÿ;ï{ýsÅ—> N1f¬?çDñÕ¿%¡6ƈÃD¿=oµ"ðfŸh´x¢EØñß™'%¶š²˜tŒc×_ˆÓþ_l2Ï$ID¾üéwqÆ#ÿ‹ æœ8Î[oöj>ñæ‘€$дBà!òâƒÐb¡‡ÕÁÁn#?ãø¦›nš29a%cAÃ"?¢ìŽ"Ⲙ ;ëìâ"28sBB8² (=sÂî3 ùj‹Ô½÷Þ»ÕŽ‹lvŽq…Êb ‚/ï–²ˆgÑ:ᄦ÷ç u'/uǺ•‹M‚!°ãË}NÕ2)ÝÄç´rŠI[xVR^Œ±`Ç=/¢YPa‘dÅn3ý“Ï~• Y,°FH°p"Õ²@‚ Ö†ÒrKÛD]Å÷çÇL‹ôœ`ÇF©À#O{ï+ rQî«AŸÿG]?þø6}øe§aS Sú~ž£™ $Ä?‹lÆ–¢Ìƒ±ÆxÑ„HÈV$æs«ÔE³œÀË 1X X‚EŒy\t³Î̱\ðó<¦²K(VB¬…XC¨v6aíÅRM]´å®»‡~x² ²ÙCbÐlÂ0Os*í ,ÔMæHq‰ò"yƒŠ2YÔã™-7ˆHÚ‰(¦®9Õ*ðx/šr‰z2þùÆu”jùFå1Õ–þËß9EïZòòͦßèÄsN¸Ü↜ÛA>¾ñ¥®þÅñ\íw¤šqWË÷+wïíþ?ÄA-å¼üÿYï|ó‹XòŒÇâÈgˆ}–þ¿hÖg=úNìxýóñÞ!ËÆD£ýµÉc’€$Ð 4„À+×1X€y±ÀyþP±`Ì‹Užc‡ËîWE×­\& v-Y ì±Çiw9/2Xx!>Š©#7¼ââR«@ŽÊÈÎ*–¹œXx½+ ,ÜvEëdΛËü>»«ekî¤Å€3eRÊ‹ÖêƒÛg¶†¶d%/ÈÙ!Çj™Û˜…vñpg§¶¸¢Ö²@ªv‚qÀ™,R9qž’³h9!VKTÅ~¬FàáNÇb¶4Ñ>,Gà•»&!/”Û;ˆn„0Ö&¢‚’²h/ /ÄBßöέÕ"ð* ‹{D)ª½¹ƒeáÃØc "|ØøaÓ†“âfD-rÞ‹€Ãšˆr)o”-Ò•"š–öEžË|øfËs¶ÜW*OæZæ”Ñ?82þÊŒaã±ZÍ5µ|£*}'Xf,m9ØO-yWXêùΖEÆgÞ|Àå¯ \¬+¬ö;R͸«åûUn)·÷iþ@36ù®×’·–ïm.·¸‘ëP-òW ²rÀ¯ÄÑ÷½o°tL1VŸäV¹e‹µïÚMçŽ%§§MßÍvÌý±ÒLãÅikþÍ÷Ó¯ã탖‰a çú6¹ü™¸üéâý!×g¸ÿs]~ÿë[DâqMKÙkÎö/µ|Ì+ H 4„ÀÃõwAþØœp i‚官X¯R*.X9³ÃYxáì–¬j„Ï»Ÿ„‡Æõ§˜þŽÀËÏ– ¼¼Xæ¬VIÎô °°”ž·Ãý·,F©[¥…a-LJ¹q†÷I\ÄØMÎŒ°Àd—¾ö^>ó“]¥Ø©'xC¹3¹ýYüÕ²@ªv’Cæ#ZŠWo`Ñ }$D>êï ¼JÖÝrQ4kuÑ,'ð°j#vŠâ»´?³Š`ù%–ZD!®s\™@ÊcדּÎJ.‚•RµaI†J×&`‘fÑÌ{I•±Y8¯¬ÀÂÎV/αbÅ-žYªæ-nXx*]#ÀYHúŽ ¥ìIPiÎå¾à›…Å,[ü+Õ¥¸‰T©Lú|Ûø&äÔǸgò)—òÆG5¼Z¾Qí}'ˆÜ|¥Fµy3¥ßæÒ6Ñ6Úù™j¿#äïhÜÕòýjO´áºÊQ6q3®%o-ßÛöÊ­…K%w̿ވýn%žß{‰˜iüQbï[_Šãï«âôÄzwÙFs¥ß#ðÆê3l<¾kÛ@jóøPô{ÿëŠeœ»îì±Å|mϪWó=0$ f!ЯèZ“ݺJ-r¸÷¤Ò9$„g°rá6ˆ5ó=¸N½óÎ;i9/àòY¥r‘ó·ÀË‹Mvõ±Æ*- «eR:˜9o…ˆä‹(¬Z,0XܲP¯Fàa‘d9÷–ÜHË <ÄÖKvØ9çUË©Úbë–µJg`бÑ^^c a¾\B!º+%Üöñí-Ü‹Ïv•ÀC°ýôÓOi^’jxäG@Ò.6#°Ž"ô°èU{uJñÜ$gfË¥ /¼0‰¢¢ l¥9WÚlDÁ–ïKÑõ-¿‡ŸqÞ©½yÜNXèí•,tˆ,|ÕœÁkoœ”~£ªm0¨6/Ö_Î$rö±Òý—|Û±Üq¶¬RÜÕ~GrþöÆ]-߯öÄU Åß+6vjÉ[Ë÷¶§Þ^·¼'<ðV\ºáœ1á¨m¯Àw‚QGˆiÆù+èV%7ï ÆÓ|ÿÞaÐQ<7Ãx£Ä8#—·È7ËâÍvH@h@à “$  ´%ÐpêçÅEѲGD?h¥âüXoX˜aáêU ÂïóèlÁ#?9øãF0“|}þbÙbWM•Ò3xÕºhò^ÎûXEY±D±Ä¥s'Ô+Ÿ'©´0¬†I¹ ÁÎ8V;êŸW5°+\ÎEKbŽ8šiÔkB€”ÏP•Z™²¸.ö[vïÌ.³gl`ÅjÏ‚G¾JïëŒÀËnXÅ+!ˆšˆË¢¢ôš0Â; ò 9Ãȶœ•(ó,)’çȹ¿±d0öˆ¶ˆ¥§xõã²4Òi©»q¹z¶š—Zݳ¥ƒà=‹-¶Xêºjõ¦®X–sâœ(¶œku{ã" ÿR‹=gÄøgtŒÑœ˜sô–Ì|`¹¾ Ž\ÆeÏçïR±ÌrþïZð°lbcÌàö] 0Uä‚àfΈ¾*^-%Œ ‚™L3Í4éÛXí7ªÑVKÞl-ŽŸÜž"Û|ΗÙâ• ´—x¾£íÍë"£jÆ]-߯<ÞóÙÒü.¼(Ê÷&oÊÔ’·–ïm{¯Z.Ô»Z×À¯-AVîŽéÆ9Úi‘èÓrEBNÿø3†*\qPIàÝñò§±òyO¦ˆš\›PLÅ2Olnà­SüV´û‡Â_J@h )ðø£ÃB*¸OåHÙ¥…  ¸’X,sþ QÀ†Å‹ÜõX¼"ÖXа“_vÝåXæX€üƒ+°ÒõG a#ˆeaB—K,œŽ9æ˜$žˆrJû8Ixx,·X)¤J ŸJïëŒÀË 1‚„à‡±Š‹tJ¯T Ÿ9ãÅY1ÎnÁ‹:bý, £_l;Ö1&:Ö¶r®‹XŸxâ‰dý¦Ìluâ¬+cŸg¨ !аaL ê‡,žè#òVº&¼Ü‘XzMgk09U+ð°:rf“ "ßÒŸüŒ ¬Ç„̯61Þ8ÓF›¸êïc ¾ œËeLtМÝDÓÙ…®\_ðÝÀâN#0È‹å: ~þ~u•áEû—œ]äG¿"òŠÑ&Ëñ¡^Ìú‘yļæ{É8åwú»–oT-¢­–¼¸i2®p±gî°IÁw«4ãK$‰ù¸cüò-ená.ÎF!ÖÙìê^Íw¤šqWË÷+wæ³ø^2®ø†2Ï¥Q}«ÉK»«ýÞ¶'ðjù¾V+ð(ó²~ÄæW>S5RlÞrV+Q4o|þãxºåš„±FúË*WIàñ»ü¾%¦;Ö˜m‚–qñØÿ¾Œ¿ý)üÇ_wafKfé&dµßóI@hT )ð€Í]\,XX|±ëœáâ±z°˜D ð‡¡ÂÙ»ÙX@ˆªÈP\„¨&¢1Q /b‚E1e‘—ÝTH,rº[àñ~D ï§ÎüQgÁ…ð)½c­=G91)¼ˆ,3,ê±Np6 k)B—¨R‡eˆ…- Ï"…Èuùʉbùä¡òE縭!ºK­ LvÞY”qn.‡ŠÇ2X*ðX¬am‚å§Üåݹ”‰5‚E+ JÚˆf,`IÊ ùJ ŸJïëŒÀ£N,9ƒÈU,>kˆ ¬Ÿ¥<ê‡(Å]zp~ 7Çj¿´/YÜ"|ᥣ\"ê ¢·ˆA«Vo6TX<3ŠA8—ÆÙ7ÕÌ3¬ÛÔr³Ç=—9…5aDP˜ÕW_½ìEç¥ÖÆÒ +XÓYsÖažçz¥q×ÑHyô9ãƒë ˜gl0”‹¾›/GÔ2GY”·×D,…í@<²Á· ùꉮx¹­lJá}ÀüE—³î–ã‚0äž>6¸¸×2§GÅ‹æ«ýFÕ"ÚjÉËûÙ´ƒ?rløáÝÀ&u-Zè™\—€ØÅUŸ;øóíÏn·Õ|GªwÕ~¿ò7+LË=z¤ö¿ç2õs{'^ùôûdù›o²1b×ŦŒe§ÿkƒ·c6|ø›P´Þvô=ð÷€@] ¼F‡ký% æ$Àâkqñnºæl©­êNY\åûÛ{W-y»³Î–- H@õO@Wÿ}d % :#БռΪkuê”@GÖ³bµkÉÛ“Í}îÃoc÷›_ìÉWVõ®V9f›hÔªòšI@³Pà5[Ú H Û (ðºq¯xA-¢­–¼= ~‹ç>ü¦'_YÕ»f›h´m„aªÊk& H@ÍF@×l=j{$ n' ÀëvĽâµˆ¶Zòö x6R€*Pà98$  H@€$  4 ^“t¤Í€$  H@€$ Às H@€$  H@h ¼&éH›! H@€$  H@ç€$  H@€$Ð$xMÒ‘6C€$  H@€Ï1  H@€$  H I(ðš¤#m†$  H@€$ žc@€$  H@@“Pà5IGÚ H@€$  H@ <Ç€$  H@€$ &! Àk’Ž´€$  H@€xŽ H@€$  H@MB@×$i3$  H@€$  (ð€$  H@€š„€¯I:ÒfH@€$  H@Pà9$  H@€$  4 ^“t¤Í€$  H@€$ Às H@€$  H@h ¼&éH›! H@€$  H@ç€$  H@€$Ð$xMÒ‘6C€$  H@€Ï1  H@€$  H I(ðš¤#m†$  H@€$ žc@€$  H@@“Pà5IGÚ H@€$  H@ <Ç€$  H@€$ &! Àk’Ž´€$  H@€xŽ H@€$  H@MB@×$i3$  H@€$  4„Àëß¿Üzë­ñä“OÆçŸÃ;lL<ñıÄKIJË.C=´=) H@€$  H ×¨{÷ÒK/Åá‡ß}÷]ÙΚa†âàƒŽQF¥);ó’K.‰5ÖX#Fi¤¦l_±Qø¡†*æž{î¦o« ”€$  H@@w¨k÷Í7ßĶÛnß~ûmjû¸ãŽóÎ;oüøãñÈ#Ä/¿ü’~¾À ÄØ|º½Ì?þø#†rȲïyá…â‚ .ˆSN9¥ÛëÑÞ ~úé§Øf›mbýõ×å–[®Kë²ûî»ÇÔSOúùÍ7ߌãŽ;.Î>ûìŠLºôå& H@€$ &#P×ëÕÕW_O7ÝtñÏþ3†n¸ôïwß}7vÙe—V‘wê©§&¡pÌ1Çă>˜òyä‘1Çs¤ÿ¿ñÆÇ_|‘„à 7ÜÃ?|üðñæšk¦ß/´ÐB±ÿþûÇõ×_ŸDéÊ+¯Œo¼1•‡Øœh¢‰b»í¶‹Yf™¥Í0xöÙgãºë®‹7Þx#Y °*n´ÑF1å”S¶æÃÊøÔSOÅL3Íûí·_œuÖYéßcŽ9fëûJÇÖ¡‡‹/¾x,²È"ƒuØ!BO>ùäXzé¥cæ™gîÒºï½÷Þ±úê«'!o’€$  H@€j#P×o«­¶Š?ü0µq7묳¶iÝgœ·ß~{úÙZk­›o¾yÜu×]Ø#mºé¦±Î:ëÄ—_~n¸aë³GuTÌ>ûìñÜsÏžûî›~Ž…jÕUWm#ðúôé“D`1á zÙe—¥s€¤›o¾9Î9çœA¨#DO<ñĘbŠ)Òï²À}ôÑc´ÑF‹wÞy'ý|É%—Œ=öØcç±ZÒž+®¸"‰ÑréÏ?ÿŒ!†¢¶¯³Ü´}ª©¦J<ý‰ål’€$  H@€j#P·ñ²ÒJ+ÅÀS‹n»í¶A‚©<ôÐCIø‘^xá$ „CÒ‚ .p@<üðÃqôÑG·’Ù`ƒ ’àÃêvá…¦Ÿg `Ñ‚7Ì0ÃÄ&›l“M6Yœyæ™ññǧ¼¸ƒâŠq‡v,\X÷°Ú‘|_}õUÌ3Ï<Ž”^®ÄòË/Ÿ¬‹L0A«,vÝ£>šË{ì±­?æ,VFD%VÃN8!¦vÚôÿ/¾øâTÊ£ÎóÏ?zŽ´ó–[nI,ù9ÿqÄ[E–P¬’ÊœÖ^{íôïì’É»aXñÞ~ûíØk¯½S,{Ÿ}öYzÖQêŒÕó믿NV×wÜ1Ä)­Ëï¿ÿžêBÉ—Þ|ï5×\SÄáÿ—€$  H@€ª P·svˆ Ö0DMiêׯ_tÐAéÇX÷²ØCÀ!°ÆgœÀÍÁÅóÓL3Mr£Ìy±äq–o„FH–;JQàqæ,‹¶K/½4®ºêªô®lí㬉tÒI'%¡B"ù‰îÉ{qÛ, ¼¾}û&aØ^ºè¢‹’ûi>ä…VÅ-·Ü2æ›o¾@€„K¾9çœ3^~ùåtfëæ„×׬ IDATN˜¬7ÝtSì´ÓNIHÂŒŸ!ÜrÙx7\b)wÒI'Mu¡ÝwÞygìºë®é݈òÇ<Î?ÿüÄ¶šº –qѤÏ8si’€$  H@€ª'P·&¬¼òÊñÛo¿¥Ö ¤²[dnÞý÷ߟ‚r8§–Ý-±,aµ#q†qõúë¯Â—G#çð°ô}òÉ'IqÄ)Qàqno½õÖ«øs¬iXÏÚK¼oŒ1Æh#ð°Êut– ÷N¬‚XÒrBàq-ÄÖ[oÝú³=÷Ü3 7ÎætÈ!‡ÄôÓO«¬²J¬»îºÉU÷ÓœJϽuVàÁ¡IB#ˆ±¾qž‘„XÃu–(¨Ô±šºðÜf›mûì³O²Nš$  H@€$ ê ÔµÀÃÂôÞ{ï¥Ö æšk®6-Ã=ðž{îI?Cˆ!ÈHœã‚E"Ð ‚¡ˆ% ë …ÐÁ…—FÄ©«åau+Zð¥[i/a]D⦚SÑM’ŸQ÷VX!ýzä‘GnÍ÷óÏ?ÇŠ+®˜î ÄÂvî¹ç¶ºI’©«n¡ÙÊöÊ+¯¤rKë2`À€da{챫ª ÏcݤŸ8'i’€$  H@€ª'P×ë×å—_žZC „Q8òÖ[oÅn»íÖjáãŒÜä“Ožò¾ÿþûÉ’„;$‚Û$Ö,ÎŒ­¶ÚjÉu‘T´¨Õ"ðˆ„É™3ÖÃö¢]Ö*ð8_ÇÙµR ^>Ç;x´‹×¢‹.ڦוX'q¥Ä•7ÊœJ.•ˆÉZÎàQnQàáŠõÑŲ˜*CŸTSž£DÓÔ‚WýD6§$  H@€ P×ïûï¿O®‡DÁ$a-"¸ V!\0ó=xˆ\úŠ ‹‘(Çk¬èß¿kÀ•쾉àãÚÎŽ!ê²ûg-{Û°‘Z‚ã\õ{æ™gÒ7Î÷‘jx\Õ@0”¢;f©róy·r‘8q›ÄE’:âÚ™Ï žò<\Má«&‰ú„†ßW ²R*ðˆ6Š•Ÿ/³Ì2ƒÌ®jë‚h¥X`Ç|g©$  H@€$Pºx´ƒ³s¸Xr]¹DôG¢ZŽ4ÒHm~¨äæ+ˆðxÞyçµæqÆãøãoýw-‡Š‘8Kë‡ÈÄý“T«ÀCÀråC1úg9÷ôÓO§@3œ[j©¥’è%'ÂÑŠ+*UyœéÚ‰Õ‘óyYàÐ…w“À0XNqm/Šf©À£Xô(ûí·OA4ý‡µ”T®.äÇ 5×…H¥Ô®~D óЬ€$  H@èu/ðh%1 ²òÄOÄ矞„ ED©,Mˆ\(sÊ—žgWÂüs¬n¸8æT«Àã9­-óµ×^‹Ÿ~ú)Y 4‚°uÔQ;%ðh3A`ˆL™­‹å…sa:—²s·ïã¬"mâ\–³ÓO?=±£‚­<ÿüóéê‡,ª°t⦠¬f;ï¼sk‹-¶XÕ<êõ ·×;î¸#‰»ñÆ/ÌçËՅ눰™ë™Ê'Ÿ|2 b“$  H@€$P†xµ5©yrc™ä¾<ÜR»2•^.Þ•eÿݲpÅ-”{ M€$  H@@mxµñêÑܸ_^{íµm.;ïŠ ”YéŠ2»¢ .='Z*wàá*j’€$  H@€j# À«Wç&Ø×?¯Aø»•¨WÇ¥ó¸k.¸à‚·‰>/ H@€$ ^I@×+»ÝFK@€$  H@ÍH@׌½j›$  H@€$ ^I@×+»ÝFK@€$  H@ÍH@׌½j›$  H@€$ ^I@×+»ÝFK@€$  H@ÍH@׌½j›$  H@€$ ^I@×+»ÝFK@€$  H@ÍH@׌½j›$  H@€$ ^I@×+»ÝFK@€$  H@ÍH@׌½j›$  H@€$ ^I nÞ€â‘G‰wß}·WvŒ–€$ æ 0Ùd“Å"‹,}úôiŽÙ H@¨ku+ðn¹å–˜d’IbÎ9ç¬k€VN€$ЧŸ~:>ùä“XqÅ% H@èvu+ðÎ:ë¬Øa‡º€/€$  t'ßÿ=Î9çœØn»íºó5–- H@Hx H@@78ãŒ3xÝÌØâ%  Hà/ SN9e|ðÁqûí·ÇK/½<ð@D³Ì2Kzt®¹æŠ 'œ°Uàm´ÑFqë­·ÆH#SO=u,½ôÒI " x?¢nÌ1ÇŒ·ß~;€LžSO=µØ¬EàýñÇÉmô­·ÞŠVX!ÆüÔ¾|06Ùd“XmµÕÚ¼E]4ž|òÉT"uÂQ¸þúë§ü& H@è9 ¼žcí›$  ôvM!ð¾ÿþû$`°°ekÝ/¿ü[l±E|óÍ7qà 7$ ןþ™߸ãŽgŸ}vkßÿþûïÁÿf¤»îº+N>ùä².šÙ‚‡E ëÙÎ;ïÜF´Ý{ï½IbÌéðÃO"òØcM¢2§ZÞ›o¾™,„«¬²J‘9 0 ‰Ìœ²oä‘GN‚uæ™gN¿Âª¹é¦›Æ(£ŒW]uUo÷¶_@Pàõ(n_& H Wh W©;î¸ø×¿þ§vZL3Í4Ià­ºêªÉUò¬³ÎŠ>}ú”}´‡µî¢‹.J®¥{î¹'N<ñÄØrË-c­µÖê”À{ýõ×c§vŠ…^8öÛo¿6¢²øþ,ðvÛm·XvÙeÛT îšX³KgGu÷÷€$ð÷ (ðþ>CK€$ ê4•ÀÃÝ·Ä×^{-¸êÆŸ~ú)Ž:ꨘsÎ9¬yçž{n:¿¶ÒJ+%Äy½bªFàñ"ª\úñÇ“EWÏwÞy'ÕKâ:묛o¾yE‡Õñ»ï¾kS$çåòÙ=,r´wO¬¸a– µö.:?è ƒÒÙÀ+®¸"µß$ H@=C@×3œ}‹$  D:6û쳂bˆ–`$–T鮆‰…m‡v¨ªØ~ø!øãyÿý÷'×HܧšjªÀê…Ð:âˆ#Ò™¸œˆ’yíµ×¦Æ=ôбøâ‹ÇV[mÕ*¤:+ð°^}õÕqÍ5פ@-ÓO?}L7Ýtñ믿Æ-·Ü’‚¡à6šS©‹&çéŽ>úè6mF˜"PIœÃ£n”…€Å “³wë®»n 5ÔP)O{ïC‰'žx".¿üòt^Ð$ H@=C@×3œ}‹$  4‰ÀË–-.'G4e·I"H"äJ^îø?ü0GÃsòÉ'3Ï<3¹>vV༅ .\O°í¶Û¶ FD&Q-;xA!(K16ÚhÉbWšžþù¸ð “µK$çóxNi H@õI@WŸýb­$  4#†·àa½#p ÁKbRL ¼œ—óqœ“#Êå´ÓN÷Ýw_üñƒœ™#²RÎE“à'ï¿ÿ~ÜtÓMmÎæU+ðj`Xô8ׇk*çêxµ4¿$ ž! ÀëξE€šÀ‚GÍ5×\3]sÀÐ!†"õ+çÞ=ôÐôßlÁûùçŸÓr‹,²H›¾GþûßÿNgó¸ÛëØ^{í•®>Øc=ÚämOàaµÃuM¢U’ˆry 'Äã?>ˆ÷JÎ×]|ñÅŽE„#gûpù̉;ý8ÓÇ9C,• ¼1šAÀ`! À,Ø}©$ ^I á-xôb QÆÙ»f˜!]0Ž#ræ /¼Ð*ð²e18ï¼ó¦(šÜ+ÇwÜswä‘G¦AÀù9î¹#0 "7IÊæBôögÛ¸ˆ|‚ &ˆ\0‰»Ç{,äÜ_©‹ævÛm—áf›mÖ&ºf¹‘ˆE‘ús–pÆgL®¤\á‚wD\x½rÛh H (𠓬¢$ &!ÐïÛo¿M÷Ú!xHˆ1DÓ×_ûî»o›3x-‚” Œ°ˆq9÷çm°Ám"R" 9“‡å +¿ïÛ·o»—IÞÝwßX§˜bŠQƳ¥ˆ–\åÀ™ÁŽî¦ÃZ‡)"Èœü{¢‰&Jçï¸ø<'ƒ¬4ÉÌ´@SPà5UwÚ H@uM )^]¶r€$Ðë (ðzý€$ # Àë1Ô¾H€z+^oíyÛ- H ç (ðzž¹o”€$ ^F@×Ë:ÜæJ@Œxƒ¾¯–€$ ÞA@×;úÙVJ@¨ ¼zèë  H@MM@×ÔÝkã$  Ô^]u‡•‘€$ f$ ÀkÆ^µM€ê“€¯>ûÅZI@@Pà5QgÚ H@uN@Wçdõ$  H ñ (ð¿m$ F! Àk”ž²ž€$аx ÛuV\@ÃPà5\—Ya H@h4 ¼Fë1ë+ H q (ð·ï¬¹$  4^ƒt”Õ”€$ÐxMЉ6A€ꛀ¯¾ûÇÚI@h& ¼fêMÛ" H@uI@W—Ýb¥$  4%^Sv«’€$ z" À«§Þ°.€š›€¯¹û×ÖI@@PàÕA'X H@½„€¯—t´Í”€$ ÁC`àÀqöÙgÇvÛm7x*à[%  H W¨[wë­·Æ$“LsÌ1G¯ê+ H@ÍE _¿~ñÉ'ŸDß¾}›«a¶F€ê’@Ý ¼ÄC=ï¿ÿ~]‚³R€$ jL>ùä±ð GŸ>}ªÉn H@Àß"P·ïoµÊ‡%  H@€$  ôB ¼^Øé6Y€$  H@hN ¼æìW[% H@€$  ôB ¼^Øé6Y€$  H@hN ¼æìW[% H@€$  ôB ¼^Øé6Y€$  H@hN ¼æìW[% H@€$  ôB ¼^Øé6Y€$  H@hN ¼æìW[% H@€$  ôB ¼^Øé6Y€$  H@hN ¼æìW[% H@€$  ôB ¼^Øé6Y€$  H@hNu+ðÎ;XñÆ+R¿øâ‹cÀ€±ãŽ;ÿÜqÇí²zíµ×âê«¯Ž—^z)~ÿý÷˜jª©bà 7ŒÙf›-½ãú믻ï¾;Î>ûìzè¡ã?þˆW\1.½ôÒ{ì±Sê¶ÖZkÅ‘GsÌ1G›ºmµÕV±ÄKÄzë­Wu×XcØvÛmc饗®ú™zÌøâ‹/ÆÑGGqDL1ūЗ\rI|õÕW±ë®»¶–SÚ7ë>œ<ðÀqË-·Ä;ï¼à 3LÌ>ûì±ùæ›Çøãï|hð1òì³ÏÆé§Ÿ^xakKnºé¦¸ù曃ùÕÙT®ÜΖÕÓÏ•ûžS‡fù÷4Oß' H@=O nÞwß}—Dé‘G‰«®º*Î<óÌVBºwß}·ËÞã?GuT,¸à‚±ì²ËÆH#@b‘{Úi§¥îSO=ýúõ‹vØ!Õçû￵×^[WÅøýøããòË/-·Ü2ÆcŒ*ž¨œå¬³ÎŠŸþ¹À+훿õŽ .¸ -öÙ¬˜sÎ9ãÏ?ÿŒûî»/~ûí·Øk¯½!çCã”G}4‰»®xåÊmJå¾ç ¼Fé=ë) H@¨[Wìžl-c¡YLo¿ýv— ¼Ÿ~ú)6Ùd“Xl±Åbûí·oó.¶C 1DÙQóé§ŸÆf›mÖío»í¶‹¥–ZÊ‘ûÿ üñ1ÔPCµxÂé:/¿ürì±Ç±ß~ûÅ /ì|¨m{ߊ*‹è±l÷Þ{oòV(xXlñŒèl*WngËêéçÊ}ϳÀóÜÓ½áû$  H 3šBàxâ‰qå•Wîã7^ì¾ûî1õÔS·òÀ]ˆÅ ¿ &˜ ‰¸ùçŸ^X&N=õÔ´àéÓ§OEžE¦ûï¿?Ž;î¸6y©ÏÄO\µ‹æ.»ì³Ì2Kr{Ë ‹ ÿ^n¹åÒpÚ`ƒ âÍ7ߌ'žx"FyäXsÍ5c•UV)[OÄêE]?üp²¶à&Š‹ç˜cŽ™ò#ޱÀà.yòÉ'ÇgŸ}×]w]üøãÉe « eä4óÌ3DZÇ›Þ}Ûm·Å믿ž/.£¸œ9ä­õÜwß}ãÁŒ'Ÿ|2†~øØtÓM[…i©(ÇÍ•Åd1a)Â…óÿû_r‡}î¹çâ‡~ˆYg5õí¨£ŽšØ|òÉ'­ågʹ—Q>ÿûòË/Ó¸àÙf˜¡õYضWçRÀå‡÷]wÝ•,]|ðAŒ2Ê(±ÑFµº×ÂáO¬Ò¸>N:餩LHú 6n¼,(óÆÂ/¿ü’,j°8p`,°Àé÷#Ž8bÙ1Ð^»+õîG <á„Rýí¥zŸ¿þúkš'‡vXÌ=÷Ü©)| `O¾ ô}ÞŒ5¬ÌÙ庽oíá…’ÕŸ9‡K÷¡‡šæl{ãjp““N:)b9ápë­·í¹ýöÛc§vŠsÎ9'°ºÏ4ÓLé{ÁX&µ77+•[:†(÷ÜsÏçŸ>Fa„ô`žPW^y%Íu„gvfÓïSž|ŸøþóŸÿ ¾OÕðn¯©_¥ïùôÓOŸÊßzë­ã­·ÞJù¨sñÛÆóˆC< hÞ+­´Rò9ØîÄò—€$ Nh ‡˜ba€¸;ãŒ3’U‡Å) ÑwÀ$qƒÀ*qÊ)§¤|N8al¸`òÇ›ßW» å¼e²øa±8ÖXc¥ ‚¤Ú3xÕ <ÊÝb‹-bÆgLŒÅ×A4ˆXÅ‚ÀÂ’Ån¤ˆ,Î"$o‚,´<,䔿Ï>û¤… AqÈ!‡¤3W,¾&šh¢$JXü²àyÿý÷ÓâjÏ=÷LVO‹ žßm·ÝÒ™EÎQâ’ÉÿXð” <\,y ñzðÁ§wÐFÜri ýÆ Ñ7×\s¥ú²8¦n¸yþãÿH“ó¥÷²håœÞd“Mÿþ÷¿ãÚk¯ ¡SN9eUu. µ‘sŒ%˜Áõ_ÿúWm,þÇgœVˆz±ð⿈:8b ¦¯`ˆP@”ÀqL»ËŒqúÁ㥩£vWêÿb9,fçwÞ4æy>T#ðèD9cl¸á†KBo¡…ŠÑG½Ãocîšk®IÂŽ>có…¹Tïã„Mæ"±Ä£Þ´5í´Ó¦ï*cŽy¾Â +$ACjonV*·8†¾þúëä%ÁÆÓºë®ß~ûmÚ\cŽb1¦ „Ñ6ÛlË,³LrƒgòsD.úO?ýtê/ú®Þíõq®[¥ï9lèO¾1Ô‹o""¡—¿m´6±ñBÞ/¾ø"mþm¼ñÆz^trâc€$Ð9M!ð-y±Î\,,êIˆ‚y°[Ÿ²Î:ë´¡†5‹sF!a!*>·ÿþû§Åv©ˆxõÕWÓ®\¥»·,"ø£Ÿƒ¬T+ðXps.0'Bˆƒ<°M;èT~Çî8â—„"P ï]yå•[E‚j¾ùæk}~µÕVKbh‘EI?Ú‰HÎLJ‡L°rÐ ,zÙòøÍ7ߤvbÕ„y%·Z‚Ù Ü=˜r‰º`CH“<œb•bß p×_ý´H\~ùå[‹¤Îˆ\þ[MKëÒQKóÓß«®ºjªçâ‹/ÞÊ [Èøã¬iÑU‹.ºh⇓ú²˜DtÄqÚ\´¼UÓîÜ¥ý_¬;ì°|åy‚Pe®åD°yPïó¡Ç\!¨Cˆ…bêèBûé?‚’ä¾é̸êéqBx¸À—ºh"X/»ì²Vlˆ`GPU37Ë•[|Žo%ã‰÷"šHÙj‡ ¡‡ÅO 60{ì±$¨x>6O`Ž%ñðïj·×Çź•ûžçþäÊ7’„€ã{š¿m´ ¯6ó7ÿ†n¬¿•¾ŸeaúC H@Àß$ЯE“à'Xµx,Øu&±«žb‡¨—,ü‹‰?̸²“LBt|þùçéÿï¼óÎi1E£–-ÏM7ÝtmÞƒÀD¨Õ*ðJ£h²`añ€›c1±S{b ˜D£6ZjG%¡…õ ‘‹%…1Ï`±C´‘p‹BdÁ™Å;ëD%©4Ò\^\³ÃúVé½ÔëmÁ‚BbÁËÂŽ€ o¼ñF ‡vØd-!u$ðèK„gQHñ‹O‹,Ȫ©sˆU´‘üŒθj}ôÑGÉ:Çf.[åP'¸=6'¶ˆb\Sˆâ8ƪG_áÆZt)®¦ÝÕœ_e¼Ñg¼Ÿ„p¤hs ë, õ>ªxýû÷Oaú‰Í¾¸ø£oH¥¨“Í…Á=Nò\('ðJ£h"ÄØèAèU37;xŒ,ú9Peš "##âpgŒ!ä}lÊ ð°þ17ø®ö¸iV3+õqéünOà¿Á¥ß66`øç(ÖO\€ùÛb’€$  ô¦xvâûöí›\Þ°„ âÒ³K÷ÜsO²P J‡[ ŠZ^5×$`Ùá IGgðJnU¸‡fAšÛ‡ÀCà²_LXú8CƒE¢Ò« ]„ .Pðãß,~¬XDYˆ!\±–²ˆ)ZÃÊ-‚òù§rïE(R.ÖÄdN<Ù„0=š,IÝ-ðŠg¶Š+-Üs~úÖl"pŽ7MvúCEëiqc¢’Àƒ1â—8Æ4®Ÿ¥)ŸË?ï*‡ØçœTé¼|~­3opÌÆ'ÜÛ;ƒ—E n6Æ$ù§™fš¿!µ ¼z'µ <Îæ±ÐÑÜìŒÀ£>|Wp+Gà}øá‡iìóÃÒÌ÷‘M7Äß>æ!õÁ5¶=Wìw6JûWÔbªUàåòÙä¢]ôú \¾l˜$  H@=E ©XOœµÃjÕQÂ…˜!pH©u¯=‡(½°¸`¯å¼R7GžÅ¡T ²RNˆ/ áÝÅÄ.2eb â܉sn”É™šJ"#‹¦%—\2¹–&0 ±‘]9昴èê¬ÀcGž>â, gérBXpæ†ï|_g¤ØÍÏ!Ênyñ Z©‹&ân¥.šXžˆÕ. ‹,:x#x]g ƒ«cg‹R¬çŸ~²´—°´uÔîj÷#° IDAT,xX±äÒÎåÔ‘À«·ù@½áž]dùwž#¥ÖÝÜFÆ›" âºô êÛQ»«x”C[›ˆSKО;î¸#ESå|ã£Þçí ÿó¦gk©;ü²À#°s›3 HBLsÑÑ7¤³¯Æ ®ÖÌg¬çÌ6Ã:xéhn–+·8DD@(ñíÄõO¬Ýù[B~\B 8Ä7![qáÄ;Ëuv ¯F€µ×Çź•ûžóÍìhÞsF‘6á¾gÏ<óLŠ@Ê™B“$  H §4½À$'¹F…®2œ+B(ÏJsÖ€« Z„ sÜíHå@¸DòÜ5,kÕFÑdÁÄn>®}ˆ7Ü^DT,ZðXذØ~íµ×RºB´Ér +‹W„E¾&‘Ýù*-ð ÁŸÝ.$˜ñ.ꃉ³rˆ?Ü_ù/ åÎ <¸R¿b"pJ>“‡{®¡„ gÁMTº,ðX$"89ŸÇY5"ý•ö ‹z~Fÿw.úžEXN-ÜJùv”F°,`ðÆjаd1ÛYGèS,ô),†âEëZ®kGí®VàQg9EÔTú›+‘aÞ(ó¹ŒX@Ü!â|ô VnÄ2}Åœƒ nvDÐÄ”7Úû†tVàÕÃ8aŽÃ…ÍÎÄ1÷zè¡´éR´ê-xô9߀öæf¹r‰x[L¸`rŽË bˆï+WØ7ÞØpcŽ3çòYP,dÔÍ+¾AÕÎãŽú¸X·Òïù<óÌÓ¡ÀãyÚÄ7’¿!lÖ1GhS¶H–ýXûC H@@h×Åm¶¸ B€ˆd§›Ý|B‚EçR°þ™$  H@€$ ú% À«ß¾éñša‰#ê$.¸ã‘°z²ÃÕ#ï ÷xÅ|¡$  H@€$P^U˜zO¦;ï¼3ø¡Èq7Åňv„÷/½Ï¯÷P±¥€$  H@h  ¼Æè'k) H@€$  H C ¼™A€$  H@@cPà5F?YK H@€$  H@PàuˆÈ €$  H@€ƒ€¯1úÉZJ@€$  H@耯CDf€$  H@€$ÐxÑOÖR€$  H@@‡x"2ƒ$  H@€$ Æ  À«±Ÿ^|ñÅ8úè£ãˆ#Žˆ)¦˜"=½ûî»ÇŠ+®K,±D¥™]€$  H@@×PàÕÈòã?ŽË/¿<¶ÜrËcŒ1ÒÓÛm·]¬µÖZ ¼Yš]€$  H@èZ %ðþüóÏbˆ!º–@”¶ñÆǦ›nú·^½¶­ ðX„$  H@€$ÐCêZàvØa1à 3ÄW_}wÞygl¸á†±æškƳÏ>_|q¼ûî»1ÁÄ&›lóÏ?BvÓM7ÅþóŸ˜cŽ9âž{î‰o¾ù&¦vÚØyçcÜqÇmÅúÌ3ÏÄ¥—^ï¼óNú9¸e–Y¦õ÷·Þzk\ýõ1`À€˜jª©7ÓL3ÅÛo¿;î¸czÿC™~^L›m¶Y¬½öÚñÓO?ÅE]?üpüöÛo©>Ûn»mŒ9昭õ|á…bÁLùxÇ¢‹.gœqF\yå•1ÜpÃ¥|üñGj÷V[m‹/¾x _# H@€$  4"ºx¯¾új,µÔR±ÁÄÐCüû€HbiÎ9猗_~9N9å”$Œ&œpÂ$ðÎ;ï¼”1ˆeìøãÏ>û,N;í´ÔGÏ=÷\ì¿ÿþ±Å[Ä ,o¼ñFœtÒIéß}ûö×_=vÛm·8ꨣbòÉ'ÎÝ:ꨃ<„᯿þšÞƒè[l±ÅR±2î»ï¾ñã?Æ;ìÃ?|«˜<ýôÓcÄGLõ¼æšk’°ã]#)J% kÜ +¬þ¶xeñDËm¶Ù¦¬ÀC\­¶ÚjIt-¼ð±ꪫ& àrË-×ÊË –4D¢îÄOLBKÚJ+­“N:iÊ[À»îºë’X$K1aym´Ñ’À,'DÉûÑGÅÖ[o\@©ÇFm”¬ƒóÍ7Ÿ#Y€$  H@@»NàáBÉ97ΫB0»>–Zð¾ÿþût.Kß¼óΫ¬²Jl¿ýö±ì²Ë¶ñÚk¯Å®»îš¬vã?~«˜ã,Þý÷ߟò/¿üòU <ž»ì²ËÚÔWËQF%Y!+ <ÀZ7Ûl³¥ÿñ gòpý4I@€$  H@h@C <‚ã¬V°r©œp"Ð .—.¬bXÇgœØ{ï½[‹à<Ïâ¢9ÔPCµ)ú†nH¿Ã"W΂·úê«'—Ñ¥—^:=GÎøán9ÑD¥ŸýòË/ÉE‹\vÑ,¢ù¥<òHuŽÁ=s§vrK@€$  H@è@à ¼§Ÿ~::è XýõSð„Ó£>š¢`;ì°Iˆþù±îºë&wLÎð|òÉ)ŠevõD€a+ ²‚eñ…ð8p`,´ÐB1Â#Q¾þúë8î¸ãÊ <ÎßqŽ+â7LÎ÷áZ ²‚8<óÌ3+ZsoñnÎêñ_¬y³Î:k‡i H@€$  H@ 'ðè²§žz*Y¸FBt˹æš+¹mr.Çù7¬i\“€ÈBòûa†¦µÇûõë—,z\µ€5o5ÖH.˜$"nb}Îï¿ÿ³Ì2K:GP•r¼—^z)Eáìß¿*qFÍ .¸ ‰Ï|MgÇ{ìôŽö\4ù=ï衇RôM®c0I@€$  H@èˆ@] ¼Ž*_î÷Yत‘çô¸w;þL€$  H@€ª!Д¯ÒÙ¶j€ îçädw¦§ºêVõl}ú‘–€$  4^£Í¨ã‘€$  H@€š–€¯i§ÞK@€$  H@F@×h3êx$  H@€$ ¦% ÀkÚ©wà€$  H@@£Pà5ÚŒ: H@€$  H i (ðšvê¸$  H@€$Ðhx6£ŽG€$  H@hZ ¼¦z. H@€$  4^£Í¨ã‘€$  H@€š–€¯i§ÞK@€$  H@F@×h3êx$  H@€$ ¦% ÀkÚ©wà€$  H@@£Pà5ÚŒ: H@€$  H i (ðšvê¸$  H@€$Ðhx6£ŽG€$  H@hZ ¼¦z. H@€$  4^£Í¨ã‘€$  H@€š–€¯i§ÞK@€$  H@F@×h3êx$  H@€$ ¦% ÀkÚ©wà€$  H@@£Pà5ÚŒ: H@€$  H i (ðšvê¸$  H@€$Ðhx6£ŽG€$  H@hZ ¼¦z. H@€$  4^£Í¨ã‘€$  H@€š–€¯i§ÞK@€$  H@F@×h3êx$  H@€$ ¦% Àëà©ÿ÷ßÃI'ž{î¹°Øb‹…<0Œ>úèÜ O' H@€$  4"ºx7ß|s¸ä’KZpïÒ¥K˜xâ‰ÃœsÎÖ\sͰÀ tªyùæ›oÂV[mUèóå—_¦šjªøûYgî¿ÿþ0Ùd“…«¯¾ºSËÎJ@€$  H@£ž@§xyd{ì±GX}õÕG=É {ðÏ?ÿ„cŽ9& <8,²È"áˆ#Ž(xð;ì°ðâ‹/*ð*dÙž‡½ýöÛá³Ï> +­´R{6[·m]wÝua•UV‰kM“€$  H@hFà-·Ür¡gÏžá÷ß/¿ürxðÁã,Œ7Þxášk® ãŒ3N§šB5Gm´}ÞyçÃ'Ÿ|R3‡¸4´ø29à€Âl]tÑQºŽðZßwß}¡ÿþaŒ1Æh·¾üý÷ß¡wïÞáðÃK.¹d¸å–[§Ÿ~öÚk¯v;‡ I@€$  ŒzFàָ馛ˆtÐAá•W^‰¿ŸsÎ9a¶Ùf‹?><\ýõá‰'žßÿ}˜rÊ)£§‚Í{7ÙÐÏSN9%ÜyçÑs†ò¹ãŽ;†i¦™¦Åì|ñůÇK/½† ¦›nº°òÊ+‡µÖZ+6šì§Ÿ~ŠçöÙgÃwß}&˜`‚°ð ‡-·Ü2L>ùäñ°uÖY'üñDZ?wß}wxä‘GÂùçŸûž·ã?>tïÞ=¾Œ°ecŽ· ±6묳†u×]7,¾øâ…½ûî»aÏ=÷Œ¿Ÿx≱×^{m ÿˆ˜o¼1 2$¾ÅWD>Øo¿ý6ÜpÀ˜{î¹Ãé§Ÿ>R_Ž<òÈðüóχ9æ˜#Àÿ²Ë.‹<Ë|óÍÅÂSLQøâž{î‰Ç|õÕW‘¡µÛl³Ma¾88Û.}¼øâ‹ [pÁÃn»íàJ¸îo¼Æk¬8§Ûm·] ÁŠhæ|üÃ7þøãÇ<Ç­·Þ:L4ÑD#'½ðÁ„“O>9\xá…­ÓQoÀïnß¾}Ûõ”yÇœ±&a=á„¶ë¹lL€$  H`Ôè´ÐF6¹kˆDÀ>ûìÅLÞ–_~ù(p°b¹}Ùã'tÒèAAœaˆ Â'ñæ Axì±Ç†1Ç3Œ1"Š‘?þx¤ã*.,/ðyçw^ÑUÞí·ß Pô˜M6Ù$Š,+ðzôèQ®¼‡ Cx¦vvÚi§(±gžy&†Žb}úô‰Â5oIˆáYB¬!²†÷ë裎/‘k¸í¶ÛFÁ˜·±Ç;\pÁaÚi§o¥vaˆèÍ·‹Ø¦=øfmï½÷ŽBCÜp áÉ'Ÿé|ÿûßÿ"_Î[Ìàѵk×°Å[}¿ž^,æù­¤ÜèÕ«WÁƒÇgà…0'—U“€$  H@h Nàýúë¯Ñë…÷‰M+Þ;Ì>ûì-§]vÙ%z²úõëÞ|óÍxìæ›o7üˆ „Ê·ß~__c5b®ä KǾöÚk™øÃ?Œa™Œ)yë`‚ˆA¼Ñç¼!”ðb"Lù,†À˸¦ˆsÉ8ðⲎƒx0ñüÁnß}÷"’°U„)ŸÅ#˜ÂESÞ[[Þ 7Ü…Þi„c{ô…q¾óÎ;QÄ"ü4 H@€$ Æ Ð©^6ÄÏíRFµM6ì­ ¼ì# ’hcs6ÀΙ áH€$§.^„ ùzIèU+ðð°Ç†šºÄKÄŸß{ï½8 Q…¸ÊŠÌÖD m%¯ ÂoyqâÏS1Ë 1ƉàÅZ{=å-þ‰0Ë9Œä•ú|C^cÊ£KUSñœ‘“ˆ¸Â3†øjͲçËóõ×_G¡~Çw^Nq¥üÆäéB<²6Ä-^6¼·¼¬ÀÍç½µUàá¹Ã£—ÎÛ}¡ÿ¬wòMñj€$  H@A Ó<ªhêwÜqÇȳ¡^vÙeãï•<„•*[xæ`Ëñl:„]µ±A("‚Š˜ÉÒ¹ùý¿¼¬è('ð²yÙåJ ;%ôCøEUÈÃÙZA’j^ªÐHØ"¹uœOXz¾yu—^ziìVkí¶§ÀK8Ù’/I8&óœ, ¼ì3 ˜ˆ¬~ʱpÄóš*_ò?áÂÉÚKàeC6i»=úB;ÉkL¨¦ÕUã ÝQH@€$ N#ðR(eÖûDˆ^ B Ñ$|‘M7BOJ¶ºevª[xÙÇæ™ ÑÌæ|áJ9hÉÛ—_NTù$GŒjž„ÕfÉFº˜ÀÃû” ›à D &ˆh†JõCŒrú„‘bÅB4[xOÈ#b /ÂùˆÕ:)êÒšU#ð²}Æ[È9˜# Â`í)ðh<6òÙ˜s<^Ùjž¥.sD.¢¾˜/+ðnÌß¹çž;Rsœ /"ñ•,/ðÒW›ƒ—xíÑúˆïe ÿõëP€$  H óètä)¯ŠŸ³9tlœ Ä+TXÄãòþûïÇ"©âbVàá­Bp!ÀØ8§GàÕ¡òf¾È Å*ȇBó…¥<@Š¥§ÅòɽClÏFnÏé£8ašÅ^ ›n ±J a´›Š¬[–Ьàm+Ud¥”ÀãO0y•ä2¦œÂbö)Àƒ Æ[ˆÈãÆ7˜¼u´¿T„…u‰7+ðwˆjÚ@|",i§×Ö¾'ˆ75UÑÄŒ0E¸j€$  H@A S <гaOÏ<Ãc– ¬à£"âSO=sÌðxQ±ÏI*MŸxT®ÄÃÐAü­°Â ±Ú&‚-käç!À(pAî|΋÷'…‚²ÙgÓLÞG}?>ýôÓG¯…KJYáØl~ÎAè'^6΃áíA pÓƒÎ).B]²j•4Ó³ù`“žƒ×ÚÒ®FàÑ^NXÿøã±"%9uðy衇Ú]àq>˜Ð>žCòùp]øSä&?§iœTöä!ô)|´˜ÀãXÖyƒ¬;ªoÒ6â: &òùù<ˆ>=F‚ušUÌÞÍT±ÁGÈ-ÕN[{LBÞƒ×Ö¾’ËÚNý%|˜yIÏBlŒ¯4G! H@€š›@] ¼ZMMVàáùb“ÞŒFî¹ra¯äñ5£!ÈÈ­KÏÑk/Å.Þ^mÿ×v¸™@h1zHÕ9ÿk›~^€$  H`Ôhz—ÍáõÓÑ1=xýõ×U. äñ< yˆÍjxÒð–ÖÚ^–/²Ò^í¶G;wÞygxë­·bQM€$  H q(ð¶Ú*4ië„Ç2=—ްQ môèÑ£Y'a' —‚0íeõ,ðÈGe¬<D“€$  H@h ¼&x ¡"'ù‚ݺu‹9Y•Ñ$  H@€$ ÎO )^çŸ6G  H@€$  H`d ½ûî»a÷Ýw«®ºjØgŸ} ÇqÄá­·Þ 'Ÿ|r˜yæ™Ë¶“=`­µÖ “N:i¸üòËË~î•W^ GuTXzé¥Ã¾ûî[öx¨ jæ¬6=°U t ^Çpö,€$Büm¶Ù&Ì4ÓLqÓ>ªí›o¾ [l±EØyçÃzë­W“î4ªÀÛh£ÂO?ý;ì°Ð³gϪØU#î¼óÎpÞyç…gœ1\xá…UǃÛ@5sÖ~gµ% t<^Ç3÷Œ€š•@Ã<„ÁÜsÏ]ïã?;íJL IDAT´“¯ÄUÕšïÓO? _|ñEXtÑEÃh£VÕuYXøûï¿ÃsÏ=f™e–0ÕTSUun?ÕÌYûÕ–$ÐñxÏÜ3J@hV #ðz÷î^xáºx„îµ×^ ¼6¼ÿr!*þ ½QóYçlÔp÷¬O@×ñÌ=£$ f%ÐoĈ¡W¯^a‰%–¨ ÷ÒK/…ƒ>¸¦%®zËÁ;ðÀà +¬Pözj̓Wöƒ%@,L6Ùdá²Ë.û/ÍøÙ$ Àë@Øžj”PàRüž\@SèôïC /¾øâH“¶Æk„=÷Ü3¾Î&r•UV Ûm·]àìÓO?&Ÿ|òpÑEÅ<,ò±Î:ë¬0×\sµhg³Í6 „òeE¿ß~ûíá¾ûî _~ùeo¼ñb8áV[m¦˜bŠ€'ñ¯¿þ©?¾å–[.¾þË/¿„k¯½6 4(æ›M;í´auÖ «¯¾úHŸûöÛoÕW^žþù0lذ0û쳇­·Þ:œqÆá÷߯Xà~øá1ô‘|³[o½5ÜsÏ=\AÂé3çφDVË%Y9ôÐCÃ8ãŒÇ÷Á„ 'œ0,µÔRIX%kMàÑOÆzõÕWGžÉž}öÙpýõ×Â_1Âq7ß|óø2æyê©§ýúõ ×]w]¸ÿþûÃ÷ߦŸ~ú(¶Zh¡ŠÎ?tèÐX0桇 ·ÜrK ltâ‰'Žù”År*ûöÞ{ï…?þøc¤9Dp2Çå¬Üº`]õéÓ'|ýõ×á’K.iÁ礓N >úh8óÌ3 LÞ|óÍ8Ïo¼ñFœëI&™$^p}ôÑGbÁµOøq“b¬±ÆŠsvØa‡XÐç®»îŠ; ÞîÝ»Çñ#D«¬{o²Ä‚ÏÃjši¦‰ë„ãY+›nºiä‰ð†?³–ï\wˆ¯,ÓÙf›-|ôÑGqís2ÿÆ &˜ Þ`É ´Öj¨\3ÌÅÏ?ÿ{챸æ/¸à‚¸¶°r×c1Ö¾&QA@7*¨{N H@ÍI Ó <¦ ÏÔ¶ÛnÛjˆ&›Èþù'zØ ³ÑLVù÷ßãæ˜ jÿþý m"Ê?6ÑØ½÷ÞÅL±*š„0âÕ8á„ B¾Qý“ÍõUW]Kýcûï¿<¡@Hf2íÞ{ï]µÀcƒŽ1D¢·oß¾QüâJž¦j¸Ð¯$ðr„+ÆØø±!l9?VÀãÑ ôOêDMTà€Ç+;—Ì3´•VZ)zoñ@açž{nÞ¼†g·’ó#ؘ“äE|üñÇãœá…ÅC•l“M6‰žÏK/½´À”õÈœá…m%Vͺ@P&aÉMúóöÛoÇ>$áýM¢6ôëÇŒž¸¼0ã8rGyt†Ã[̦F‰êÃA–_~ùLÙxÓ ÞÄ£{ÅW´¡¼^Là½öÚkQ¨"Ð>úè0æ˜cÆöSn+Ÿk¥×c%ü=Fµ& À«5aÛ—€$ D iÞŸþïö#ò²VaCI(#›\ÂØð&³Ö^]vÙ%ztØg-mjñرÁÆÓBh1<ùŠ’Õ>&¡µÐÇìæ;+H«áB¥žƒ‡Gq‡×“ ;VÀC "V`^êÙxlüñ*!’³¼—ðF$#p*9?Þ#Ñ7ÞóÀ( AˆÚŽ;îXS­ ùäB˜b©ù©f]¤ó’ŸÉù0}ËÚ¯¿þóÝCˆÀ”ÉÀBÐwÜqa‘E)É"»nò/yeÓz-Å4õ ï9„7r±bsÆZA<—2®”OYîz,ÙoJ ƒ(ð:´§‘€$ ¨Q²5#’Ñþ¯:Ý¿Åø´ööf‰G¢Òü¥JrðZóàñG÷Ž;îhQ}0¥XÍì8)&ÂÆ—7Æ‘ o¬Và¥qá¥Ä{CN+r!S(å¨xº$ìðŒa<ªaN!”d¥æ§šu‘Ú#/±–òÓ²Åf8Ï!ž; çÀ%Gž%7'F…Àcm!Ô¹ÞRþ_±9#”–0TB5[3Š&孵뱽¿›lOm! Àk 5?# H@m!ÐbT äqäå­T)v뱨^­ÈÚ®$  ä 4„À£‚%9^ä"þ•/HRJà%1–-1W†ÐF6™xÁ’Àãuª9.³Ì2-8&DnE\R)ûl›é„ò\2*Y®½öÚ-Úacž ·¢F¡Êüg½-| …ofûVni'ᔊ¸¤ã %$¿ðIò›’§§.´•ynäZ¥|0rÀ¨ÌøÝwßEo[*rS,l’vŠ ¼ôȉÔ7ŽK¢‘Ü:æïWæè¿X¥ë‚\>D-,ðÚÁ™ùc¹Á€Ç–ü5n¾Ê¦2]xφü_K} ý”gÇÍnPì&[É^ÅÂ^ ¹LÏÒ£êm6÷kÜRt¥×ã™?+ö" Àk/’¶# H@å4„Àcl\ñlà›sÎ9cùüôPêRgªñàgþÇ[†Hd³œ„“ÀK¢‡3!bˆ±÷ß?>óްËã?>òfC‹G‘Â)ˆ<6º´‡ñ«¯¾ŠynüOéy^g£Š‡×(à‘Âד©HÏ­C„áÙC,±É­öAç´Ç£xç&tò™gž‰b¾Ó—dÕpÉ <ÂÈý«ôz¬„¿ÇH Öxµ&lû€$4ŒÀÃÓEHC,l SARxÜð.ñq6¤ã <“E§ç¯%`xÃòAž5†HLÂŒcÙD““G›löyŸ*•<^ƒ Š:6ÄóÌ3OÜàÎ;ï¼-V'í.IÎã"Œ±D˜“W.|45–„^FÄð¢Ì<£fÃ\ìñÕpIO¥ÅKFq7;Õéñäâ¥-ö@n‚ /%ydÅJÕvªÁVÐÙÖ*£VðQ‘@ÃPà5ü;@ H@uC@W7SQÛŽ(ðÚŸïí·ßCeyxö‘œ‰ð]ŠË¶‰ÈwÜqÛ¿uÖ¢¯Î&ÄîÔ^]M‡‘€$ÐÐx =½ÿÿàxí?Ñ乑óF™^½z…¹æš+æa’wI"ïS …|´f0^3̲cl+^[Éù9 H@¨–€¯Zbôx^m&ŽŠ¦º¡ s¨ JQ‘EY$V Í–ø¯Mê§U^ýÌ…=©? ¼ú›{$ H Q (ðuf—$  Ô ^ÝL…‘€$Ððx ?ÅP€F5Þ¨žÏ/ H y(ðšg®©$  Œ" ¼QÞÓJ@hB ¼&œt‡, H@K@×±¼=›$ f& ÀkæÙwì€$Ð!x‚Ù“H@ÀÿP๠$  H@5& À«1`›—€$ ž‹A€$Pc ¼¶y H@Pà¹$  H@E@×Q¤=$  èÁs H@€jL@WcÀ6/ H@zð\€$ Ž" Àë(ÒžG€ôà¹$  H@5$ð÷߇þýû‡>}úÔð,6- H@øêVàÝqÇa†fÝ»ww®$  H@–ÀàÁƒÃСCCïÞ½;íì¸$  tu+ð†}ôÑðÉ'ŸtšöT€$#Э[·Ð³gÏеkWÙH@€jN n#}ôÑkÀH@€jIàŸþ©eó¶- H@hA ®žs% H@€$  H@•PàUÎÊ#%  H@€$  Ô5^]O“€$  H@€$P9^å¬|xØ`ƒ ÂN;í–\rÉ0ù䓇ÑG½pÄ/¿ü6Úh£š ¼§Ÿ~:œp a©¥– «®ºjüñ îÑG çž{nsÌ1ÃóÏ?V¬o¼®Àk¹*¿øâ‹põÕW‡vØ!L:餭r눵ܯ_¿ðûï¿·»À+ÖnGŒ'{޶ôᦛn ãŽ;nèÝ»w»w·OŸ>aà 7lH—ÿ.ø/EguV¼‰3ß|óµû<Ø $  H ž Ô­ÀËBK<Y«ä.q=ïUßÞ{ï½°Ç{„«®º*Š»¼}ùå—aÛm·­©Àã:Þ¶å–[.ìºë®-ºðï¿ÿ†ÑF­èð‹õ­òJ+­Ô¦)(Õÿ65Øj[µM·el§vZèÒ¥ËHoàÀÑ»ÞV+Ön[Ûjëçê¡Ù¾oµÕVa›m¶iHWÍ•óàUÓ–ÇJ@€@C¼3Î8#\{íµ°µ©§ž:†*eC_z饸ÑÄã7í´ÓFá±ÄKŒ4—ÆýöÛ/œ~úé1d¡„§0ÛÞO?ýà Ÿzê©ðí·ß†é§Ÿ>Š©9æ˜#¶GØÑ+¯¼–]vÙèaá˜Å[,Czè¡è¹B`et¼|Oךk®½lńޛ /¼0‹§€v¶ÜrËØî#<N=õÔÂØ¸{}Ê)§~Ï¿Ïðk¬±¢7´½X>øàƒðIÂ3 kͲaZ­õm®¹æªØƒ·×^{…ùçŸ?l·Ýv…S‘ßS¨!š›o¾yœßgžy&†Óáñ\{íµ‹v“ã=ôÐrÈZb}0ߥÖãzî¹çB÷îÝÃý÷ß~üñÇø™=÷ܳE81"ˆß}÷]\³ôsî¹ç.ôãŽ;îˆë o6á­lðçwÞ!µo¼ñF‹9Os ·Rk%­×W_}5zY/»ì²xÖîùçŸ×ëØcû‚G„ÐÚwÜ1,¿üò…þÑ_Bd“õèÑ#†2þ»îº+®ûÄ~Ðï8 L8á„ñð?ü0Žíå—_¿þúkX`âµ6ÑDEÅÚÍOÐ×_.¾øâðâ‹/Æ·]Ä«I¿Ë‘0çœsÆÐá{ï½7pS‚Ï#þ¹îZëC©~Ó‡¼ø(w>ÓÚ<§ñ~óÍ7qî³Æwk›öaF¨î7Þ8Ò8øÌü.¹ä’ðØcŰm<ûŒs¼ñÆiͳÞiÚ;_Wð¦ëãÏ?ÿŒ?sÞEY$~žïVÚã|Ǧ›oGydÀ£Éµ6ÝtÓŰÉžÑÌÿþÕW_EO?ë›ñõìÙ3®Aæ¶’1çC±ùýàƒŽ ž}öÙ0Î8ãD±œý~òÉ'ãuÀ÷1k>aî¬OM€$Ð4„ÀCd!pwlLñ&° Ç}‡vX̹bóÉfáì³ÏŽÇ±áÈZÚ”°Áf£I~ØÙœ§ö'DÜᙚl²É¢Ðb#Ê&c“Â&‡D6š¼Ç&wâ‰'.Üyçò߀ˆ D#^.6]lBØÌ!ÒØÐå½L?üðC<á°É&›ÄÏ"¤fši¦pÈ!‡ÄM c>è ƒ¢¨%| Z2Þ‡mú‰‡÷Ù´"ðÚ‹%³÷ß?².eÙM]k}c³]iˆf¥1o¿ýöažyæ‰YDÈGQTø3'ˆTæsñżÈ),µ®y¤IÄ#ž1¼AiÓ æŸ=¹kÌßÃ?7ègžyf˜e–YâæzŸ}ö‰a®Ýºu‹óÊF7/ðX§Åæ”u[j­¤õzà 7DaǹXýfÞÄç>üðã`gcœì¯¿þ 䜱Îvß}÷ L.%ãgý!j¹69Žki5Öˆ×FØ5\¸.™c„á /¯—ÖÚÍ®%Ö>ë6œƒ6ظoºé¦1dµÜØ CÖymï¾ûn¼îé×Wk}(ÕoúWLà•:O©yÎŽaÅZâ:åûÎðæ||‘/³Ì2ñÆbŠï„ã?>ŽƬUÆÃ÷ ߬w¾¿Šå ²ÞYSéÆ7’˜{Ä7ƪx¬+æ{Ê)§Œk›pûK/½4®—rë‘kõ@Èþ /¼…e%c†[1Ç 1Ö9"“o®AþqS5Îw(ë™÷¹ÉÁäæ×g11ÜþÈÛG H@h> !ðÎ;X!Æð‘{ÁÆÛÿýãm6«ÉøŽwcã7n1ãIॻмùÄOÄyj/¿DÞ|ó͸JálZn¹å–¸iHÆ~†f(„°±Yá܈86ëˆ=6¦ˆÎä±£ Ô6ž\Œ[%k…~â±`#;É$“ú8ý裢¸ÄX[F6ùyC< òEVŽ\éÂO%ª˜!ñürýaÅÚÍ~Žkìž{î‰ýOžÆô~%cG!V¹Ù‘Œ9æ¦ ž±JúÀ1ù~x¥ÎSjžóœÖZk­ÀÏYá8Q²ä±fxHYOðJsŒ$ZùÏ_SÅŠ1&¾›¸>ªxÙïR&ýAð!ÔÊ <„,7Ûò!ÞŒ‘þ”3ïxx “'ŸõÌÍ¢ø{Àw.ÞvnÚacúɉ¬W½èâõE H@@h—­¢‰‡‚ „ÇKž ~¦ wìÙ\g­XN_¶½´Qe3ÍÝ]„Þ6„ !3Î8ãH›–´Iä6w¢1B¤(À€Ð$|“Íb.ÛG6C„:!²ÆØ¸ÛœÝ”¦¢UVYå? ¼öbI¿ñLàIÀƒ#œ#L‘Í%c¯Fàá bƒ›5ÂñÖT+ðòU4ÙÔ1ÌcÞòEx—[WÅënÝu×^Öõì˜óâå ´36Ÿ„zâIÃ;ÃÃ*x•¬•Öª]~þùçQв—xÈX_x0óÖšÀËWÑD¨â%Aèaˆs¼á„Åá=cÃM¨0Â0]7yᘿð¬xà#õ©’±ËãbNÅ#D±µ>”ëw1—¯r™=O©yά5Wª}(>ûýÂæ{‹Ù|u1G„4®j^¾ÂqV´•x¬np3„ïk®ô\ÉÜxÙë>ãØcžcÂL¹IÁz^h¡…âÍnl0"04 H@@g!а¼&6)6käeMM>䦔À£=6Ÿéñlx)ÛX#Ä+yÏŠm˜Ù0”xlø¹ÃŸõpÑWÎGH^~S›x¼Ïf¼\µxmaÉp¼lÖ󜹋׳ZWÉcØ ‘wX.//ðY#Ì1 Ò,ób¯Üº*¶R…P„=Ær/õuÉ ¼Óx3ðøµUàå×J©Çà­Ã;Ê?<7ˆéä5Îò©Fà‘c“N®^ÎØC"r+xô Ñ’½Ù‘úULàåÇÞšHÈæp[¹~W*ðò¹¢Åæù¿¼Ô>!¥\ÄMç-=&¥Ôzç=¾ß¸±ÅõÁÍ'„f%9xyÇws'­œÀã¼Ü!g!Êw$er8+™»J^v\›Ü´ãfÂ’|m8j€$ ÎD ¡‰ ?¹vxŒÊY%a—-ȧƒÜ¬ÿ"ð1ËÈ(¶Îö›;ñlôÙ“Kƒ¥M#A>R¹ÍTe3»ùªdìÕ°DÌ ¬)Sž÷”–xÅúÆ+ÍÁÃʦñ„ᥠ0‚.[d%+ððf ® ÃJ^ÖrÞr,Š 'Â}ñ4¶ˆèEØÐ|ˆ&¡¢‹¼BF»„Úåç«·JÖJ)G¾¢Ž¼+6¼ÅØÐÇbÏ}+Ö.k6^ÂÛ IDAT <  pcO/!Ô…N¸’À+÷<9ŽM×B6Ï”¶*{%"!߇JúÝV—æ;;Ïù5°Þzëŵš}†c¹qàyCðâ…#* œ»¡Á5L8yZ<®”£‰÷›k/_d%ûóùþDsí—úJ‘Äá\ÇåÆÌçªx|oŽŸB„Ëqò} H@@½hx—67ä;‘[ÃF1EU8`Ö*9xP¦™fš¸9EÜ‘çF(âxä%Ñ.ž'úÉŸ¢ l¨È©Ë›$Ž¥07wÕ Ýcƒ…•xˆ>›˜àÍ$G.ÿÐøžš¼—Õ°¤š“O>9nW\qŘ'Exõ‹¢D~“W¬oˆ¡J›~„›HÄ2óóñÇTE*”¹d|<ð@™É[±µr,j áPý¯ž+Æœ [†I8&e„9¹•°!,“Šš„8â!^zé¥ãšÀ#J‘r7ókµ7¼åÖJ)ǹ)ôÃÿxóZ«"Ȇ1ÈœrMQy¶œÀ£@ ëñÍfŸ¢5ˆ2¼CIàk7;7T¨M× Ea0<ÇÜ€áæG¹±W"ò}À‹T®ßÕ ¼Róœ_‹Ü¸ ޱ1Frl˃RÂX¹é‚8DäQ‰ÐPÖf±õŽ÷ 1ÅÿôpÅìõÁšM7Føî@”Òf^àQ8ŠïZúJ4óÎ:f?‰ä3xb³k†›“Âq•<61lÔ?ýôÓXb;Û4á5ÄX[B49÷gŸ}y'’þ¹cÍÆ?oËf o .pl¬åíò6yˆ!rÊ ¼jX¦>3D "˜œÆÃÝÿT$¢¯|ßͬTà±ä±„["ÞÈ÷cIÕÁ¬±"ß~ûíØ'òͨ¨YÌŠ ¼r,ÂO ¢±Å Ö]Ú0²Aæ8Æ‹p§â ï§3Sq“b'\¤Ì¡bôUl­ãVn­”xŒ‘ó?þøã1'0_Œ#±¢ïx&É£cÝR”¢œÀã³ä±âÍd3Ïc¨‰xM¯X»ùùA¼sÝ á c®n&”{%"¡XÊõ»ZWjžóãeœä0"nY—ðJÆ1|øðèõçæ7¹¸!„'–7ÚMaÀ|—plþúàz¦ˆ;Ö%‚ïë%=&›\œ—›XÜÜ@˜¦‚A|áI¾ŸÏ®®  •.áƒÅ{¹Î:ëÄîV2æj=xa<êô^Éèw*~äÖA€$Ðt ×@ÚG d $÷_ô]D©Š'Ѥ5„ž¾ü#Z}ôˆSž…˜ÂP™Ü$àæB”›š$  H 3Pàu†Y²Ž@9ÏX½¯ a—èÀ“/öSïý·m'КǺí-vŽO}ôÑÑK7‘BVx:ñ"æMõÐü£J:Ǩì¥$  4#^3κc®9Î,ðKL§&$”œ'­y4«Àã1.xñÈ÷CÜ!ö?%ç²Xnnó¬G* H@€¯³Í˜ý•€$  H@€$Ð žKC€$  H@@ƒPà5ÈD: H@€$  H@ <×€$  H@€$ ! Àk‰t€$  H@€x® H@€$  H@ B@× é0$  H@€$  (ð\€$  H@€„@Ó ¼>ø ð0çË/¿tíÚ5Üÿý ¼FXýŽA€$  H@ F ©Þ¿ÿþ>üðò¼u×]7ì±Çaùå—ÓÛm·)ðìBp8€$  H@hu+ð®¿þúpÏ=÷„+¯¼2rþå—_ÂFmVZi¥°ï¾ûÆ×Ž;î¸póÍ7‡1Ç3|ñÅá / „TŽ;î¸ñØ-·Ü2¾‡sÌ1aî¹çßÿ}l{‹-¶ -´P Çyvß}÷(æÖXc°ÕV[µ˜ç{ï½·¨À{ñÅc_?úè£ê¹á††UVY%~öÐC OzøóÏ?ÃÚk¯öÙgŸ°ì²Ë†±Æk$÷òË/GGç’K.…Ú™gžïÝ»w wÜqGA¬öë×/<ûì³aþùç/ˆÕ½÷Þ;,¶Øb±ïš$  H@€$ ¶¨[÷Ï?ÿD±³ÓN;EA†("ÿí¦›n _|qô’!º_|ñèÙÃ{öÐCEO {óÍ7£€â³=ƒràÀášk®‰^½l!„èÒK/xà(Vÿý÷°Á„ÓN;-Ì9çœm™G?# H@€$  H Ô­ÀcnN8á„j‰w Q´Ã;D!‡W¬gÏžaýõ×gœqF˜}öÙÃGÆüpÀ¦•;¼oˆ+Â%x·âÉIàõéÓ' AB+·Ûn»K£œÀ[guÂ.»ìV[mµÂçð"@ñ2"F7ÝtÓXsÑEÞ뮺*l¶ÙfQ~þùçÑ ‰xÅk¨I@€$  H@h ºxä»ÞÈ# C «¯¾:†_®°Â QÝxãQxAàõíÛ·¬À#Ooã77ÜpCzÓM7]g9Ç9 «®ºjá3o¿ýv¦8*p’[7Î8ãDǘ¦n¡mrñòªI@€$  H@h+ºx_~ùeØvÛmc.b¡GîZÿþýÃꫯsÝ?üð8vžI÷È#DAÕ¥K—øZ Ñ¼à‚ B·nÝJzððâ<õÔSc>ÞÃdåÁ)§œ2xà…Ï  å$D“þމ°[d‘Eâïx É)6lX|¾Â¡¨I@€$  H@h+ºx !Dèå ,‹– ˆÈ¹›cŽ9¢oÍ5׌c§2&^´=zÄpÈü1Y™qÆc¬TˆfzÐ9í öR%M>—xTàäøK.¹$L0Áᥗ^ŠB3_dqÊg1rÿ¶Ùf›(4·Þzë(ôžyæ™(Hé+ùwôU“€$  H@€$ÐVu/ð(ˆ‚ ¢º%U&1B.y~Ý€ 3ÌPûgŸ}½b¯¾újÌÝC"¦¨|Y©Àã8¸0Îu×]7üþûïaâ‰'×]w]%ã¯ø˜jÞC=N?ýôíï»ï¾aРA ¼Š©{ $  H@€$P ºx·ÜrK¸øâ‹ãXn;í´SüùŽ;îýúõ‹?O5ÕTáòË/?÷Ýwa‹-¶ˆ?wïÞ=üñÕp({lµo¯½ö ï¼óN¡ÿ³Ï>{èÑ£G@¸êÁ+‹»]>|x¸á†¶ÛnÛ.íÕ{#ÜÔXe•UÂd“MVï]µ€$  H@5"P·綾^ ‡zhöb‹-Ž:ê¨øóI'üñ6µxì}<è ƒâëë­·^ØqÇÛ Ù?ÿün½õÖªB4 Ç6lXìÃ]wݺté>òÈ#xí63¥ºúê«ÃüCjG¥½öÚkáÄO Çw\˜yæ™Û­+ÿýwôl~øáaÉ%— ÜùôÓO74 H@€$ æ$P·ï§Ÿ~ ›l²Iœ•é§Ÿ>\xá…ñç­·Þ:|ýõ×…ÙB0-¾øâáî»ïçw^|PÈ•VZ)þüï¿ÿ†ûî»/Ü{ï½á“O> ãŒ3NX`bÛݺu+´“ùäðØcŦñRâ­Ä¶Új«ðÍ7ß„ÑG=nô篿þZÈ\zé¥ ‚9Û¯¬g’1#diÿÇ ÿûßÿBŸ>}ÂüóÏ_øÈ/¿üî¹çžðÌ3Ï„Ï?ÿ<üùçŸq~´Ë.»ìHûì0Æc&>û>áŸTÃ$$nši¦‰Ç²áÎÞ2ÞG@$ÄûÅv¬õT|¨_“€$  H@h\u-ððÐ%±Ãœß)¦²é¦›FdžC"Bð„"H¨ b<+l˜1D`ò„୹袋âëÆ:+H9ˆ¬å=|é=Äð¼U#ð¨ô˜6äÙŠ¡´IÁ˜ß~û-6' ±Ã¯Fn¢‘=ÂïBˆñâéÃãEÈ)>ŠYv\xÙ`‹µö:BŽ~0Ù\ÈÔ6çÇ3×ÚçÏ<óÌ€ÃóÔ³gÏø3Þ5Š‘`„^"¦²óW¬ï²Ti5ÿ>᫬ƒ¹æš+¾•^ÊÙä5ÆÂ<ï·ß~E K>!‚›>«I[ÙðVæqö_‚¡˜ÀË 4ó­ <Þ'ϋѬÕRàeCOñžQa!‹ØI?B“ç‡0ÈT°&¿’«x„#R¡‘yÁð ñþ¤\:<„¯v„ÀCè¤Üü¸ÈMd^ðÔaù0I^#¼4‰ó¬–7Ü<`xî M!›|®½^ Ù¤M„:ÞØl?XÃô– âÿìó‹ <ŽG â5.æ±mØo2& H@€$ ÔµÀÃ#‡G‹ÿ^xᢉ%/bîË/¿Œ¹O„ÎaÙ\¨lØ!‹ˆŒÿ£>.¢Y‰ÀÛn»íb¨Š\4rÏùKÖšÀCŒ=÷Üsñ0¼“„dfC4Š©0!§©Ä6D“?<–yX¬T oùepÃó“Æ› ©ŸÕ¼l?©ì‰°Æðd}üñÇñçöxÉ¡}òÙW$—1VjpÀãENVLàQØ9yùJ›9áÀXÉ…KWDáz)ŽŠ•Ŭ— ™åÑ ôóÕW_œ“µ§ÀCÔ¤ÇcP¬„œ:þ'ì”üÄRÏ·ÃÓˆ¢(X1GÅPæ ±ÅÂóF'k/Þª«®Zx›pá9æ˜# [ ÇkË/¿|ü==ñT‘•¼Àãæ…‚ðÂ2Ƽ=ÿüóÑsÚäý$.³9x„u^Ê}jyÛ–€$  D <‚$  H ÆÎ?ÿ|^Û¼$  ü? {ØqÇÃl³ÍÖbI 2$\vÙeáí·ß¿þúk˜i¦™Â:ë¬V\qÅ0Úh£Žýå—_µ×^ ~úé§0í´ÓÆãV_}õV—ŸÙtÓMÃôÓOúõë7ÒqwÝuW8÷ÜsÃÁÇ‘Þpà á³Ï>‹ ßÿý0餓†Í6Û,žùý÷ßÃW\~øáø3 ÷ÜsÏxL2^'¸êí¢$ÐáxŽÜJ@hZ %ðvÝu×пÿ0×\s…É&›,ôéÓ'Šë®».\~ùåaºé¦ K.¹døùçŸÃc=ºví.¸à‚0É$“ÄК8AÜqÄaðàÁaþùçíòÉ'á™gž .¸`8餓¢ CÌ!¿ýöÛУG0à 3„ï¾û.н±Æ+Š&ú…}ýõ×±]ºt ½zõ ãŒ3NxçwÂóÏ?Î8ãŒ( 1ÚÚwß}Ã7ß|–^zé0å”S†W^y%öuË-·Œ³5;î¸ã¢(¼ð Ì3ÎØâ°Ã;,¼üòËQØ;þúëÄý§ÏO=õTøë¯¿¢X¾þúëÃüæ›o¾(¤?þøã°è¢‹†c=V×´_!\¨„€¯J# H@íA ¡Þ˜cŽ=J+¯¼rÍk¯½öÛo¿(XŽ>úèÀ1e¯½ö k­µVèÛ·oI‡÷ Ïb Q• !¢ ±Ô³gÏøò³Ï>ºuëÖÂ3ˆç!¹ù曇­¶Ú*wË-·Dáuøá‡Gá– ÏÛLPøýÀ ¯¾új8á„¢Ç CpuÔQáÅ_ W]uU ZvQ H鸭·ÞºðÖo¿ý6ÜpðÈ"‹„#<2¾ž9³æškD)¢o§v _|ñEô.&᪯=.KÛ€€¯ÑfÔñH@¨_ %ðÇ<ôÐC[Ð>æ˜c¢ŠN³vÀDrõÕWÇ—‹‰“¿ÿþ; $;Ì9çœñ¸SN9%†âo¼ñâkIàcˆçŽÐP¼•YCðâAÄ Hè+¦ϯ H@#Pà¹*$  H £4¼À£ ž¬RF¸äøã_Tœ$OY©ÏÏ<óÌ…B&äåáÙ"ÇŽ×ç™gžXeÀ€aŽ9æçœsN¡©aÆEaEá9‚Ûl³M¡0Ì5×\®¼òÊ’}§Ø ¹|­â’¼Àt,6Úh£(XSxf9‡oŠ)¦Ià¥ÐU^G]®žGè¬xuæì·$ ÎG ábñD¨fk¶Øb‹Å·ŠyŸ(¦BL*eæÃ?S{äÌÍ=÷ܱH y}TÃ$ü16!”/X^à¥Ï“φG0Òï¿ÿ>†c–yçw†óÎ;/,¼ðÂ1L²˜! ÿ÷¿ÿ•\y„]¦\¹7Þx#æ$fÃ3xïµÇ@ç" Àë\óeo%  tf /ð¨~É#¡D„•²bñÅãþý÷ßXL$ûH„|[xén½õÖpÚi§EY²r/G¥Lræ(~BŽt ³{÷îáÄOló:ã± Tæ$4ó¥—^Š^²á™ ¼6£õƒ€*" À«“I@@;hx‡ áYoxÓxœAö™m<Ë$©p ùr=¡h ^¯däï!îðªí¿ÿþ…Jœ¼ÏóëxÜ–ŠŽdC ‰äñ´‘õà½ð 1„3ÛòôѤh Õ/1ú×­X1–칓H#÷Q˜}¾U3ÉE\mµÕ!§LɆg¶§Àka;¬U›€$Ði (ð:íÔÙq H@Ž@à ×(`2Ë,³ÄÂ*@γ +D!Èsíðúá›fšibíñˆžqGåOÄ'bóÁŒÇzê©…ª”ô“6ù$žAŽÅ»Çk<(öi“çéaùÇ9ð^Eá@Á™|xf{ ¼Övº+ÃK@hG ¼v„iS€$P’@S<ðL¸Ûn»-æÙ!jˆ0Ä]ª$ÉqxÚ(|BUÈì£ ðÄQÕòTÖ$d\ïÞ½c;É øøãGAIΕ')ò‚h$ïêš©È B‹pNáÀ±ˆAž=‡8ˆwÒ'Ž¥O<ÞÈCÑ)Þ³ìÒã 8ްN^N¾a>5‹á<ùðÌöx¥z-J@hV ¼fyÇ- H ã 4„ÀëxlïŒÃ‡ë®»n®ùðÌÎ7{, H sPàu®ù²·€:3^gž½*úþðÃÇ"+G}tX|ñÅ«ø¤‡J@À% Àû¯ý¼$  TJ@W)©N|Þ»¾}û†.]º„‹.º¨ÅÃÔ;ñ°ìº$ NC@×i¦ÊŽJ@èôx~ [y‡/¾øb,&C…Nrôx¦ž& H@K@×±¼=›$ f& ÀkàÙ?묳ÂC=xúvÛmW¨ ÚÀCvh€ê’€¯.§ÅNI@hH ¼†œV% H@õD@WO³a_$  46^cϯ£“€$ :  À«ƒI° €š„€¯I&ÚaJ@À¨# Àuì=³$ f# Àk¶w¼€$ÐáxŽÜJ@hZ ¼¦z. H@E@×Q¤=$  (ð\€$ PàÕ°ÍK@@€ÏÅ  H@¨1^Û¼$  (ð\€$ Ž" Àë(ÒžG€ôà¹$  H@5& À«1`›—€$ =x® H@@GPàuiÏ# H@zð\€$ PàÕ°ÍK@€<×€$  H #üý÷ß¡ÿþ¡OŸ>q:Ï! H@MN n=xwÜqG˜a†B÷îÝ›|о$  tfƒC‡ ½{÷îÌðï€$ÐIÔ­À6lXxüñÇÃ'Ÿ|ÒIPÚM H@ÀȺuëzöìºví* H@@Í Ô­À«ùÈ=$  H@€$ # Àk° u8€$  H@@óPà5ïÜ;r H@€$  H Á(ðlBŽ$  H@€$мxÍ;÷Ž\€$  H@h0 ¼›P‡# H@€$  4/^óν#—€$  H@€Œ€¯Á&ÔáH@€$  H@ÍK@×¼sïÈ%  H@€$ # Àk° u8€$  H@@óPà5ïÜ;r H@€$  H ÁÔ­À»è¢‹Â­·ÞÚ*îË/¿< 6,ì¶ÛnŸ§šjªv™šW^y%œuÖYá»ï¾ ;î¸cxñÅÃLöÝwߨþ1ǺvíZø½]NÚ ùçŸÂé§Ÿž|òÉ0É$“„‹/¾8Œ1Æ…ž_qÅáûï¿{ï½wáµÛn»-Ü~ûí÷êÅ>øàƒ²k&Ìk¯½N<ñÄpÜqÇ…™gž¹äP*óQGÕbMÕ ›júQ ÇüµÄ—ͨ¸vöØcðÞ{ïÞ”SN×g%ãië¼å¯sÏ=7ôíÛ7l¶ÙfaµÕV«èÜÕÌM:¶’1µ¥Ýüg*9Ïßÿz÷î?üð°ä’K¶Çic¿ýö[Øyç ,K5üÒK/…óÎ;/\zé¥E«¦­v€ I@€j@ nÞÏ?ÿ6hРpÝu×… .¸ €A7dȲ›õj™í¹çžaši¦ Ûo¿}ÜŒÞ}÷ÝaÜqÇ›¬YÞóÏ?Ç~Ê)§„)¦˜"þËZ¿~ýÂï¿ÿÞï‹/¾W_}uØa‡¤“NúŸÞM7ÝÔbMU»FëáøJ6õùké´ÓN%ï›o¾ ýõWÄÆ‰áÇDÖ¥K—xs¨’ñ´uÞò×Îd“Mo"­¼òÊa¾ùæ«èÜm™óJÆÔ–vóŸ©ä<µxˆç,ËRãáæâ®5WM[íÁÍ6$  H@µ"P·/;àûî»/ôïß?zƒ²VÉÆ¢Zpë®»nÜü-¿üòE?Ú¬ï–[n ÷ÜsO¸ä’KŠraóÎf9ïÁ8p`ô°Ö‹U²f*9¦µñàÁ«·1·7ûÿý7|øá‡eo®ä¯¥z¸v¸Añ믿†£>ºæß%éå®ÿ²ÞJÍm­Úm‹ÀC<õêÕ«Ý=xÕ¬íx \ýõ­ ¼jÚòX H@@=hwÆg„k¯½6J7õÔSÇðÉÙf›­ÀÐD¿i§6l½õÖa‰%–h1/Üåßj«­Z¼vï½÷Žä±+¶I½ãŽ;b8é?üæœsθñ~úéc[Ïk#FŒ´GÐ +¬úôéFm´xÌ3Ï<î¼óÎðÎ;ïÄ׸³Oxèè£ß_ýõÃA|ðÁ0xðà0ÑDÅ/<‹^xaøôÓOÃ<óÌ8à€0á„ÆPª_ùEI(ê•W^>úè£èÑØpà Ã*«¬;óÌ3›£d›o¾yØb‹- ¿o·ÝvaèС…ß{ôèÃ;wÝuWÌ xÂæwÞ6÷–ôí?þˆ`¹Ì2ËD–cŽ9f<ÿ^{íæŸþ@Ÿ’m´ÑFñ÷l8Ü‘GðȺ7ÝtÓÅ0¯\0~$¿1ÎÿŽ7oòÓO?çkˆÞ=¼š•Œ9¿†˜ßƒ>8<öØcáÙgŸ ãŒ3NØf›mÂJ+­TÞ‡Ë.»,|ùå—Ír²“N:)ž?Ù¡‡&žxâ°ÿþûÇ—žzê©8\©=<ï¾ûnà³X©¹ç}ú;÷ÜsÇ\D>s¿ÐB µx¿üòKØ}÷Ýã‘5ÖX£¢k©Ü¹Û2–üºÎÿ^Nà•ú.ÉÏÛË/¿%ÿüóÏ£×uÖ «®ºj‹S¶ví0ç»ì²K¼Ö‹ 1Ö87SXx¿kd7Þx# ‘ëüæ›o޳Î:kdÏu–Ú-÷ýXnþ+½¦²¡òxIñêsâNïrÈ!EC4[/ßqûí·_ü^Okšï„ÞX®•,KàãÁwgö;Œï ŽË[~^87¡éÜ`仂ïŸüß—rkÎ÷%  H@M !bjË-·Œ›€óÏ??z’ø£Œ!ú;ì°¸™Bx¼ñÆáì³ÏŽÇ±±ÏÚŸþÖ^{í°Ï>û„e—]6Œ5ÖXe¡£lzñ\ÑBM ››B6P„|r›56ÝÝ»wxCØt²IŸk®¹Â'Ÿ|7Þlf–[n¹Ø=6ã?~Üè°ÑflÏ=÷\Žœ—\86aˆ$lÊõ+;n6ªl¦é#IúÈÆ”ßÙ˜±Á¤=6IlháË¿dò“_d“ϸéb‡MÝsÌç‡ãƒUÓOX¾úê«Q0öìÙ3|ûí·Qø.¶Øba§vŠíUºe#LÈÁºñÆc0‡1”xCÄ:s8öØcÇù^zé¥cnb%c.&ðØÈ²î™Ü, $”Ì;k˜1ŒyÿᇎycÌÙL3ÍÔbÓÏFŸ+bc“hDô¦ùäqS–Œ;ä„^sÍ5qÃÎæ( ¼b}fCÊ4¢yà 7„«®ºª°™B8RÀqTm?‹yOTˆDÎæ­R‡wo.ÆFš¹@ð!ðË <¼¦¯¿þzÜäå7‰ô§Ô˜9_1—_6›nºiÀóÂ:%ÄïþûïÞZŒ 1ýd³ÉËZ¾ï»îºk\›llÙ€“'¹ÁBj¹APÉÜÓ_<ÜI–=}{â‰'â6óÉÊ]KåÎ79[D©ÜXŠ^L¹Ë ¼Rß%ÙyÃcÉ÷žWn6”²b×N)—DŸ›Þ#x±¾’gŸ×ñþsÍss‚k¸ØZ(5¦rsÀ¹*½¦’À£¸ß%ÜDKE‰JåàU2^n˜!^_|ñÀ-æ1Y–e)Ìáþ­åàÑ^±yÉ~W°Î¹vŠyÿ*Y# H@è !ð²¡A„îqÄñ0ž¼EaBÉØäâMã.pÞÊmJ³›¼7ß|³pG=Û>¡R¼aÅD &†§²y¯dî‹qOŒ¸qB_¸©‘ ‹¥O宥rç&®š±Tò…YNàµö]‚Ïsà›æO7ùbV­Àãæ ×sö;…yå{¯fVDãIäF×á±k®¹f˜qÆc7Š…~f¿S¹9àÚ¯öšâ†ýäÆK²R¯’ñòÍú⻑ Ùj¶YQVŠG[^©5QÉšó H@@GhXÇæ‹ /„TòÈ%Àl’Šå³”Û”f7y„{ra¾º"ïb›BΟÝ0!¨6Ȇ¯ ^Úë„'Ë瘔x•ô+»Ð8/Â&›?ôöÛoGQ( ¿¨=a–Œ±Ú~)ÇŒ°FÂé3• Ëåàå­AN#á£rvxjñ¤"èåôPÔÖ^3Ÿ­TàqÜ"‹,§ A‰ ï›nòH »,fxöð#ðØh#kem“‰gŒPJ¬’¹/%ðXãx²Ø½lØs¹k©’sW3–J¾<Û"ðø.)®ÍùðFãbÞ¹)Ã÷AÞªx#Ιõ˜þì ¼MUûÇDDˆL•’R*$i¢PBsQѤ‘R’ÒH¥¢D…RJ#JI3Íå¯I)4ÉÐ$™Sü÷w½ïºï¾»sï9çºÃ9çþÖçããÞsö^{­ïZ{ßç·žg=Û×Í^ë?gβ¨E÷2 ¹ <ß§DÆ Ù{Ê{±n¾ä&ðé/!Á,jù0ï°ç:ú|äš±xä§Àóü™s:FD@D@ ›@F <Œ2¿7.–á v<£4lì’ÞŸ$ZgN<Ÿ„ð6DF޽ºõÖ[]R–¼ ¼DÚn'!©ìE0`@ÖÇìjx ÙOàÅzGX<±“l;c±Ä&$Œ.J4¼¯IA¼‡2–ÑKòöFËB@"/̾³XÀùñúÌyÉ <=aa9e0Î9Â1vˆCƱKx',$? {J"cŸ›À#¹ öðáÃÝž+ö‚ùï^JäÚÉô%‘‡g~ <MB(Ñáp>ÿ]²…¢˜Ó>Œ8‘¾q Þ3æ ቼDÆ Ù{Š×B°€ ƒf>ÄÚƒ—H¹·ˆzÀ»·’°SÿÎXÏó ó`a–Û{9ã%¿ñP ¼Dg¤Ž( /ð¼ñ@ˆûÞÈÖFFBBÊ€ÑÏ(e¿ !•*x/ð‘ÕUsC$!¥$@Á ÄD ²úÍ¢Iø,s„øYµj•3i;{Ù'‰ˆõž2BÄÇC¤#R1ÆIpCöÂDúœ¬À#”•ù[æ x c%‰`¬˜ƒì'$éž>„{“XHÀˆXôa|‰Œ}nÏ{BɈ=ŸI“vDï¥è½“ȵí ãGÈ5!Š„¥æTòKà1ñ–ÑG¼j$AT‘Í7OàEçÞa[G$‘·páB·èÝïG4Þ1öYâÁf#´܉¼DÆ Þ=m?íáþ!ÄÔ'‰áù@h6Ï´è‹Îãõ÷•W^qd}â*¼ìDfø=ÌaQ–Ä÷ûy>E“l1g$ðŠÂ Ñ5E@D@ò›@Æ <€±¢ŒáEh{¥Ø3ƒx ïqñ`ã kÏ=÷œMœ8q«QiÞV#Ty ˆ¹õÕWÛ“O>i*T°Ï>ûÌn¾ùf6l˜Õ­[7W ÷Ôï¿ÿn}ûöp>ñÔSOÙK/½dcÆŒ±m¶Ù&éš7oÞl'œp‚=üðÃV­Zµ¬óO=õTëÞ½»µjÕ*é:Óå„뮻ζß~{ëׯŸkr¬gfºô%^;çÛo¿ÝæÌ™c;ì°ƒ]vÙevÛm·è=¯M9}½óZμHiÇrÄ]º ¼×_Ý^yåëÖ­›íºë® Ï‹/¾hü±]sÍ5 Ÿ=a\µjUëÔ©SÂuÜqÇvØa‡¹…U&Ožl¿ýö›õêÕ+î%-ZäÄýÏ?ÿlåË—·ãŽ;ÎvÜqG=ztÖ¹'žx¢yä‘që*Œrâ™·téR›4i’uíÚÕªT©RïJÑq IDAT Í¿ï¾ûlÆ ….ð>øàûðígÏžyê×êÕ«íŒ3Î(–€råÊY»ví»XÏÌÜÚ¶m›/7ž‰6Ö33/*ì1L¤O?ý´Í˜1Ã|ðÁDÏ—còÊ!Ùû7_«JD@D@Dà¿RVàÝy熱âKéÒ¥mÚ´iFˆæóÏ?o½{÷¶ûï¿ß0¤4h`ýû÷·Š+fϱÏ<óŒ­\¹ÒêׯïDï%s˜…5j¸ûàÓO?µæÍ›ÛøñãmÏ=÷´¡C‡ÚÆÌâÂ?ÿücÍš53DÎvÛmç˜ñ=žÀ×^{Íð|5nÜØyÎ`c_š6mêBß(,¨ð9[¯^=»à‚ ²½æPHÂIñ®2o'Ä6Â*ìAæ¾¥ÝQ=vÍïŸ|ò‰sÌ1®­xl÷Ûo¿Ýã´~p­pÁsºÏ>ûíºøâ‹máÂ…î8<]çw^6ÁG{aúÝwßÙN;ídçž{nL6üyÆ&;räH[±b…›’%KºçRNÏÚ€øÄû´`Áã9vúé§žf_¸ï÷ÞzËy¥š4iâBKñÀS¸Ö=÷Üc_|ñ…[Ð8âˆ#ìœsÎq ,zàÍ&D3§g&u@ë·ß~ëæmhݺuV¶v ©ˆqzàܵ(-[¶tgÚÉszìØ±n\æFçÎJ¼1öíì³Ïvã¾'¨&,ý„túrË-·Ø_ýe×^{­›ËeÊ”q_1vS¦LqóÌ·#/÷U¼û7ÛÕ/" " "PÀRVàaœò‡‘‡Ã;1€!€A¶÷Þ{;ƒˆ?Þxn0Þ(Ƭô"0–§OŸnï¼óŽ3>0È|É‹ÀcEwÖ¬Y6{öl[·n]V]ìƒÁûõå—_ºë„ËW\aµjÕrýñÇvà 7ÄVŒ®C=Ôðà}þùçn_ÆÈŸþéD*â ‹/va‰aïßÌ™3íý÷ßw W¹re·§ £ #ƒîàƒþ×µá€8À¸E(PØëÂÏ¢þ?þø£;wíÚµî^hZ‡ñÌÞ- Ç6mÚ8cÜúDÆ|¸ûg`xì5{ì±Ç\[è³ÂÑFcèú¾úïrx9µãšPXD í¦}Ì5® 7æ B‡y†€ÁÈ&Œ‘¾"®`õœÅânTà1'0è¹>†; ƒ¹ ¼'žxÂÑå—_îöa1Öô…¶RÞ8î„€¹Ë÷Œç%—\âÂÜÞ}÷]'¤˜+Ì_Â?/½ôRwŸPí`Üï:uê84L0¸ñ¦RÌ/Œyæ/so ó8,æðXÓGîÛp‰%ð8@x3†9×?ú裳îq>FcB»èá›p€[b®Ò'¿¯°BÐRÏ]wÝåî-žáâ6—Å¥ÝvÛÍ]'Þ³†6ÀþœËœ´skÇ/:íß}÷ÝÝøÂÿ½÷ÞsŸùÌ|C¸²àıD 0ŸðRS¼ðÏiÌé¬yöŽ7ÎÍmDyXàq¿Pó˜… æ*ûJé;×b|xÉÞW‰Ü¿1øúPD@D@ ˆ@Ê <ú‹!Iò…è<þ?òÈ#Y«¿+ q %„¢êðÃwØ0O«ð ¦ó"ðXeg_F!«À¬Jcdxà.ùu®Y³Æ]÷§Ÿ~rm < :Æ B°OŸ>Y‰!0â0H1Œ¹õuÔQö믿:ƒ# -QÇõ¾7[ü‹åÃDchz±{DýÄ;@Ƙ¤NúŒ'c1†‘ˆ Âð|óÍ7QŽ1KAàq I`´ÿþû;/ 5,ðW·ˆ/=/&b#—VÚ %ÍIàáQÈ­xR:è —Œïuc3÷0ì1\áCÿ0Š1ºY±ÿå—_b†ÿÅâ™›ÀCpP? Þ³ÝÃ˃‡§¶oÈSŸÿì›o¾qó‘…7È ùp»;ˆAŸdÅßKŒ'žQ_0˜aÀÿæ%Âo™/Æ‘yÂXbÔs_’H%êI%ð˜ïÞ«Bˆ¼2Þ«n7ó‘+É m8ùä“ÝáŒW—.]Ì{ø®¼òJwááôq€÷ïÌ3ÏÌöÈõü‡ âD,%‘gM¬D/ÜGÌ1„žåãžò\x>ÐnÚÚ¡C'bxn!’¢%,ðø.Ö3“ñGø2ö¾úÎâÑ£>êîíC?‡™“xìÂ…qá™É³Ð'Ñ™?¾»ÇØ÷Éó‰9oÌé›xÔ½'8Ÿ….XRxv „Y„‹<‰¼dï+ž-ñîß œ>$–/šEÃã¡ç ˜áÕðÑ…‹@ò%/aÉy„]Q?!¼Ñòý÷ß»•ê°ÀóÇÄiþ;ž÷Æy#ƒÕq ±D^8É "ƒœð۱Î5YÇÈ gDÐÑD1Þ<Ä'ßc$c(aœñ9Ï‚º_Ã;ây ð0˜N;í´˜¡o^à²E;XñGF ºcíYôÇæ$ðⵓ1ÅC€Qøa®à Àp£'t’‘0ãƒ1‹Ç+§‹gøØ¨QQLû}IDàEïŒv_xÞcäÒ2Ub„rF}¬x„âiÅ3Fø¢/ÚxÃ0Ü)9e«$‚•:˜K\¾ÑKàåvGÏÏMà…³hâ ÄàgNã±cñƒæ…¸Âsލ —¨˜à»Dž5±Ø°°Ãbb-B+,f©›±`ŽñìàÙ†°­Y³¦k[‹-²·xÌ_8”ÈÏŒ{“z·v Nxq¹?£…ïx&àeõÅ‹/Ä+‘±²#‡ŸëœOà±hÆâìðØr¿" Y8e¢/ÙûŠáx÷oŽ }!" " @ cû&„á`ñs4Û äýžXæEàšI˜ž$ê"œ …u ÑÖ¼hÍpV̼<Ú…§ qrÊ)§d5“ð¢o¼Ñ ïõá˰ Â(gOaQèD !ñ’†#ÞUúí9/ð0PÙ{-<„^= pêó—ð±[#ðⵓýjZ®KÈ"‚cƒ’ÿœä·ß~Ûy\ðÞ!ža‚±Ñ̉g¸/Q‡‘[ïãØ¼‡¨Åû‡—Õöz"†Q„Jç$ðCÚ‹P‹8­XïhÞñÜ^xÌã <êd±„E(îC„+¡Þ5rMATÓÎx{ð¢/Þ}•Èýû¯I¯D@D@D  ¤´ÀÃ;Æãð;ïrZéõ†{Äðù•áÜØå$ðX]Æcê,û¬Âo4⣟/íÛ·wz´ä&ð7cå×ïç Ÿ+‹fXày±Cx§ýûÜÌð¼°…€À¨e/“/¬r³òM¦Ïp ¢c4ì"ÄC“½4$zÀ«Hè—ßûÈùìaòû±ø=‡áÅ>) 2úÉ~8ÂäÂ%‡@ÁîuÄk'aŒ„"Öð8±g1JXY,ï+ `„„á© ·5'žá¾D2Brñ¦x­7FsÛƒ5D9‡ñÇCö¸ùkûE¯?Ú.ïÁà '$×ïÑeŒã•X/‘gM´ <[ðòÜ Í^ÈãõõɃ¸W Ñd/ãpá;D ÷‹Q뙉·°æ°w…¸ãÍåÙµµcÈÜeá Á^D£í<¿ýwþ9齟$"2!Þszâ ½o´',ð£„tQ' è+ì$(¡`l²?…ë“” ZØ«„˜á;¿S`e V·1B÷Úk/g¬bÈaÀò;%Çø`ìRØCƒ7 £;:—ˆÀCjI8({À` “xí¤ˆ_1†.£}cxJðÐab€³·O0^TBÃ/OÏ7*ðÈ0ˆÀ&–9J6B¼<$CIFà±à€OûÙW†È#ƒ$s AÎ÷pE˜poÐ_D;‹$‹ÁPÅCM#B—{Ž>r/!†sD/‹à÷Yæ&ðNêÊ5ÙŒU¶VàáqEˆ²Çï=pñžÄþ<ÄY¤~/¤oo,Çwñž5´… ÿ#˜[xþÙsǘ°7Ž9N²Âõ?ŒDzˆÄ~Aîa„5cÄ}x±ž™Ü—aÿ¼%Ã¥¿—¹y6òa±ŒÏO¼—,2x¯u"ÏõxÇß ØðÌãÞg.xÁÉ<¡m$"<VÓƒÄÀb?ÆçS0 3âᒛϋ `^倘ÁðÀcOàq.{ô0N0 7ÃØ¦ÏaG–CŒŒ|Bôé¾ð9+ØE>ùA¸ýˆ † ŒI„¢ÇÀ´o'}à„!ª}IDà…_tΘÞ9Ä“LDà‘†}h$Âs1h½8×N’`¤²˜€!ˆðE”à%¥0îˆÄ4Ì©ŸpR„S¢<ýq±^“€¨õéݦ0GŒ%#ð¨¯^„ ‚#ï¨ß{Jßr\~`tãeà fA´¯$ ü½Ä<å{’³Ð¶ðç&“&2sF÷~z[+ð¨Ç IîI1¬ñçñú(ãÊÂ÷ý /.p\N/Þ³†6ÐoÎÇÃÄxŒ&œi–1cL3ÿšI¸Ÿ¨Ÿ½‹xæx]óŽköÕQ¢/Ö3“s3,Rà­g¬}»ülíRÏ;¼´Üó<ÛxyŽ.Y²Ä=7yNãýåYÉwþ9’¾3—yž„3qà‘•™gsŸö±p…×/7—È}ïþÍö‡A¿ˆ€ˆ€ˆ@HiWÀ}Ozž÷аêí 4 JÂ#1\JéT0l1¶bíyK§~¤J[å™È‹ÎS¥O[Û2{ò øÀÎ8ã kÑ¢…l·ß~»{ì±vñÅ»s¯ºê*[·nõìÙÓÊ–-k?ü°}ûí·6jÔ('ûôéc5rB²qãF;óÌ3mÈ!vÀm@<öíÛ×vÞyg›>}º½óÎ;öÀXÉ’%5“D@D@D@D@D@D e¤´À‹RzüñÇmÖ¬Yöàƒf j{ÖYgÙW\a‡~¸û~óæÍvúé§^¾}÷Ý7eR ”xˆ©·ß~ÛæÌ™c_ýµýñÇV¦L'Þ(±²h¾ûî»îsŽ™9s¦M:Õ&Mš”m¤¯¹æ«\¹²n¿ÿþ»|÷Üsí±ÇvË-·XÅŠí’K.±ùóçgeè$dÓ—5kÖu4kÖL3HD@D@D@D@D@R†@J <„ÚòåË­S§NÖ¸qc'öð´M™2%GGø$Þ5öÌ=ÿüónÿÜ#<’ øàÁƒˆ»òÊ+ÝçtÞ8®Ó±cG'÷Ûo?ûâ‹/œd^•*U²Õ@ôûüRf4ÕbM ežO€2zôh«[·®¤_|Ñ%LÉMàq<¡™ãƳ¹sçÚÕW_í~Þe—]\ì±Ãc×¹sg¢Iy饗\˜æ\`wÝu—Mœ8Ñíù#± ¢¤*­[·.ÖEH})+ðV­Zåö¿!ÆÈŒùùçŸ;áE&̰ÀÃËvþùç[ƒ œ°3fŒË~I¢B<ñα—.œdeÑ¢EnŸIV(«W¯¶³Ï>Û%U!;f×®]³Fn„ ÎHÈfÆ í—_~± ØÉ'Ÿl´±[·nÖ¾}{w¾Šˆ€ˆ€ˆ€ˆ€ˆ€%”x@yõÕW]xåŸþiÍ›7·#<Ò†žMàáÝ[±b…KŽBØ$0?þø,¦dÐ$) ûøükeÕªUËÆ}èСöÞ{ïÙÝwßmõêÕËúŽLœ$byá…œ¸«Y³¦µlÙÒ…r"ð~íÚµsž>(J)-ðŠŒ®-" " " " " éF@/ÝFLíHàijˆ€ˆ€ˆ€ˆ€ˆ€ˆ@†ÀËT7D@D@D@D@D@D@Os@D@D@D@D@D@2„€^† ¤º!" " " " " xš" " " " " "!$ð2d Õ ÀÓ ! —!©nˆ€ˆ€ˆ€ˆ€ˆ€ˆ€žæ€ˆ€ˆ€ˆ€ˆ€ˆ€d ¼ HuCD@D@D@D@D@$ð4D@D@D@D@D@D CHàeÈ@ª" " " " " " §9 " " " " " B@/CRÝ <ÍÈx2ꆈ€ˆ€ˆ€ˆ€ˆ€Hàiˆ€ˆ€ˆ€ˆ€ˆ€ˆ@†ÀËT7D@D@D@D@D@D@Os@D@D@D@D@D@2„€^† ¤º!" " " " " xš" " " " " "!$ð2d Õ ÀÓ ! —!©nˆ€ˆ€ˆ€ˆ€ˆ€ˆ€žæ€ˆ€ˆ€ˆ€ˆ€ˆ€d ¼ HuCD@D@D@D@D@$ð4D@D@D@D@D@D CHàeÈ@ª" " " " " " §9 " " " " " B@/—ܲe‹ÝrË-öþûïÛ!‡b °’%KfÈЫ" " " " " ™F ¥ÞSO=e>ø`ŽÌ>ø`:thÉ/¿üb]ºtɪ„ V£F»^ºWüúë¯Ûm·ÝæºÁ¸í´ÓNéÞ%µ_D@D@D@D@ÒŠ€^.õyóf»þúëíÃ?´ƒ:È®½öZyðrá5eÊ?~|±xëÖ­³'žxÂÎ?ÿü´z䥱ï½÷ž•*UÊÝ*" " " " EO mÞhûï¿6bµjÕ²æÍ›8EB5K”(‘Ðu…Å5ŒsôèÑöüóÏ{7iÒ$Û¸q£]xá… Í™Dš8q¢ýþûïÖ·o߬Ãñp¿ôÒK6fÌÛf›m©&_Ž9õÔS­{÷îÖªU+ûæ›oløðá® ÅuÞç TU"" " " ùD m¡’:uʱÛC† ±>øÀöÞ{o8p ó$Í;×Ú 6´Ë.»ÌªU«fá0ÂN8Ázõê•UçäÉ“íá‡v¿ã­;ì°Ã줓Nru`¼¾ð î;:Šè{öÙgí±Ç³™3gÚªU«Üù\/­Ù³g¡ž;ì°ƒ£;v´Š+f3Ò}*õ<óÌ3öÆoØüa»ì²‹õèÑÃ5jäŽÿú믭OŸ>îg<‹Ë–-³©S§ÚÏ?ÿl;qÆÖºuk{òÉ'íÅ_t‚`÷Ýw·K/½ÔêÕ«—ÝO?ýd>ú¨c´aëS§ŽvÚivøá‡g—(Ó¥K—:á±zõjC ‡ ý=÷Üsÿ5náðÛxýædêž1c†½ûî»FÛÿúë/«]»¶~úévÔQGåÈóñÇ·7ß|Ó}D˜ÀæÔE=M›6µÞ½{[¥J•²ê¡\K–,± *¸}˜ô%|\´cÿý·uîÜÙ»UªTɧÛÔì¾ûîsãxÌw¼Ë={öÌ·ë$RQXàq<{SO9åÇGED@D@D@D h dœÀÓAÈ¢,\ü~½õë×Û™gži›6m2<€=ôPÖaW\q…}ñÅV¶lYbW¦L™\'">þøcW‡ÐëׯŸ"ÑRµjU»ýöÛ³öò……NùòåmíÚµÙNA >òÈ#®-a·ýöÛ;Ñ-ˆžü1ÛÇ줟ÞÃB¯¹æ'¢!ܦM÷±xñ˜r½‹/¾8æLNDàÅë7b«[·nÿê—¿ ãè£v¿†yÆb„Pƒ%â7\Z¶liW^y¥ûˆëÝtÓM6gΜõ q8jÔ(ÛvÛmcö÷wÞ±çž{.k/b̃òðáˆ#ܼ ¼ã8GXQ(Z)𼘭÷Êñ3F{ݺuq{ë­·:ò„/bÀ¿ýöÛÎh¦`#ò(ñõÝu×]Y{ þüóOJŠPÄËCØ'F=¿cH{Á…Á·-,HÎ:ë,gLS0Ò9—‚÷Šv„=xa‘8}úô,ƒè#Ürÿý÷;@Áóƒ¡ÈDàRw„uR>ÿüó,â“úÃ/Sê TpñâÅ®¾xY4“é7õÁ‚(CœþðÃvã7f‰¶iÓ¦9Á”S½aÑM¯ë ·/'^]ÿb—ýtÞ[Ǽ˜7ožÊðˆU.¿ür'þÞ”•+WÚ%—\âÄÂïîÝwßíD¼÷x!ÔÆggŸ}¶;ÉÂÄŠ+ìž{îq¢ëºë®s”ù „|TàѶ«¯¾Ú P¿ pçwºßÛµkçÚƒÀc>ÒN# ¦ü£¿x6¹&‚o·Ýv³W^yÅ…ü²R½zõ¬:±!D“ù¦,³1§…>B#6ý=ɾ`tûPB> ‹’>4hÐÀësÂ41¶ÙÅž3ŒbŒn<ˆ<}x6(ñ‚ aæ ž%¼W”=öØÃyµ|ÁHÇSCñ^¹° ï3ŒõyXàaœß|óÍ®®×^{Í%º `Èû=Yíx½(„Pž|òÉÎËÂër+>ä1¦Ô—W¯ßÔ÷ ¡A_Ùs-ô”O„^@ Âêˆ#Žp?÷ïßß>ûì3÷3c…`?ñĨʩàñ|àb~ˆ‡ß>ûìã¾G¨#’>ŠŸ#xûz±SEà!ÄÙHØ!ÿ¥ô{é|r›Âxˆ@ÂåÇþN< x¿(Ì%9„–/„är!š\?žÀÃûHZDž/ì ¤O9 <<’Ô‹¨g¼UD@D@D@D@ŠŽ@Ú¼Xˆ*W®œ•ˆ$Y1B}$U!¹Š/Ÿ€Ä–G" Ââ|ÌpÛÙ'Æ~'ŸŒ"¡“<ÚBè¼¢¯’à»}÷Ý×eœ¤$Ë”½láPDê ¬¯R´$ÓoÂ0ñEÛ~½B~ <öáÑòû%£mÇÞþž\™g#m§ÿd¾ä3„5û=ñŒ±÷“‚cOûBÙH"ž‘#GºkxAGÒ2³2?ÉZÊü‰ <Þiˆ0‹&Ya\Nâ̇hzû?úè#×6¾ã,LèÇ×ÁžSÆ•öò.Hæ9‰}Ø#Éï*" " " " EG X <ï%ñø1R½Gpkç’o Æ1Y£xu0ŽÃ/ÊNFèä—À£}¼¼ïÑA›x«üþ±dõ’¨†>-_¾Ü`x/}Hexš'ÓoÎûH¦I²gúôÿþ5|ŸŸúyìC#1 ×äw¸à•ä•þÑ[—9Å«|¬gG¯#, }ä5 5/ðH„B½œ‹w™,–3„"=æÉP/ŽåùãÅçxý1Å›‡g-<§ãyð Eà³@h*I„Hƒ@õ1JÖRƃ± Í¼rÁ‡9ÝãLWHi§át#€×‹X^ò¹õ¡†À‹—Ý4UYàí#±‹ÏLšªíT»D@D@D@D 8À+£¬>*³°ß ¡—H‰å‰Kä¼T8O!a£ZÿˆTh—Ú " " " Å•€^qyõ»ÀæHȨa¼ ¥³Àc_ ¡²Í›7×M}/" " " "P$ð ².!" " " " " …A@¯0(ë" " " " " "P$ð ².!" " " " " …A@¯0(ë" " " " " "P$ð ².!" " " " " …A@¯0(ë" " " " " "P$ð ².!" " " " " …A@¯0(ë" " " " " "P$ð ².!" " " " " …A@¯0(ë" " " " " "PRVà­Y³ÆfÏžmß}÷]!`Ð%D@D@D `Ô©SÇŽ<òH+_¾|Á\@µŠ€ˆ€ˆ@ˆ@Ê ¼©S§Ú®»îjM›6Õ€‰€ˆ€ˆ@Úøè£lÙ²ev '¤mÔpô!²ï¾û={¦IµTD@D@bøûï¿íþûï·=zˆˆ€ˆ€8 ¼G¬ ˆ€ˆ€w£G–À+î“@ýB" WH uâK@¯øŽ½z." …M@¯°‰ëz" " ÅŽ€^±ruXD@ŠŒ€^‘¡×…E@D@Š  ¼â2Ò꧈€= ¼¢µ@D@D à Hàeø«{" "B$ðRh0ÔÌ$ —™ãª^‰€ˆ@*ÀKÅQQ›D@D@2Š€^F §:#" )M@/¥‡GÈx™0Šêƒˆ€¤ ¼ô'µRD@D Hà¥ñà©é" "f$ðÒlÀÔ\ô# —~c¦‹€ˆ@ºÀKבS»E@D@Ò†€^Ú •*" iO@/í‡PHux©>BjŸˆ€d ¼ÌKõDD@D E Hà¥èÀ¨Y" "$ð2pPÕ%Ô" —Zã¡Öˆ€ˆ@&ÀËÇÑ]µj•]zé¥V®\9»ûî»mÛm·ÍÇÚU•ˆ€ˆ@ºÀKבS»E@D ýHàåã˜}ÿý÷vñÅ[É’%íÑGµ*UªäcíªJD@D ] Hà¥ëÈ©Ý" "~$ðòyÌ>ýôSç¹Û{ï½ó¹fU'" "®$ðÒuäÔnH?xé7fj±ˆ€ˆ@šÀK³SsE@D Hà¥ñà©é" " éA@/=ÆI­L ïÉ'Ÿ´xÀ&Ožl_~ù¥=ñĶxñb«X±¢~øávÞyçÙvÛmçÆ«W¯^¶hÑ"{üñÇ­R¥JÙÆðÇ´®]»ZÛ¶m­OŸ>öõ×_»¤)W]u•í¶Ûn6~üx#s›m¶±† Ú\`µk×ÎVG‡ÜÞ» &dÂüPD@D@ò€^>@T" "  È(·Ï>ûØÂ… ­yóæ¶Ã;ûî»ïÜ~¸‘#Gºä'Ó§O·Q£F9¡×¾}ûl¦L™b>ø ÝrË-vÀd ¼zõêÙ·ß~k5²:uê¸:çÎkÛo¿½ñG»FYõHà%4ïtˆ€+xÅj¸ÕY(R%ðrW_}u–àÚ¼y³]sÍ5öñÇ[¿~ý¬uëÖ¶zõjëÔ©“íµ×^vÇwdƒß·o_[²d‰ó"½¯råÊ6hÐ Ûÿý³Žì±Çlâĉ֪U+»âŠ+$ðŠtëâ" "Ú$ðR{|Ô:È$%ðú÷ïoÇsL¶ñùüóϸ;ôÐCmèСnºÉÞ|óM{ä‘G¬zõêî3Þawæ™gÚñÇïÂ3)^àµiÓÆ.¿üòlõnÚ´ÉN=õT+]º´"Š ¤Èƒ—I·‡ú"" ùC@/8ªø2^àýõ×_.“½rìÓ£àÑÃ#Ç:DeæÌ™Î£wóÍ7[“&Mâ <èÝ»·-X°ÀyüðòIàÅŸp:BD@Š# ¼â8ê곈€ Œx`Å+WµjU›4i’£Lèf—.]ܺûî»Ï}6dÈ›?~Vx&ŸåæÁã{<†Ÿ|ò‰ Õ¬Y³¦^ÑÌa]UD@Rž€^Ê‘(" C ãÞÊ•+­cÇŽnÏÝ=÷Ü“5pˆ2öÑ7ÎjÕªåÂ- ïôᙉ<2n’ysÚ´iîåæ…hf̽¡Žˆ€ˆ@¾ÀË7”ªHD@D Œx>ôòä“O¶îÝ»gáXºt©þùvÎ9çØ~ûíç^…ÏŒ'ð–-[æ^¿P·nÝ,/ žî7X$ð4/D@D@ ‹@F <’¡°/Ž÷ÔQ~ýõW»ì²Ëì·ß~³1cƸwÙ… Ù/ɪÉ+^}õU÷n¼R¥JeâC4yM°aòöÙ‘`…Nöòáñã½y¾ÈƒWXSW×ô! —>c¥–Š€ˆ@ºÈ(W¶lY«P¡‚ldµœ3gŽ­]»Ö.ºè";í´Óþ5V³fͲ#F¸½xGqD¶ðLöz)x {¿Â ™9¯»î:+Q¢„^ºß j¿ˆ€  ¼„«ªE@D@²È(7`ÀûùçŸí…^p¯=Ø}÷Ý]–ÌfÍšÅö7ºïׯ_ÿ¯ð̰À# 'ïÀcÏ{îxµ‰[Ø·ç_à/ žî0( <Í Â"QoàÀÖ¢E‹¤ØõêÕËV¬Xñ¯ð̰À#3œ|%© è`bO@¯ØOB#Q/Ö‹Îs#‰°;÷Üs,ݺuûסñ^“Ph£¤ ‰€ˆ€¤5 ¼´>5^D@ÒŠ@±x[¶lqa™³g϶‡~تU«&—VSWô! —>c¥–Š€ˆ@º(vä(Ó§Ow T>ÿüs÷Ž<^—«Èƒ—îÓ[íÔ —ã Vˆ€ˆ@q Pì™3ïºë.«T©’{)ùgœ‘- fxÐ%ðŠÃ- >Š€ˆ@ÁÀ+xƺ‚ˆ€ˆÀd„ÀÓ`Š€ˆ€ˆ@*ÀKåÑQÛD@D ³HàeÖxª7" " )H@/EM % —¡«n‰€ˆ€¤ ¼Ô µDD@2€^¦°ú'" "Pä$ðŠ|Ô(6$ðŠÍP«£" " EE@¯¨Èëº" "PüHà¿1WE@D@ ™€^!×åD@D À+ƃ¯®‹€ˆ€ ¼Âᬫˆ€ˆ€è5 š" " "Pà$ð ±. " "ð_òài*ˆ€ˆ€ˆ@À+`Àª^D@D ‹€ž&ƒˆ€ˆ€0 ¼¬êE@D@$ð4D@D@D 0üóÏ?6fÌëÑ£Ga\N×(æRÖƒ7mÚ4Ûu×]­I“&Å|ˆÔ}t&ðá‡Ú²eˬ]»véÜ µ]D@D M¤¬À[·n½þúëöÃ?¤ J5SD@D@þM`÷Ýw·#Ž8ÂÊ—//<" " "PàRVàÑó’%K8]@D@D@ ’ÀæÍ› ²zÕ-" " Ù¤´ÀÓX‰€ˆ€ˆ€ˆ€ˆ€ˆ€$N@/qV:RD@D@D@D@D@Rš€^J'" " " " " ‰ÀKœ•Ž”& —ÒãƉ€ˆ€ˆ€ˆ€ˆ€ˆ@â$ðg¥#E@D@D@D@D@D ¥ Hà¥ôð¨q" " " " " "8 ¼ÄYéHHix)=k“'O¶þùÇn¹å«W¯^V¯7oÞl'œp‚=üðÃV­Z5÷ù¢E‹ „åûßýn7ÍúÚæ,þÍþú{³5©]Ù†WßZîõŸëŽxí{ðÝïíÓÇXéR%ìŸÍ[¬ÌåSíûëÚØ.•Ëeµ¹Ém¯Û ûÕ°NØ7Ûèõ˜2ÏýºÖ^¾¤y£Zeàóvç)ì܃wMøl|ëköÍ/klÅm­Â¶Û$|îàæÛò?7Ú¸Ndsâ¸÷¬Êv¥müÙM®']œ½èWë8áC{±G3k´S¥tivRíÌiž†+™8q¢ýþûïÖ·o߬¹/Ÿ{î9㻼–XõFëêׯŸ»Ç[¶l™õÕõ×_oåË—7¾+Œrê©§Z÷îÝ­U«V…q9]CD@D@Dà¿RVàýùçŸNÀQfÏžíÄʽ÷Þ›5pºï¾û®@DIºÏŽuëÖÙi§f_|±5kÖÌvÜqG+Y²dV·V¯^mgœqF ¼©Ÿ-³3Ç`'7ÚÉ.8t7Û!4¯,øÅ&Ï]bô;ڶݦ¤Íøb¹Í˜¿ÂF¾¿kßïëþ²jƒf¤œÀ[øËZ;øö×m§JåìÚ@ žqÀÎ O“>Ojëþú'›Àþê7¶}Ùm¬{óÝ®']Dp_÷â—6üÄV³bÙtivRíÌiž†+¹ï¾ûlÆ ù.ðbÕm|=ìôÓO—ÀKjTu°ˆ€ˆ€d”xa¼/½ô’3Æ­|‡KAyÒ}h¿ùæëÝ»·=òÈ#NÜEËòåËíüóÏ/P·fãß¶ûЙ֩im»ûÔFÙš°e‹Y‰±)ûÛZÛó†Y.ðF¼.Ixðn ÞȺ;–·~_gOœpÂÓä¼IÞÉ’Ù^Â'ëÀ”$Ó< 7vĈVªT© ¼©S§º¨ƒ¼–XõFëêÒ¥‹wÞyE.ðšÇ{l^»ªóD@D@D@ò@ #ÞwÜa=ö˜}öÙgV³fM‚Iœ;w®3¨ðøí´ÓNvî¹çÚa‡ö/\Æ+®¸Ân¿ýv'(Jx Ãõ­ZµÊžzê){ûí·í×_µÚµk;1µ÷Þ{»úÁúä“Oì¨£Ž²I“&¹c9äw m|å•W¬téÒN`… D+óœ[¡Bkß¾½ó²•ˆ¡„–.]jcÇŽuÇ–+WÎÕÓ¹sgWïk¯½fÇÏê[Æ í¶ÛnËú=ú=_À¯L™2Κ_,'~ðƒuâÿlù°¶V©\é§æ]o.²»ÞXd‹‡´¶G?úѺb(\Þ¾ìH;´NK4D³ÙoÚQ{îh7·ß/«šêGðæûÙ…‘Bˆæà6õíãÿ°iŸ/ <‹e슖õìÒ#ëæØÎÂzÏ ážÀ;åÁ÷ƒ0ͬléÿyEX¹ÎúOýÜ^þêgWÇÙÖ¶ÛÖþAX'-_ÚÔ¯a3ºæê¨xðÂ!š÷¼µØîyk‘-]µÞšÖÞ!èþÖl÷ªYçÒîÉçd“?^bÓÏgù2¥lX²ê…êÆ ¶÷SŸØsç”iÓbÏj® µwø_¨«¯ì•?Û ççÛËþ´=‚>QO‡†µÜ׌Ék_ÿbªmC_úÊ Úæuw´G:7µªåËXçG>²?Öo²éÿïúté*7Fßic¿­ýËšŽxÝ]ÛÚêTÙÎæ-Ye-FͶYAíEÿŸ}Ô÷ËM'ØúMÿ¸6Lù¿%FÛ[ï]Ý…ÎîTé?^?Î;êî·ì­>GZßg?³Xiuª–·‡Îj𩜭­›ì´õK[òÇzk·_MsævCÐöG>üÑyŠol÷?Nœˆ@ëóÌgöÆ7¿Xå`~^rx]pì^Ž[¼ëæ6O=ß .¸À–-[–5vM›6µaƹçÃóÏ?ïž÷ß¿q/7hÐÀú÷ïo+VtÇ/^¼Ø=cæÍ›gk×®µý÷ßß=ƒ*Uªd9Õë/ôË/¿â.\xÖð,!D“:êÖ­kS¦L±õë×;ˆóÏ™džCï¾û®MŸ>Ý,XàÎ'ó¢‹.ÊŠ Dóì³ÏvÏQŽÝ~ûí]dÁ‰'ž˜Õ<Ú0~üx{ë­·lÓ¦MÖ¤IÖYµjUcaï°ÇÜ¶Ùæ?!Ñ„—ŸuÖYÖ­[7kÑ¢…mܸÑ|ðA{ã7\8: ôg»í¶Ëñ^Ö" " "é2Bà!²8ˆ»Ñ£G»UsDÑwÍ5×8£#ë‹/¾°»îºË·óÎÙÃì¼Gpß}÷uûÓØ¿öÇdÕ÷á‡:qwôÑG;#¡…!‡!BÁ€ÃàhÓ¦uíÚÕ}‡1W¹rå¬uŽaÿ£ÑxÉ%—8ã£# ‘†¡]ý^¹r¥;C¨cÇŽîÜ»ï¾ÛêÔ©cƒ r}8p µUªTqÂϾ‡%íÄÃÇ÷•¼übyI°7nn` ¿wùQ¹ÞCal¿³9Áþ­£æØ××´²ƒpHŒsŒîüxÛnSÊn D_ó@@͘¿Ü.Ä3’%rÂF4Ô¹n¦¡µ*–³êW¿`‚½s'þWý²f£5þº5 ö› =¾¾3v§ÂñêÖõmKà®ìì·C´Ü{zc+U²„ÛkxC_üÊFÏ^lûôÔªè„É­¯~mï\v”5Þå?ûØxìýCà´Ü+C¯/t¡?^œ)„}Ž{÷;›Ñ­™mˆ¿ç>]j§6ÞÙjl¿m¶1xká¯ÖöþwìΓY›}ªû#·nðšÛ¿…Õ«VÁ ¼XE¤ÞqrCÛˆ/„s§¦»8!HXí©½ŸM¼Óþ×±ôfï#œ@Š <~?0ecÎllûÖ¬hef«{ß¶?7l²Q§íïúÅ^ÅÏÁùñ-\øª¯‡1¢Ï»BµÓÄíç€÷ì@ôQ|[ FÌ"¦¹ý «ô™¶Ò޹vÆ|ûqèñ.L˜ñ: £'7ªe—·ØÓ~\¹ÞÎ}ôã`ìöqb9Þus›§4båºë®s÷ߥ—^êD"…{Ÿû’!žYÇ3¦mÛ¶îù@!yÃ󊹄0<ðÀÝs$§zÃü×_9!ÅýÌsŠër}ü#<Ò- !¼xvÜxãîy’Ìsˆö±p„øÜgŸ}ì‡~pý¸òÊ+Ý5)<Ëx¶\xá…¶ß~ûÙ|àDíµ×^ëب㪫®2BÊ{öìieË–uÏÅo¿ýÖFåže:urÇtÐA®NµøÑÇñ<ÛaÂóç>õó\.¬}†™n ¨" " éI #ÆÀ{ìáFÕÈ‘#mÚ´iîw V¬YÕõà £äÌ3ÏÌ6j^à!|ðôQXY&$Ê׿ùóç;c‡CbÀ=ýôÓN¼ùB’…]wÝ5+T‹ý…\Çê=FÍ{ï½çD§_I§<_á±xz衬Umß<€½/¿üÒ.¿ür{ôÑG-þûXIVò‹åY!¾2ðò¼Ø½™»üâÀðn|ÛkYM™„8·O g|{_¾„A6ùVÌM¼D>ÊV0¶IØâ“¬$êÁqRC·/З6÷½cÛ"ã© þz‰gí¡÷¾³ÿëÿŸ„ì+,Wº”M8ç? R7cßùÖ¾ÜÚ «hi;æÝ@¬–Í¢x«7üm; ~щ©‹šÕÉ:ý¸ M¡+ oxÿš[ IDATÐnï‰üyõF«œç½œý‘:;HfƒøAçT޾g¶íˆÑ»B¡³$}9,ð”lµ—“Ûƒä7? =.«ŠžO~x½Ö9ïãßôÿÛ^Oe· í—Q7G÷\×C¬}ƒÿx _ <ˆ­ƒþÍt¬í]½‚ûlm°Oq×!/Ùõm÷±ž¡z\ÝÊö¬VÞó伟ìÜÀË»nD÷{¬¶2ö­±}o<ŠÕ¯žao^z„¾GU2ãKç¹{eˬPá;±<+hóÕ ¼Ü®›Ó< 3G| 6¢IVžxâ ÷¼ð /wÞy§ýöÛoNÈÅ*ˆ™Y³f¹J¬z£çuèÐÁ.»ì²…hrÉ<‚Çw\ÖžÜDŸC±ÚyõÕW»E1®KAౘÅ._X„"ê`ðàÁNlò;‹d,,QسxÎ9ç¸Å-ú0tèPçÙô ه͂çáá䚢•( žÃá}Ç1ÁêCÈP!ðÂY4ñ°aÀ ÈX¹feœ‚§ÌŒ2Ìæ.±öô…ëóéÕW_5„Þ6’ÁÒ¹Ûn»9Ã"š%ƒ¬zõênžB(Q»víÜ ?á›)ˆ¹pY•Fd"úÂ…¾‰ÎV {bžuëÖ[%ðò‹å¥A¨à߯´÷ƒd*”Mÿlq¡y”CïxÃyÀÚ5¨™”À;x·¬W$ŒrØÌ¯‚Ä,›’xÑ,šˆ£W‚°ÄOü/ë çÛâž9vD ”IA()‰S?Å×îþwmR—c>&â <8Ttë+_Û˜·¿ua”höÏ ›6[ù+§»ÃO\ û)ð4žøÀ{.D²ëauìâ@pU«Ý{ÇþÇmúþg/kÕ 4Õ—5ýí¾ 2£¢›c®šþ…½µè7'&)xh—¯'™E÷»é•À“x¼óæäÁó!›œ·‘pÔ°ˆäsXU¯PƉçh=|ÿÒ—+ì„€7«­Ô±[•rvß][¤Û™Y§]th‰µ¦!œsáþoüûŸ@DV°¯l‘Ðu·FàEŸ,ÖàuGèQð\%0gNàÉþúk'h¡FR¶FàE³h"ÆðÂÆ™Ìsˆv^‰ðäIÉ›=ôP'¾(±²h"æxÖñ¼|òÉ'ýˆáÅ0ß?"j,°±è„WÁ†×“çöGá¼xd ?7yò<&Ä•¾ªˆ€ˆ€ˆ@q$±ÃA†ÂxaO\¸ðÇ?ºO#7G}Yþõ|°k„ yïY^áFa/#måzì™ —Xïx„9„ÀË Ë‡ÞûÞz>9ÏíUà .ì‰#ä.Y—ÈkðÞˆ±x{ð¢M^å@vÏpñ^2>ó‚€ ŠN ¼=‡Yë`O‚ƒW<Òyëa©ì‡ó…Ä.÷a›ß¯Œ ä$ð^ÞP Í—‚Ó‘V¹}rc_xeûMu{ÒØ·.Ë–vc•“À{3í|§ïî#Â<óŽýì¥#´ôÕ@Ï ^‹@ITà Îãuá˃}~ã¼µÃ;¸=ɼ©]uso-û£ÉŒâÜ„¥¿n~ ¼O?ýÔEP¥d/á‰7vbȾ9J~ ¼F¹½}É<‡ˆBèÓ§‹ŽàÙS¿~}×~¦Š”XJBÄ Eే73\ðîáµ#ú‚=v„¢óÜÃóGv<}<›î¹çžlçó‹ýË¿¾Ð" " "P d´Àà ´‡½v¬Ç+‰<„]8QÀO?ýäöÚmÀ#É«õ?>™@NmeÅš0T >öœP|ˆ&áK»ï¾{\žÏ²öÖ%Ò÷dX® ¼j{Þð²{×^¡pÉMàÍ Ÿì¡ {{87Ñ=xˆŽÚÁûóÆvüÏ;çHBØ‚.œd%,ð=„6Û½J–×Ç·wÜ;ßÙÀÀ{5/Ï ç»é0ö='œØSvu(¯-VhäIWm‡ÀÃN¨ Ѭ5x†‘ÙÏ›/ô…½iO†B4Ãíö¼°À s&ìÁžˆàzô!¦Ñ¹–ˆÀƒYR óÄG¸«yMDà‘ä¡õe¢¹×C4y•cu]°Om"B+/oP0žO²Ô>¿êXç–D®›Ó< ×…‡ïRx?X¬ îe/ðHºÂ‚ž{ÄåÅ_t‰H¼À‹Uo´§œrŠ[0 ¿ƒ.Ö{ððày—ÌshæÌ™îä=k\ÿÖ[oµ¿ÿþ;G‡w6±Ç™h7²ÈmÀcÿ õ’ñ’v²RýñÇ»ýyì© ^œÌ±$``…Ÿð-Và õ+çñöàÂĹž ÞL’ÁD_ïÃS½÷2–´ùÉÿûÉÎ 2.v cd<‰RZկþìë#t”÷õ ýoh©oÿÌ` Ú}×® ’Àt9¸¶{Gß3AB–+[î•£WŒͰºÈúåŠÕ.ã&H`BIDàái<&H¦³6 'YÁë87ÓŒy"B+/oéª A’•×ìÈ€Óà6{[…`^¾ÎÃëVu n¹nNó4œd…ç0‹U>Ââý÷ßwÏW¢¨‹°v ç0À…†"y .t"“N(®2^à1°doãdgc%˜Œt á½—ˆÀÀ éÇtaIì!¡ Ÿ!Æò¢Éµ—,YâV¢Ù‹ƒ$0¼Î\´p,{YXõG ’êœc½`'ð¨Ð(˜` ±"N2ˆx/–¾Í³ƒ¬˜7Ïú&Ø÷{ñ—i²W@Ã'çˆe òwC ÌØ·÷Hª×6Ø7•¨À#C"¯Z˜ì#Üñ~T5" " " " " "PÔ$ðŠzt}È'xùRÕˆ€ˆ€ˆ€ˆ€ˆ€ˆ@Q(öoÑ¢EÖ«W/›0a‚Õ¨Q#æx|òÉ'6räHûí·ß좋.²¿ÿþÛž{î9›8qbQ_¡]ÿ×_µ›nºÉ.\h‡v˜]uÕU…víxzöÙgãŽGô˜§žzÊ^zé%3fŒm³Í6¹^âú믷òåË[¿~ýb·~ýzëÖ­›uÖYvÜqÇÅkîV}ϵN9å”ë8öØc];arÝu×ÙöÛoŸc¿’mh"÷R²uÆ:>‘ëÌ;×®¾új{òÉ'­B… ÿªæÔSOµîÝ»[«V­ò£I^ÇæÍ›íöÛo·9sæØ;ì`÷ÜsõìÙ³Pæ\²û矬]»v6xð`kÖ¬Y²§çùxžÇ|ð=:ÏuèDÈx ¼>}úX­Zµì /t†þÌ™3ã ŠL˜á><øàƒöþû*UJ™.&"f¢Ç`~øá‡ÎHŽWâ <Œo 6ŒWÝV}¿eË[¶lYVW\q…»n›6mÜgÛm·U®\9!‡ø)W®œ3Æó£$"¼ ë:™&ð˜¯ÌÃÛn»ÍªU«fU«V-´9—ì˜Ià%KLÇ‹€ˆ€ˆ@þ(ÖcyñâÅq=x'Ÿ|²õîÝÛZ´háè'"(òw˜Š¾6„Âá²Ë.Ëscà]¢D‰<ŸŸÓ‰ŒÇÔ©S6·còêu'ðò½CITxÎ9çØI'd§vZ¶³a’Äe:4•¶AƒåêÁëÑ£‡áñL‡òôÓOÛŒ3Œ…–T/,xœp …îÁ{øá‡Ý"T^=xõ|JõñRûD@D@2@Ê ¼ÇÜ4üѦ¬^½ÚÎ8ã gùP9<0Æ 3ÂíJ—.mK—.µ±cÇ!•x&8¶sçÎî; †ú¾ûîk¿ÿþ»«ãø€È&ð¸Î¥—^êÄ\Û¶m­K—.ÙFýÅ_Œ)ð>þøc×Öo¿ýÖ…zž~úéÖºukw.¡bˆ£+¯¼ÒýþöÛo»vÓo`>ôÐCöõ×_Û-·Ü’ízôñÚk¯u¦L™2î»iÓ¦Ù”)SlÒ¤Iî÷+V¸­/¾øÂyÖŽ8â×·m·Ý6ëøgžyÆV®\iõë×wý­]»¶ûnÞ¼y6nÜ8ûé§Ÿœ—±à½A¾!\pA6¯mÜÿý-·>çÄ;*D•;ÿüó]hÕ‚ ÜXÁîÄOt—ÿ믿ÜÏŒÝAä>ûî»ï ãcw§vrãñüóÏ»±~ä‘G\(í~ûíçD¹»Šòèï¹1äÚp­[·®ãN˜dË–-]¼` ‡ü!tð¬RGè7ß|ãÚÁx׫WÏõ#xòäÉ6}út[µjÕ¿æX¶rù%7ÜÿýîÞhРõïßß*V¬èjŒ Wæ÷Òš5klÏ=÷tsŸs¢%§ã¼À»ãŽ;ì±Ç³Ï>ûÌjÖ¬™­ßÔ•Û¼Id¼c É~øÁî½÷^ûꫯlçw¶C9ÄñÍ-Dóì³Ïvcóî»ïºPU榟w´sùòåvß}÷¹ç ažíÛ·wÏ X‹7nt÷ãk¯½æžU7vÞaÆù2~üx{ë­·lÓ¦MÖ¤IŠŽÂ\äÇsŒ«ãçŸvó×ÕwÞi/¿ürÖ0ÐnÆ=fúý÷ßÛ]wÝ娹Ž/O»ãÝGpåš7ß|³órOÀº¸×Þxã Ã;GØ%so1eݺuŽÓ;ï¼ã8âf± ÑLt|ãÝ?\—q#,“vò\¯R¥Š[° <<¹´…gÏŠsÏ=×…˜{îŸ~ú©5oÞÜs~èС‰Þz:ND@D@R–@Ê <Œ}¼EüáÅHD!Þ0®¼§†ï0fn¼ñF'^.¹ägÚ¢ŠûxÖ¬Yn1žü Ï9ê§m|Æ B‡61éÂ+Çñ,Bð,Ùm·ÝܽN=Ìs]81g™Ûô}àÀîù˹Ì'ž7ü‹µ/™ñÍíþá~äùÓ©S'lj…5žiôÓ <ÆŸ¶Óî¦M›º00ßó,„ûO<á„sŸsýb`Â7 $²ã±sñÅ;ƒ ãƒ?äˆ þ(zè¡Î¨G<½òÊ+†'Ì‹ˆùóç;#„szêjÞèg<ÂÆ"{ëXeÇàZ¾tèÐÁÕƒÀ‹%XmÆÃÐñ$^·G}ÔáD.ˆ |Œð 68ïÁˆ#œ‡-YçÅõ† F‰?h×£Àð†np^<Ä(^¿Ü †RõêÕáG‰×gÄe,ÞÑkÄJvÁyˆl ÄD B 5þyÍ50æh#Æž·x¼œRí¡`üû‚W“„*Ì=J,ç=Œ|ϼb|ú˜3\—Â\eþ0g’ cÍMàa¼âÑôF+‹x7ñû~y‡ ÁsÃýC»r*¹çï%„Ë{ìáªÀ£…7È÷;Þ¼!Q¢BÄ'FBÌ!(øÝ{¹كdz%ì­f!Ï?ó޹Ä" óÇa’Ô‹°%;ž+ˆ…pñ¡¢,@x¯9÷;ㆠãÙÂܤnï‘ç|²ØâÇ Ï±x¾þðœcÑ{!H´Ä"÷x"÷‘?·x®R¼ §m$w¡p-Æ‘v#¢è»¿Ïø>·=xÉ´#·ûO#‚Œ±ò%šd…ˆ î}¼¾pßí³Ï>Ž íg‘ó|ßrœøúBD@D@Òˆ@Ê <âMÀàb5ñÒµkWgxáC`à†Çã›0*þðûÂê2Æ"F4á’±öRy£#!ˆøÁx—x°FV‰Ãñ"@1 £¬4“óàƒvÞC oÄ5ᑈW„Q¸$âÁÃÈÂÄcÂÞÂK1ê½À¥>Œ!_ÁC°á@P`HxàŽ«÷HEçpTàÅë3mIdïZ,‡€Ç Ixc"a¬=‘ˆ[êÆhg®Äx91„C¬~à]&L•ðRJ,ÎÌêÇ¡Ãذ˜€1ÎGPa ³€g0™’›À‹î9d®ÑO„^´_+î%ö01ðTâÁ‰–ÜŽ‹:íw¼yC˜]²£päpf×D^4‹&"Œó˜wˆ#~ß7x±ó‹îñâ¾…5‹9ÑÂwÜcañÆ1ÜO1/”âU<ç=lxYÇðâyHd@"÷Q¬ñc¡ æÀs•gˆÏDËÿÌe_òKàåvÿ°/š¿<ï| <Úˆ·:úìC\sžÒ⸗:™g‹ŽH_)-ðØï†ñ€1„±„¡Dø† ¢ÃQKà1,‹„FÅxݬêâõ@èÂãK<Ç5 ao{¦Õìm#<Žð,}˜fźÙ…qÏÊy´ tX¥Îmç`¸°G†º¹ÂK Høa8‡ Æ¥÷ìàÕá•<¬ÜsN´D^"}ΫÀƒ?±ƒá £x{ð¢òÚµkWã7žÀˉ!ûÕrxxGüb@¢Ï#ã…À`cÏ- xI“)É @æ5㘹’›ÀcQ†{;^*aü¼cÑÅß3DÀ!ÇÂûyÃ…}­ˆ¼ÆœC¢vÝuWúL¨$^µp’X±ð”“')YcÇ‹9ÌæuûLº´7×XãGßX¸"q ‘ǽÇó‡$®ËXðôIX¸/ æ¹ëEçyiGTà±ßšPcÆÆìƒäºŒ¥x^œ2~ìãFX#Ú Ã•,+]ÿˆ«Ý" " "%òÐ ŒsjG0”ØcÂê:F”/K–,q+ج|caì#¦|â…DuáiCD°êŽÑxŽ=R+ ¼E¤ãÆpĨІ·a±ß£ã £‰°PB¾Â{X¢ƒ„á‰×#C…,BÄÂ>„g‘.œPÌSN9Žî€Â÷ˆ™^xÁ‰;D”ß§C}ìi$´“¶ÀqKPD=x‰ô9Q'ŒKÂ2¹p"óŸ/RŒ9¸ s¼ŒMcÎ^L}†Æ»CŽYXàÅcXB?¹6û£(´ÕïòÉOÿÚƒ'" " "$ðò¢ªÈ;â(ð EH) xØ?FØ!»„Þ²_(üƼ“ՙʼna¾ì·%”‘½€<Â2Ù—J¸¨Šˆ€ˆ€ˆ@ñ! W|ÆZ=Èpx>Àꞈ€ˆ€ˆ€ˆ€ˆ@ñ! W|ÆZ=Èë“,E IDATpx>Àꞈ€ˆ€ˆ€ˆ€ˆ@ñ! W|ÆZ=Èpx>Àꞈ€ˆ€ˆ€ˆ€ˆ@ñ! W|ÆZ=Èpx>Àꞈ€ˆ€ˆ€ˆ€ˆ@ñ! W|ÆZ=Èp)-ðæÎk£F²‡z(kž}öY{î¹çlâĉ;4ëׯ·nݺÙYgeÇwœë'ýýý÷ß­oß¾Þï!C†Ø¶Ûnkƒ *ðkqXã¼yóf;á„ìᇶjÕª¹v|öÙgvóÍ7Û°aìnݺ…Ò¶Xùè£lðàÁn^ÖªUËòÅ_ØW\a °£>Ú}ö÷ßÛI'dýúõ³-ZØ©§žjÝ»w·V­ZÅl{´‹-²^½zÙ„ ¬FEÒßœwÉvð©§ž²—^zÉÆŒcÛl³M²§çxü?ÿücíÚµscܬY³|«7§Š˜õêÕss#/%ÚÞXϼÔëÏ¡}Ü‹-[¶Ìs5…Í4Ï Õ‰" " "RZàÍ™3ÇÑÅMà!nFŽé„@Æ Ý4»ï¾ûlÆ …"ðnºé&+S¦Œ,…QbóêÕ«íŒ3ÎÈ&ð–.]j“&M²®]»Z•*U £i1¯±jÕ*ëØ±c60eÊ'ÂÛ¶mk={ötç-^¼ØýÇ{CåÊ•s†}çξPæÍ›gãÆ³Ÿ~úÉ…úÎצM÷]8œï‚ .°eË–eÕÛ´iS¦HAìòï·ß~sa^»ï¾ûºïh'\kÄ#¸bÅ òzá…ÚðáÃm¿ýö˪óÆotýìÓ§Mž<Ùyðh–ôsÍš5ŽÌÃeÓ¦Mvï½÷Úÿ·wPvÙ® `Á݃Cð@€…àÛÁ] º¸»»[ÐàÎÜmq—üç«=5§Óég3/É›7_3'™÷º»ª¾ºÝsuoU?þøã‘ѼóÎbJ¯$ QþÞ{ï…)§œ2l¾ùæ¡OŸ>¡hœû÷ïÛ—-'tRlS6e‘þáŸxâ‰1Mï­·ÞŠ©Œ)"Âù8–ôç¶Ûn D޲宻îŠLN;í´˜b9þøã‡%–X"l²É&Qà–+GqDèÚµk8ðÀcØÆ.»ìŽ=öØÈjœqƉcK›Ž;î¸Ö1Ýn»íÂ!CÂ<mÇ9 ¾¼ +xåì2ß^Æoÿý÷ƒ O>ùdèÖ­Û0õq|9-gw©®"»îºëöpÁĺ‰ˆ‘ꈸ… å矎÷öÒ£G˜ȱo¼q<ö÷ßk¬±F8üðÃÃB -ÏÁv¸×ņòéÚ¥îåß~û­¦¶ž‰­’¢\MŠæO<íë7Þˆ¶Oä}Ûm·ö‘/ˆNøpÏ’ÂË=@¿xF¥Íz´7ŸüÅ_„óÏ??<÷Üs±I¤[rbç¥ÚÏ3…{=[˜tÁÖëÑÆ²7˜_J@€$Ðf +ðp I{Cä!äpœF8q8_½zõŠb GrÀ€15g™‚CçwÆh×TSM/Iœ¼ÓÅõ®¹æš(\m8›8Ÿ;í´Stîp”¾üòË(8pvpÆ“ã(ä¸ 'œ0:mW^yeŒ6’>ˆ°ä»ùçŸ?¦ó!.N=õÔ0ýôÓGÇçnƒ 6ˆ?+­´Røðã¨Lë\²}<ì°Ãâuô5G¤+"¾è'×%b3DÓL3ÍÔÚN„‚xºé¦‹ ‰6Ñf„gv lf¤GRǸãŽV¸¶9à 3Äup¡¼À£ÿ&„'N#¼ûöíëᜃ:(:°ˆSÄÔ)§œ£[“O>ùpãL¤’c§8óOEíâ|"uØ$í^pÁã83.ˆ: cʸá ÷îÝ;NT ôGÄu%‡ •³ËüS€ú‘ŒLxPWª¯’–²»l=©ÍY£Œ7ç3æŒ'÷0ü'DÎ~ûíïÆ;ã~å'EÍÚ*ðò÷r=ÚRîéJ_ðÜ7³Ï>{øàƒ¢ î½÷Þ­k1³ç_vÙeñ†½°Ž”S># +®×^vÉÖÒzOì†Taî_ì“g)‚£6ŠcS®ýŒÃºë®ljµ¥ðäüz´±Íµóÿüxàì’–‡j¯ÀËî2IDö<„æÀcD—Bñ{÷Ýwc4•‚Ý01@$‘öÐǼ@-º•ˆ>1‘Àu°¡ &˜ òcÍ()¿Œ;b*m°Â5òcšD Ø@%WÉ.óí¬T_56Z­ÀËŽ ˜0ÉÞ?Ø÷`Úù’—Tê%ðòφz´¥Ò£”]+ùØÑ\"àL¨äw¡EäòŒÈÚ×Î ¼zµ7;öŒ3QRvx-*•ÚŸxõjc%®~/ H@@Û4Àé&‚’¶«çÿùqÂóÎ{‘ÀÃ1c½R6â^Ö€!ZJ '„©wDMŠœg®À#] G!òH”’và¦+G¤À£^œk"nD¡ˆ¢!òH³+Uè3¢‰”Bú™¦sޤ8Ò]ÖO‘‰xEL.µÔRÃ\§“hãˆxˆgÆ !I$”TEœñ™tÒI[ÛÂ1¬ÿBt0îˆÁìzÎRLX/F¿pxù—”]Ö[ÒoR aKä7EEK ®´Î¬’À«d—Õ ¼T_56Ú‡mÁŸµùBd+¥EÃ-•¼ÀcBaQë¼¼À«G[Ê=Z™Ð`Ý*‘Hîmì‹gíGgK_Ö‹f'I²¯^íÍÚ¶ˆØÎN6¥vUÓþ¼À«WÛö'˳$  H@¨D ¡éo8ŠÙwÞ 2"xIàýôÓO1r’¢d•]T&ÒY7Tô~­"G‹u]ií mF Ñ6Ö¹PRt‘ÍHX“-iv?Eäòb€èNÎ …Mú‰xɧh"fpÌËíÂÈ;õˆâq=R3YŸXMA°ÀŒ´¯r…ö"Þp*ÓZÈR¯](çÄ#ª$€hOŠà%‡xd#‘|º\QÛY‹ÈÂ%¥5Vê#B‘ˆ)é®L pµ×^;:ûô!»YL{^%»Ì·µR}ÕØhÞîòuÙ"ñˆMõÎÒ[IeÍFŸ“ Ëî\‰°ÀvRŠ+QuDS¹MVò¯^m)eDˆ¹Ç=IȳÑ‘ï¼Àãž%Uœ(^ÚL‰ëÒGÖs/׫½Ù±ÇnÒ³(?ÁUMû±g&»ÒûëÕÆjž7# H@@íZàá¬ã’BÇîˆl˜RIàQ€ÓM¤‰÷ȱ։”Æ´ö)‹©èzDÕÒ¹¤á!˜Ø}ŽEX—”œZÖøáH¥M$˜ g—J"G(®Á¦"1Ö?‘ÎÇÌ=Ž‘Fœ.œX¢¬‘A4!Fyç§ö‘G‰ë¹>‘”J¢q•¶Ùg=!)ŠDØXVTp˜‰®ÐW8Ð?Òó»\²i )¦óÌ3O@dãà/¹ä’qYraÉ&5ˆ 4Î.})g¢mpKçíc'Ñ¢MVŠR4“À#JÄõaW;©³8ãŒ)›¤A!Œa̦$¤´±NèfQIk›¸nV ãÀcCôŸÔT* ®üx±ñiŸ¬—ÄŽ+Ùe­¯’r½"»ËÖSj=*ìHUD òØ9ÑÃzSì‰~1a‘6a¡¢ŸYǘ‘ÚÉ5{&¸N-¨r[ÚÂ}Á}JJwJéeƒ’l5{챸F•çbŸ{”H0;æÇsobØÈÔSOï_~°AXÔ£½Ô“µ5&ŸÒóŒ¨3aÇæ)o¾ùfÅö39ÂZJ®Á³Ž‰Œö2­ýO•gH@€$P-†x8;¬«Ã‰Â)'õ顇 EkðRŽã"Üî¸ãŽ(îX_Ålp/Ec>úè£è´ãè! ؃èB*9µ8©ìЈó‰ã‡”݈„k°Þ†¶!Žh×H©ƒ4Rq„§vÚ¸m9)žyßqÆq$qÈh ¢#õ“ÔI¾g'PRÓËÑ+ <ÖÕ¡a'HœÍ¢ÂkøCÁAGÀ!²˜Ù‚sŽÐ¢NÒ„8¬ô•BÔñгLš+kÎh+N~Ñ8“^K¿8‡z‰²ÉL­íá'X±æBƒõv|ް&‚H?©“hEÚƒ px¤l6â! “tÛ´ù DZa m'bÙ¯_¿ÖSkx!6#al5•ì2߯Jõ¥ë•²ÑRv—­§”­"²…GÐ3±Áî¡i ìûï¿Åõ믿'8xuITQ“2ÜÿÜ_ØŒYØc©×$äŸ \§-mA¼3éA{±mÆÿ#@³Ûå9Á¶ˆ&Kø!Z$ðh “$l^Ľ­1a„CzMB{Û[ôü€7í$-ûg¢…gÿ¯Ô~ÎaŠØ“6õhcáM净$  ´›@C ¼v÷n] ’pAÕÖý²Lœ×ôJºWÐ$R‚SšÖÿ!š˜$ Iúfv#hn§mB£¿ÁÎdBÔ" H@€™€¯ £ÓÑ颤#1!"Ai3¢v¤9"=—hÑ0"K¤?N¯ÁhÆþw¤>å7Yi¤¶IäÕlø2ÅS4RÓl‹$  H@Ž€¯ FÑÑ©¤vÏ,Z—Ø$ { é±W\qEÜh…;Œ>úèÍÐåÂ>œqÆáöÛoGuT˜þù›¶Ÿ£ºcüb÷ZÙ<þøãá¸ãŽ ›l²IXwÝu«:}‡vï¿ÿ~´µÑF-´Ç±/¾øb8ùä“Ã×_¶ÝvÛ°Új«UÕ.’€$  4^Ë(æÞ'Ÿ|.¿üò°Í6Û„‰&š¨]ïï¿ÿŽŽÆrË-æž{îi3×]w]{ì±Ãª«®Ú!Û_M£Ï:ë¬ð믿–xO?ýtxæ™gÂÎ;ï\Íåª:æ¼óÎ 7Þxcüo#”ö8ÚÐþŽÒ†fx<ð@8þøãÃFm6Ûl³ª† ž¯è»Ûn»…)¦˜"l½õÖqnœqÆ©ª]$ H@h ¼WËÀVŠàÕr­ŽzìСCC—.]:jóc»O8á„I(Á;úè£Ã#ûì³€3˜Ê€býÙúËõˆ à0Ršµ.WÑJf¹‰2öêÕ«õ²GqDLËìß¿ìc6 •ßç˜cŽ˜Òxçw¶¦f=÷ÜsáÒK/ ï¾ûn·õÖ[/,¿üò­×ä¼Ùf›-üùçŸá®»î*Éj¿ýö ÷Þ{osØ5#Âuî¹ç†?ü0Ì9çœaŸ}ö ã7^ëµo½õÖ(”¾ùæ›XÇ¿þõ¯0Í4ÓÄï+Õ»ÕV[…O?ý´õZ ,°@LÏÍ–›nº)Ü|óÍTNÊçŸN;í´ðꫯÆ6.±Ä‘ÃXc5Ìy_~ùe´—^z)~¾à‚ FÞŒNq¶ÐþgŸ}6 üþýï‡ûî»/<ñÄh¶+®¸"¼óÎ;¡gÏžQ ¬½öÚ¡k×®ñ2ØØLV^yåþâ‹/" X¥H4í!rÈXeëç~‚aVàõîÝ;Ú9ã|õÕW·ötUìíC }úô©›-¦ ÑîóÏ??¶‘ÂýD;`‹½b¤àapÙtÓMãó€rá…FÖØs*¤›ò=(Üx`ÏçŸ>œxâ‰ñ{úõ /„Ÿ~ú)Þc¤]3¶”ü1œ~úéH.÷x*DåIO¤”³AX%æÂ}E4üâ‹/p@U€}ÜvÛmqóÌ3Ç{Ú" H@^àñG’™Xhœœ…í¶Û®&ÇXDÔ¤“N…N Î N%‡0ÄIÀéÇaÂIëÛ·o˜p £x"“uÈSO=u˜5LEçt=öˆ ŽŽ?88k8~¬}ãûûï¿? œÒé§Ÿ~˜™rœ§ƒ:( BqpˆBsÎ9Q¤uÔ_®>œœ)„Áˆó†C…“_$ðÈ7Þ8®I{å•Wbû¸óÍ7ß ÿùÏâï)µ“ëà´óâ„ch#\Óz/Ú ƒ½öÚ+:T8ßO=õT*Dר Ñ oœ. B“ïqq‚#Dâ§R½°ƒ5¶€Ê9ùuvy‡ƒ‡(À6¿ÿþûèx2ÙÄVºyDâkÒx¿öÚká²Ë.‹6Íd×b&íÆYF¨1A­Ã»¢L*,µÔRñ|~°¹vÚ)V—œfŽYi¥•¢d¢€ÿÓN bùã?n=ñ9ùä“Çzp|ó¼$æ¸ï¨—˜}ðÁáÊ+¯lV|^[üî»ïbg˜a†(ܘ$a„ô¿~ø!¶Lp,÷÷÷ ¥ZÇ„iÑE}@ÀÒ~Ä=ub“ˆñÄþsa—ÜŒ÷cÃDB9äzœÏäÔ½òSm/¥óÒ÷ÙgŸ=<üðÃÑv°q#<¸.kà¨a…í%Çz·™fš)ÞwŒâCäšRîúÜLÜÐGl…gB’ûœÉ„¼Àcâ-M€ñÜÅvy^ðH)Ε"xÜ_ÜSÜ#<ËHŸ^ýõãóû9_²ÏØ4ñÇó›M",Ãsg7Ï‘ìó†ðå9ŒXçy‚mñüåoÏ3V+ð®¹æš(ìhc”&!FÆvë€$ ÎM á^~m þxâ˜WÁK=†›óp|œ_Ià1@À¤™þ¬É$g‡$„Î:uPŠ ÿÓ¦+ß~ûmt`O:é¤è¼Ýpà qö›z“ãL;™ÍN³È©.Ä §§…‚C…# +õ—«ãE8Ç6¢EI å#xD‚ˆ`¥B}OB`â…{/¿.9/ðàCÄ“ã)bîo¢¡Œ!¥’À#rM_ŠØpªƒgGQÚeþ»úê«G»H“Ùqf’…¨+ð`ûALr¯bßLº01’ ÷ ü[­Àƹ7Ós¹¤±ù…$  H Î:œÀã3Ž&§½ZG:R*Y½’Àc¦œ:qù£Ï¬uJÝ*Zƒ‡ãËL0J‘À˦l¦g QÒLqÀD p4q@è‘lÁÑĉȊ"8XD_pÞòõçëÃÄC°!’¤?&ñRÁË ï5×\3:mÙBÌ^ãè0óßVéˆÄäXÒ_"Do’ƒ“,ú?œÓjê­UàáÜå¡_DEp:‹fè$Ìüc«+ÆbÚi§CXNà¥t·ä ×d>?å”SbÚ\ŠåfŽÁÑ…¶ƒð%zJŠ,“Ø ãƒM!>(EkðÒ=öXêDÓžE©Âõ°ED ¶µï¾û÷¨ã;DCv‚€>ð à~FtV+ð²÷Á†>âìYFÈùá9CA`S5îì…{œÉˆJ6Hú/÷bÑ\¯óRé}Ü·DÑ“-`#¤‚Ã$¥0ÙcÈ$¶ƒ ,wýYg5ŠIDq^ÔÓ—"Gš-“F¤À1ÄæàÉ„¥’Àcc‚£›ÕÃõ¬ˆKm-x|‡íÁÇdBÚ"?W´//ð°Ö»bÿ<xÎ`·8ö””"‹HAT²öÀï•la€PDœ¤R‹À§~kQ IDATCØ0ùÃZ0"mÙÂdAš¸âaN]­@$·È.ˆ’zÈu¬å®Oz'iLÞ¤5jÙ6äQeR[‰Žñ|&+FŠ-÷¥’À㢹L*µ&‡ fírQ©Eàq>¶JV–¨6"¾Õ¼tUZƒ§À+*?”€$ ‘@ Ã <„ ô)8ˆˆ”¶ÃŒ1i4ùMV²/¥<áD’ÂSÀËŽ:é:œ_x¨Uàá@〥ô°rv€³ã œõjœŸ¢Íiˆ1ãŽ(%€ H›ÄT#ðpÀp*³‘œZ1NkÛª’ÀcC œòÁ)ê5õVz×_>E3[)hp¢ùwÂ:­J}aÓ½÷Þ;:­D²ÑÉ¢hHÖ!æ=|ÓM7]tÊÙ¨…èP>E3½[,ÝYÇø¤hQ•4 úC v!ˆtú@„¨(½±¶ˆ@cÛ}ÄU>"J?ÓwØ%EÏh7ëö°¹{î¹§õÞAŒÁÛÌn²’xi£¡l %™¶$‡ˆîׯ_az`%dcR>MœgW5kð˜èÁ¾XëG4«(Rœµ3ÚLÛy."zó)šØ+÷9“Gˆ(ì ÜõÙha‰Á(e¤!ó6Ëð¼Më6ÓõË ¼"»£ßÜS<£Ù†¬™¬æW.E“óùÀµ¹IÍLOH·åy³|Š&k0±ƒdsœGZ:Ûd'»ÉŠo$x0V! H@…^à1;NZÑ%Ö›‘ªˆã’vœäÿ8ÉÙeý ä©z¬ËÂ1dÝ k‹p–p¾‰°à°2ËÌ®xyÁ‡à!âB gAÅZvk«F<Ô*ðp pf˜UÇ1%RBÚZ‘cGD§ç ˆ<úSÅÆ4”j"xœOª)¡8’¬}I¥‡°ÆYÍo²B”g‹ÒV•×Å™DÔAdüp ™‰gÝTµõb/8ª0Ç&²é¼\#+ðHÕÅÑeƒÖTÝÁñãÜ´3+çÐÖH± ÑÆG5­+b3œ@v€%‚F$t¼ìŽ„i Rj2‚ŒÉ î ~Š6Y)'ðX[Ä.¤•ÒO~°1ƈ8‘ ¢sØw6ZBÄœu„"‡ô'_êa‹¤C§qÄ)¤²y÷.ß±Ö 'B[¼iMaŠ® øÙà‚qƒ;»M–x< XwEº#b‚È;kç’ÀãúÜ[¬%£D§¸'S)gƒDëx¾™L!ao¤ùr]žMˆ.ì‡(Ï©|áYuÇwD{ãxúŽ“þKÊ ÿ"æˆö#4øŽçv•ÞÔSO^xḻ#ÏlN”r×ϾˆœõŸD‹I»$jͳ'/ð¨Ÿè!må¹É˜vžvj¥>l‹ç9õÓŽRvÇÄ ¶;ÖÌå7?Jœjàq“Œ›¿0ö©`ÿÜCümaÒ€4kD+}ÂîIÕÅ6ü¤ëÃ噫ÀÎtý@€F†xˆ7ÖÝàøâPággV““C$Æef›qFÓkp&H-"*Àl4NŽbš}Åc¶™Y]¯¼À#bˆÇçi-UÚz¼-¢¥’àÂQæú´5»•=íN‹ý³¶Â1ôõC8Š8¼8¢éýZ•êK×"] Îy¾Ç5pt˜ÕÆ™C4Pov¬Úª‡àÈã#ªpr‰’‘jJ©¦^Æžµf¬¿"¥,»=;×È <êc#DÛ®ãu¬“ËŽ#2ʹD‰XO„SHäÑŽ° R‹“‰mS'ìŠ×%„É1Ô‰s‰ðɾ&tºrÁÂQpHS¡Ï8°ðã^à_l>¥½ÒVxâà§hGÑóª¶H¨¡…(`2‡¢ê£>Š4ˆ"*Œ3ߥ()̉!¤¹W™„¡/¤ –xôƒtE¸ Þ0\¶ÒOT\Ò€}éµ%Œ'Â6T²AúĤiˈt&BK„÷)¢! »ì¶þ‰1u2y…=Ñ®A;¦¢ÀôáG¤•±ÂÖ(² °;l“TÍz¢œ¤U“¢IZ9)¿ H@@3Pà5àh²Å8Q<œÄbG’+ÖÇY$ÐV¤•’^Fj)‘aR.YwÆzÅ¢5žDõH[f-Ñ¿¢µwmmKG:/½˜œ—ˆ§ÍiHsd dÚŒ¨=ýAh#øà°Øb‹UÕõ7ß|3ì¶ÛnñšnºéFé3»ª{$  H x5üä“OÂå—_¶Ùf›0ÑDųwÜqǰÞzë)ðjdyÝu×…±Ç;:lÕ–³Î:+üúë¯U <ÑÉ'Ÿ…ÏÜsÏ]m5 sÜÓO?žyæ™°óÎ;Ç6ýðÃaýõ×ï¯hìjÿ†È:7DW½Àûàƒâ$ÏE]&Ÿ|òQúÌ®³x9 H@@!%ð†ºtéÒpC¹Ùf›…-¶Ø¢]¯-}ëè¼¶ ä 'œFm´ª^[êhäs>ûì³°å–[ <&–]vÙFn~»ÛÖÙÇ?DàÝrË-áâ‹/n7Ój.@dmdØW¥^Š`×Áûî»ï†n`Ö­[·ØÝz<³«áæ1€$ QA ¡fŽ9æˆ)ywÞygØd“Mºë®žþùèØ¼÷Þ{aÊ)§ ›o¾yèÓ§OäÇñ§žz*Ì?ÿüaàÀáÛo¿ ½zõŠ):“M6Y+ãçž{.:Éï¾ûnüœÜòË/ßúý­·ÞHûñÇÃÌ3Ï‚¹æš+¼ýöÛá_ÿúW¬¿k×®ñólÁù&ÂBz3Æ?üpøã?b{H¡ëÙ³gk;_z饰øâ‹Çã¨c©¥– gœqF¸òÊ+ÃXcá¡ßÛn»mXz饇© >ã?~L½öÚkc¤‰âˆ%!üÄO„Ûn»-¼ñÆñ3"Y\‹¶SJõ3oŒ8^ûí·_¸÷Þ{cD‰z‰*;÷ÜsÇ~æœsΰÏ>û„ñÆ/žþÎ;ïD†/¼ðBøé§ŸÂ¼óÎÓ19—’¨ü>Ûl³…?ÿü3Üu×]Ãõg«­¶ Ÿ~úikÓX`˜*[©y§‘ß÷ßÿ0hРðä“OF§žG)¢E/¾øbèÞ½{XmµÕâ¸Â Ÿ¤êüüóÏÑHÆó¼óÎ üq˜bŠ)šk®VXa…aPxàa‚ &{ï½wüü±Ç‹}€Kªÿ / ¤• 0 ÚsJÅ{àÂñÇ?ÌõN:é¤0ûì³ú´ÝvÛ…!C†Žc\ò}ÊžøÅ_„óÏ??pP°¢ÒØQjÆ”¾sڵ馛†1Æ£Õvù{%šýÕW_…EY$ìºë®Ñvï»ï¾x,÷BêÌöÚk¯pÜqÇ…3Ï<3Þw¤Ë1<„à÷ý÷ßg¿Ù±+5þÙ~áÌcsp¥]ÓL3MlÏ€TÊõlž ðÇŽ7Ø`ƒÖs¹ç6pÚi§…W_}5Ú÷K,ï[xbÿ#Â.n¿ýöØ·sÎ9'ŽϧìýGü|ýõ×a–Yf 0ä™ZTʵ³ûªôLíß¿˜gžybRážâ÷”>¿Wþùçx>þøã1›h?ÏÞ8 5E³ÒýÌäãÈ3òË/¿,ùÌ.„⇀$ H áÞàÁƒ£“¸ñÆÇ5Hü~ÐAE±„ƒCuÊ)§Da4ÕTSE‡gŠãƒüqgÖ?9`ŒŽ ŽöÖ[oœ¹ÿüç?ñwÄÐ{ìŽ>úè0à 3œ>œ¶¼ÀCþþûï±Dß?þñØFDŽ+Î "‘Ääé§ŸÆgœØÎk®¹& ;êÂy¡ÐnZsÔÍlõÕW_Ý:ûœì AÄ.¹ä’Q€¼õÖ[áÔSO GuT”ô‡a… U Ñ€¸ ­åú™·eœ(„N:â‰'ž…4‚Œõpô޶$®Šm`œ`‚YpÁ£SJ)x8‰ŒëÌ„çÑ„2ëöHÝe—]¢H%šW®ÔS$ð.pŸo¾ùÂ7Þ… ?ô‘°ÓN;EÛà\œB„b[L"‡™¾ TèN$?+­´R¼ˆÄüºLÄ¢{ à¼"2q|ÓZJx"˜RÔ! <Ä>öŽÁ„‰'ž8 )ꦌsïÞ½Î?B/õ)/‚°KláÆù´a£6Š) ôÞÔ ljúé§N5Û¥~Ä+¢Ñ „kŠds }¤~l;1Ký„?ýEÔa‹ˆADcòïÿ;,´ÐB±®ìØ?}Î&w\“É„*íCÌRèO©¾ólac­µÖŠ}cÜëÜ[܇åž;‡rH 8ЧgŸ}6¬±Æq‚fDÙBáʆ{{å•WŽc@=¶†=1~÷ß9<ëfši¦a¸1©R®•ì«Ò3•Êjx<;o¾ù&>_YoÌD?Ùm+w?S7÷bJÏ,zf§ ¯üsÏß%  H@‘@à <œ,fÆSAœ±B¥‚Ó€ÁA)Z›’œKD ç"RpŽqRA@1Ó}ÅWDáBt†H ÇeK6‚—"‚«¯¾zt^’3TœaœK"Ö1£HàxÚIäŽM&œpÂÖ*§8ºˆK €($‚1_HœËTÒl8Ž~QAز9í%òUªŸùsq¢ˆü¥™vXœ¬ìfDf6\³¨À˜ â€R$ðØTˆJ!ªRp²qÜËm²’í#×)xÙ¾Àq“¢aô Áƒ½¤Hè 7Ü#Ljçd‡zhXtÑEcSFØãDô¦TÉÛbªoß¾áž{ ì„ &%Ïy{F„ ‚Š6YÁ¾'”©H}ʶÇ'ûK‘âô=×%‡í'ñôÚk¯E1†-"h<¸N*ŒÇ´ÓNÛ:.ˆx Œ³#Œ=Qw õ?òÈ#±®T˜¸!2Èx]5ãŸíkjûe—]ïår}ÇîŒk¾Tzî @˜ø`<³eDÚDô+EVnDê˜ar‰…X{Æ„C*ÜL0ño-íä*g_•ž©¨ZQbžÑé¹M{‹6YÉ?›ò÷sѽ˜f—¼aýB€$Ð 4¼ÀËîÉŒ.3Ô”ñâÿ8ÅD|pfŠßãø"ºp¾IÑÌgœNAc„‘4fðÓîkÕ<ÒÍ‹Y˜vâœåÀ*µID Î>í`vç1 ‰¬­ÁÉ"bGD„BÚfJ«$e‡“kÁ‚YûRýÌÛr^$%^â´Ù b• Ñ ¢)>úhŒÆáx9æ˜1rI)xù]Aóý)rðËõ‘zŠ^vÇIfô‰¶qÄ1ˆó‹˜ËÚ„ Îf‘ PcŽ€áD‚‰‚å 6ŒxA`.¼ðÂ1b‹“Ž3ŽÐaü‘ØNq-¯\Ÿ²í`BÎûî»ïpíã;¢˜Y‘M›áƒ’Æ\d»ŒË¤“NÚMŽ8“/D#‹˜MÂ6™ÔH!Éd ‘Á¢±«Fàa›D«w]Ò¬ÓŠåúÎs!/ˆhC5Ï"|+¢D<‹xn$á52ì‚vb?´¡Gtž{'+¨9æDwS9kåÚ™¿‡ò÷L¥g*\jxL"ñ& •R¯ZÛO×Qà wëû$  4'ðpœ/Ìòg kJ}ÌožvLÎ&Î*3íÙõQ¯¿þzŒ>à$±~Š‚SJŠ©nÏLxµópܳ…ˆëzˆ”Û(©ƒüpëšò©h\·”ÀKë\ˆ¢°ö¨%}&"Äš1ÄJv¿¨Ÿyo‹À£}¤F!hè b¾8™Eí¯ÔÎÉ;øÕô±ZGý¤"~‰$f£ÄÔ8%U·”Àã"(¼Ö€ñEH#æó…T:®ÀÃEd“Ú‡ÓÉZ*"¸)ŠÔ^—ú”·CÄkVÄ¥ï‹ßa?¤6Ž ‡"€)ù±«$ðà… `b¾R~Sô‘û©Tßé#cž¯Òs‡¶2‘ĺNÆ”ñ% ŸÖ£Žh» ~î-Ör·Eà•³ßR/ÙW5ÏTž¯ìd[í¼”ÆNä?•Z^‘í§ë(ð†{,ù$  4%ðàŽ“ÀZ»"Ǚ6˜Ò´ˆmÈF0ˆ*q.)Öue 3É|GD®È¹_{íµ£c˜ÞAFôEdbê©§Ž—úí·ßbŠŽgJÑ,õ+ÒÖulCÔ+­YËÛ]%AÄ&38}8¼iɱÇ#wù-®íg¾®ZBÇ:›^ÅÆ)DHÛ#ðòïΫ¦µ <ÚHÔ‘Í+Š„u9—¸±’qËGPøžtLDb[Ãá%—HBa‚Ã\dÏéºDx³›UrÀ³ãIÿ˜´À6R”)}O6}—æH .ëöFt¯œÀ«ôîDî/R\?"¢D“À+×wR_Iÿ-º7*=w²|¹×Ùø‰µpy±8¢ì‚ú³M&VˆlåS4Y¯†P.Wòí¬d_ÕÖj‘W®´£VÇ9}ôQ¼.Î$Î(u!Æp”‹l;c¬HÑ#%˜ GˆÍžp}Wkí€i3ì9»æ¨ÈNHÿÅþ±Mœe"~µ:¹8ÖôÛå¾à¡lºB߉(’eó ¾CP—²Ýz®Á+ÁË?é®Ù‚m3¡Ãf?\‡{ŽÈŸ!Î)åúÎvüðçœ~6÷Ië~K=wX³ˆ g7Vî)Î#Bĺ´‘iÙý$µûÁ^àÆŽ½<™¸È—Jí¬Æ¾Ê=S©Iv¼EL"ÂI'ÍŽ§)Ò‰MÒ^ž½|T·Ëú%  H@€$ Æ' Àk 1"еY¤Œ’ÊΓ H@€$  H@Õh:WmÇ=N€$  H@@³Pà5ÛˆÚ H@€$  H ÓPàuÚ¡·ã€$  H@@³Pà5ÛˆÚ H@€$  H ÓPàuÚ¡·ã€$  H@@³Pà5ÛˆÚ H@€$  H ÓPàuÚ¡·ã€$  H@@³Pà5ÛˆÚ H@€$  H Óhhwøá‡‡qÇ7ì¹çž£|€üñpÄG´¶£{÷îaöÙgÿüç?Ãl³ÍVUûvÞyçðÎ;ïÄcG}ô0ù䓇¥–Z*l°ÁaŒ1ƈŸŸ}öÙá­·Þ 'žxbU×lËAÕp]guÂ;ì–[n¹ðË/¿„í·ß>öuÅW,[åÛo¿_Ò~ñŇÉ&›¬ðØë¯¿>Ü}÷ݱ¯p°H@€$  H@õ! À«’cx×\sM<㫯¾ 7ß|sx衇Âgœ¦žzêŠWBàõîÝ;l¸á†Q4 <8œsÎ9aÁ »ï¾{à ¼¿ÿþ;œ|òÉQìÍ=÷ÜíxO?ýtxæ™g<,€$  H@@ý(ðªd™Þí·ßFm´xÂg«­¶ ýúõ ›nºiÅ+!h^xá°ùæ›·{Ï=÷„SO=5ÜtÓM1Š×ˆ¼ŠËPM¯–ëy¬$  H@€$P=†xã?~˜qÆõ×^£^Ë,³LØqÇC—.]b/Ÿxâ‰pÛm·…7Þx#~F”iÛm·ÿGH­¾úêaÝu×m%rË-·„[o½5\pÁñ³çŸ>¦¾÷Þ{aÊ)§ŒçôéÓg8‚EƒöÛo¿xÞ\sÍ#yW^yek¬±Zà&›lÛ³ôÒKLjU^à½üòËaŸ}ö W\qE˜h¢‰Ê øàƒ0`À€°÷Þ{‡üãQÄá¬gÅ"!‚ ể:(®5[`«¯¾EBmª©¦k©Þ[lVZi¥("ˆ$Ä\ |p¸úê«C·nÝ Bú ¬G+ÁC! ¨‡úèÿ 'œ>ÿüópÚi§Å:«x¤I®¿þú±­Ÿ|òI\ó·ì²Ë†í¶Û.^§œÀCLï±Çáè£3Ì0CäˆÏ üðÃðÙgŸÅ ‰r¼Úqïzª$  H@€†#ÐðrÈ!­ '%’>'Eåož@‚ÓIDATÀ “L2Ièß¿Œöì²Ë.1RÃgD¼Ggžyf˜nºé¢$:ˆ(Kå°Ã‹bG=[ò(*ëèØPñõî»ïFáC9묳bûï¿ü=Áû믿Âk¯½ꢋ.Úº­’ÀËF¾¸fŠ˜!¢èKµÁ™"z\ñ‚ðb!ÑÏr¨é1Çëšx≇á”Úsú駇™fš)~÷ÀD‘M‹R$ðòíAh"8g„=8Ú8ÁÄk0ÎóÌ3ÏpãTdpF0^vÙe±½ô›Í{=ôÐá/õ"“MföÚk¯Ð·oßx¢ÁÉæ;Zl†±^b‰%†¹n9^…F쇀$  H@h#†xù]4nDì¶ÜrËØeÒ6ï½÷Þ¸i‘~ø! ¦8 ~P ¢B´†ô?R4QD¿V^yåxL=Zñýúë¯1ŠÄ®‘Ù’é—œûûï¿·ŠC"W¢@ÔGÊ'B‚Q&ÚCAà‘jH¤Ž4Ã1Ç3,¿üòa›m¶©jͼ0âš´—¾Ñ_„Eµ/ÏBê&”ÅrTJR0I%Hä ÁL)ZƒÇØ Òx¤€ ¼rã +¢¬DÃØ †q&ʆ[d‘E MŸ lî¿ÿþ(¢‰Ð‘JʸÓNR'_ì"_J}—DbÞ^¸.ÑÄÅ[,ÚQc6Í!Ý“è#¥¯6Þ·ž& H@€$ BRà¹!’÷ý÷ßÇõgD®ÖXc¸&ŠH‚€…µmt’TFÒ%ç˜cްÑFE‘†ŽPdýY¶ 6X£–-Ià‘îGº!¢0+ Ó±Dpæ›o¾øC}DùÒ«xÔ CèL8á„ý& Ö‚–(‘GÄÎE]^|ñÅŠkðò‚*õTѱÇ»¬ÀK}EÌ!ÚˆÐí´ÓNQ0•x Dmµ/3õÑ.:‚–¶#*âE…ëíC`³æ‘h)éŸDT§Ÿ~úh+Dm‹^ùPê;Òw‰Þa_¬•Ì¢Šiíâ×__AöœSŽ—Ï% H@€$  Ô“@‡x¤G±Â™ïÚµkärì±ÇƈIx¬/c“Žã_›¢PÒZª¬^ n©MVòÇ?òÈ#QÔ±Q 除¦R´ÉJþüZÞÃ?Ó%Sú!8væL›È dIEeC•ì&+yGŠgÚ$„6UÚd%Ûîn¸! šË/¿|„<Æ‹¸Î§„ÂŽ5Šˆ5 ‘U¢¤Ià±~µyÉF²×(õÝO?ý'ˆÈu­TRZ)ãì-“åUé:~/ H@€$ ZthÇFˆÖR!`W잸ÐB ã¼#²ˆºü÷¿ÿ›´¤òì³ÏÆÔAÖV±Þë·ß~ >úh\WE¤)[ªxD‹Ø„‰æ‘NZOdž¤)’ŽIê!%RÓËàé‘C„»["¼H/$4+ðˆH½äs„Â’ôW8P`B”ŒºòkòXHÿX‹F´õvì*yüñÇG]NúÄ{óDXÓn¥yƒ'šÈ.–DíwDqÙè$ <6˜Áfè#¯¸ ê†í¶Éf/¥¾CDóš ®ÏûáÏu‰È“H&»¶²Þ“ÍX¼ˆ}w)^ìâJj,uÒ7‹$  H@€ÚC C <6¹@ð°ÖŠ´IR.ù—ÍU²ÑDÑ¢8…lÁÙÇ gsv‚dýÂ'Ÿ~Y­ÀãÚl¶Â ÐÙÜ%Eù¼<ÒyÑKÒd´7¥±c™|0î܉ØL»Dfi­ì¾‰°!Å0íþ˜ØÀ•ôKúÂ2Ùˆçñ9ÆCtTJÄǨM„ù AƒâšËTØØ„µxô%[„/;Y’¶‹p#2Çg¤hRO"¿¬‰Dü!Lk2K}[léŽ;îˆLÙ!4­ d,oÚHÚì´ÓNí ÑVŽ]aå¤õZ$  H@€$Ð -ðÚÓ±Qy.)‘DƲ/4¯G{<6qéLåÉ'ŸŒÑ06vI‘Uv1E±ãivÔÎÄžJ@€$  H O@WG›H1"C¬ù#"XÏR´‹f=¯ß¨×¢ß¤Fò:Ö³‘îHä‹÷ê‘Všv)mÔöÛ. H@€$  Œ, ¼:‘f}/=g- éŠ#"Ý®³ <^IAÔ’×-°Ù ´L6Ráõ H@€$  HàxZ‚$  H@€$ &! Àk’´€$  H@€xÚ€$  H@€$ &! Àk’´€$  H@€xÚ€$  H@€$ &! Àk’´€$  H@€xÚ€$  H@€$ &! Àk’´€$  H@€xÚ€$  H@€$ &! Àk’´€$  H@€xÚ€$  H@€$ &! Àk’´€$  H@€xÚ€$  H@€$ &! Àk’´€$  H@€xÚ€$  H@€$ &! Àk’´€$  H@€xÚ€$  H@€$ &! Àk’´€$  H@€xÚ€$  H@€$ &! Àk’´€$  H@€xÚ€$  H@€$ &! Àk’´€$  H@€ZàøÀpäÀ7Â÷¿þB ]»t sNÞ#œ²Î÷ñüïZ6Sé>Öè¡kËK1‚W½Qz¤$  H@€$Ð6Nà½vÀraöÉ{„åÎx4Ü÷Æậ ëÎ7U8äŽ×Â_o¥øjËqs´÷ÂGß…•Îz4|öÃo¡÷Ôã‡[·[,L=ÁØÃ¼õZο¶å:”®ù¿–ÔÏwÃ8cŒ[yö°w¿Yxm³MÏ’€$  H@€j$Щ‚ áFºeÏýn ¿ÿ54l¾ð´1ò¥O¾ ó¸?âCÈ=¿O¿øÿ¥O}8 òU+Ö¢MVRôî¡–ãþÑr|*¾&¡Fkôp H@€$  H ]:•À;tÅÙcTí¦? k_ðd7i÷±ÂgG­S)g=b`xë˟½§Wo¹pü¾Û7‡ßþü»¬À|àra¶Éz„÷¾ö¿íU^»LÒ“%  H@€$ ¶èTïåý— sM1^IV´ˆ³cZDÚ¦ M.Ýô›£Œ³çÍá—?Ê ¼·Y!Ì8ñ¸á¸ûÞ û¶¬ëKÅ^[ÍÒó$  H@€$ ¶è4oÖIº‡7Z^@ù¢e=ݧßÿÚÊkÞ©ÆÿþÃoÃÇ?–œiâðÐnKÆÏúœ4(<ùÞ[-JÑ|`—%ÂÒ³Lþ<¬xÖc ¼¶X¢çH@€$  H@í&ÐiÞË÷ G®:gx®EÄ-Ø"â²eáé& Oí¹tühÆÃˆ¿OŽ\9L4Θᑷ¿ kž÷Dø¶egL^pÅf ‡-»iž2hHèãKñœ½ûÍŽ[cî¸Ãæ†?®ý¿Ã$ÝÇ go0X{Þ)ã1î¢Ùn[õ€$  H@@Fàý_˦)óµlžòﻇÃZ~²…7°oÒc…}ny9ÿ[aûÅghh½ãa-oIhIÓü+Œ;æh1úÇqY×½åóç÷ífi‰R~øõÏ(¿úñ÷0Ѹc¶¼.Aç( H@€$  ŒxBàÍØsÜðö¡+Dš Ÿð`x¦å%äùrIËNš›µì¨ùôûß„EN|0~½É‚Ó„}—ëHïüà›_ÂÑ÷¼ßmwþ?çî=xSŒ×­%Š7WK”oŠ0æè]Ãݯ}v½þÅðÔ^KÇ×*ÁñÆl €$  H@èì:…ÀkæAþëä5›¹{öM€$  H@¨€¯Xx¨¯GÅ6I@€$  H`ÔPàîu«UW7”^H€$  H@ž€¯ƒ¡¯ƒ Í—€$  H@@ (ðêsT\J7*¨[§$  H@€“€¯1Ç¥êV)ðªFå€$  H@hz ¼>Ä ¼>€6_€$  H@u$ À«#ÌQq)Þ¨ n€$  H@hL ¼Æ—ª[¥À«•J@€$  H é (ð:ø+ð:øÚ| H@€$  Ô‘€¯Ž0GÅ¥x£‚ºuJ@€$  H 1 (ðs\ªn•¯jT( H@€$ ¦' ÀëàC¬Àëàhó%  H@€$PG+ðv»9„.uìi³]jèÐ0ÞØc„o¬Úl=³?€$  H@@ 4¬Àë¹ÿáÛ_þhc·šÿ´Ù'릚`ì0pÇÅš¿³öP€$  H@¨Š@à ¼Óz;ô¿é¥ÿubhËOÃyÿc1´E—0^·1Â;ô ‹N?QUíA€$  H@@óhXúS ‡ÝýzøþW"y <˜tmwsO9^8uíyBß™z6¿…ÚC H@€$  H j -ðªî…J@€$  H@@Pài€$  H@€š„€¯IÒnH@€$  H@Pài€$  H@€š„€¯IÒnH@€$  H@Pài€$  H@€š„€¯IÒnH@€$  H@Pài€$  H@€š„€¯IÒnH@€$  H@Pài€$  H@€š„€¯IÒnH@€$  H@Pài€$  H@€š„€¯IÒnH@€$  H@Pài€$  H@€š„€¯IÒnH@€$  H@Pài€$  H@€š„€¯IÒnH@€$  H@Pài€$  H@€š„€¯IÒnH@€$  H@Pài€$  H@€š„€¯IÒnH@€$  H@Pài€$  H@€š„€¯IÒnH@€$  H@Pài€$  H@€š„€¯IÒnH@€$  H@h“À›$  H YôèÑ#üðÃÍÒû! H@½{÷ŽB—¿þúk¨l$  H@€$  H ãPàuü1´€$  H@€"ž†  H@€$  H I(ðšd í†$  H@€$ ž6  H@€$  H Iü?ËnÐÓî¨IEND®B`‚uv-0.9.17+ds1/docs/assets/pypi-with-trusted-publisher.png000066400000000000000000000611141520155276700233170ustar00rootroot00000000000000‰PNG  IHDR‘Üéü% IDATx^íœTUÆ_º»én¥E ZJ$”FºC¥AAPPQ éA éîîÎï<‡ïŒw‡™ÝÙ™ÙÝ™Ùç|?,;÷žøŸ3|÷¹Ï{ÞåÉ“'Ï„…H€H€H€H€H€H€\ …"ÒJ¼„H€H€H€H€H€H@ ˆäB      p™E¤Ë¨x! E$× €Ë("]FÅ I€H€H€H€H€H€("¹H€H€H€H€H€H€\&@é2*^H$@$@$@$@$@$@É5@$@$@$@$@$@$à2ŠH—QñB      ŠH®      — PDºŒŠ’ PDr ¸L€"ÒeT¼H€H€H€H€H€H€"’k€H€H€H€H€H€HÀe‘.£â…$@$@$@$@$@$@‘\$@$@$@$@$@$@. ˆt/$      ˆä      p™E¤Ë¨x! E$× €Ë("]FÅ I l |ðÁrîÜ9ÝÈ´iÓ$mÚ´.5xéÒ%iܸ±¾6Y²d2gÎÛ}wîܑʡC‡¤|ùòÒ±cG—ê´¿(¸6ܪ7‘ ø-¯‰È-ZÈéÓ§5ˆ„ Êwß}'Ñ¢Esfòäɲ`ÁÛgŸþ¹äÎÛo!²ã¾Kà“O>‘-[¶Hš4idúôé¾ÛQÕ³°‘kÖ¬‘#FØÆýÍ7ßHÊ”)CÍ"2ÔÈx ,0‘ 5hÐ yå•W^÷ìÙ3yÿý÷åÊ•+‘»¬|g`ü±ìß¿?ÒŠÈcÇŽI»víäéÓ§Ú¥„Ž3f¨'ˆ"2ÔÈx ,0‘ëÞ½û àvîÜ)={ö ò{:‘»¾"|`M›6• .DZ‰ 8yò¤=zT *$‰'vkN("ÝÂÆ›H€H€H€H x]D&H@nݺ%±cÇ–yóæI¬X±‚€7nœ,]ºTÌuø"2 ×–O ªFòàÁ¿‘p ‚ëJ gm#EŠ2kÖ,WºÆkH€H€H€H€€×EäË/¿,þù§FDZ\¹r6l?–† j‘‰P×mÛ¶éωÈ'NÈêÕ«eÇŽÚIºÿ¾¤J•J×W·n]‰#FéÀµ#GŽÔ¿›8q¢¤K—NæÏŸ¯ëÀý­¥K—–fÍšIœ8q^˜Êж‡ "¸råJYµj•vznß¾íp‰`¬ï5×ýöÛo²nÝ:9þ¼îO¶lÙ‚§hÑ¢¡ZfÇ—Å‹ËöíÛúïyòä‘÷Þ{O²gÏn«ë­·ÞÒýEY´hQýªGŽÑ!(Y²d‘ &Øî³rýúë¯bæÌ™ºï7nÜЉZªT©"cÆŒ‘åË—K¢D‰ô˃3gÎèä0ÿüóÜ»w/H¢˜ÐŒß*^Z·n-ÕªUìóûõ×_cǾۂ JóæÍƒ$¢±Ž×Pˆ ŒÅY1ûK–,)}ûö•%K–Èï¿ÿ.§N’øñãKÞ¼y5ãL™2©bîܹ6U¿~}iÒ¤IÏÍMü²_¿~RªT)ÛçÖ=‘èÖ>æãD›ùòåÓßû6ƒ‘! L|7Ðg¬Ÿ«W¯êµ˜5kV©P¡‚þ/zôèºÖzÒ§O/S¦L‘7ê½ÍX?ÎæÁ:ø³gÏÊÏ?ÿ,ýõ—n+I’$R @ý}Î!ƒÓïspë7á;ˆSÝÅ¿1É“'×ߣêÕ« úÊB$@$@$@$à]^‘x(†€€(+Q¢„ 0ÀÖã­[·êL‘(xFò{9zôhý`è¬àazøðá5jTÛ%V±Ó²eK}?„}Aÿððn-î´÷èÑ# ÀqHÅ*"ñ  A‰‡wGBâÝwß ©Jýù?þ(H”òäÉ“®›±cÇÚ„¤7DäàÁƒµ@:pà€­=NO#"Ñ.ú»té"7oÞÔ×AÀÿðÃúçÐŽß*^Þ|óM-Ôׯ_ÿÂx!Ñ®yAà- A‚ÐÇ¿Ð&;Ö@þüùmŸyKDæÌ™3gÓ€£6Ý‘Ę'¼ÔqTðBãC±¶‘1cFkÙ²e!΃¹s†ï9\aû‚1áß „Ûšbý>·î°þðRÄQ°Å¿ü,$@$@$@$@Þ#àu ' ¢.„"D èÖ®]«]<ô£Ø‹H¸p,àT/^\;xˆ]±b…@¼¡tèÐAªV­êð¡Óü2GŽÚU9xð vKL™4iR7ÇöÐ?܇' ÙiÑ_dýꫯ´ëW©R%í†Ä‹OR§N­ÖÛ¶m«Ç‚‚±A`BlÁéÂïíÅŸ³©Æƒ³Õ1DðŽ6àág$•1Å"^`>!œàúàá}6"í™ë0f¸¡xàãÎø­âÅŒÙE .¬Ý,ãzã³6mÚÈ;ï¼£/ƒzñâEÝ/”¤I“jÁd úoïf[Y[]AükN6;œÐË—/ë˱6‘mØ$«ñ–ˆ ®MŒmšPqwEä!CdÆ zp_}õUÁ‘ »víÒNó§Ÿ~ªÝOwç÷îÝ»Wï;ü›ç/ð² Žüǵ{ˆï%ŠUD:[wØë wß=s‚?Q/.ÖŸYŸÖ¹åÏ$@$@$@$@žðºˆÄžy`7bÂû–jÂQ¿ýö[Ý{{ ·iÓ¦MúÁÖú o}°,R¤ˆÀ¡0ÅúD Â3È„ kß¾½Í™„{ûí·m÷ºÓ^Ÿ>}t Â;!&L1çpÅRkS#Vq]Íš5Ž©)xHGH&¨ÑopsVð PJãêàá¹Aƒ¶ËÁmZ³pzCD¢ˆ/<˜C[‹UDâ÷µjÕ’?ü0ˆ[ìÎøíÅ Ø@8˜uÐå… ê®”-[Vz÷îmëœ6Ã8´G|XE$Æ‚¿›#k®]»¦û€p^”^½zi†â-R›ÖPqwE¤#Ä(ÂR£D‰bc‡µeÝÏìÉ<`-ãœJ¬D(˜‚æ{Ü£Gyíµ×ôGÖï³³ug½ÆÞÁÇw/9ÜÉDëô‹ÇH€H€H€H€4¯‹H¼ýGRìÃ'²aÆéÄ¡C‡êFá ÁÍÀBq%±„Eˆ7ûäÖJì›´Ï‹ýy¦=G{Õì×CHíYE$„ƒ5ë%Æ×å—_~Ñã8ÚíÂu„Á¾Aìs³–nݺɞ={ô>.ãr:Z§p-¿üòKýñãÇŽîñ†ˆ„KÁŒ}öÅ*"ЉPZkqwüVñ7Ѽx0uÉìß¿¿þ+p„&›â-‰µ“6mÚ ã™:uªüôÓOúwo¼ñ†Íõõ–ˆ ©M¸Ü;wÖí»+"áîÞ½[×ÁÎÎvuw°ŸÓ¸…ˆ øâ‹/‚p„Ѓ[Ñg}ybý>;[wÖLÏøà;o¿·ÒÑw¿#   ðŒ€×E$ÂÒfÏž-3fÌÐ!­pÄÒŠÐUÇ—^zI‡{âïX(ŽD$.7oÞ¬É}ûöÙB@Íp!ÌÌýøõ¡ÓÑñ"Ø?h›¯]»¶vý¬%´í!‘2Í¢ÀƒóŠ>ÁD Ü@k’„^Â=t¥Ä×&P]oußPg£FB¬Ö"n\7GÅ*"á£=kqwü!%†ÁþÌN:é¦:kæK‰µûL·ku@ÃRD:kÓ]‰¾Ã‰Ä÷š, a­˜k¼ 0‰ŠÜ|GñÒÕR¦L[¤AHßgSÂnñÒ YlñâÆD* „ÉXH€H€H€H€¼K ÌD$º‰‡8dµ„û‡SL„%:‘Øo‡}¸â¡{8ÂÍìeô–ˆt·=ŒÏŒ!tÈwïÞˆh$}Á¾Oë9öNœÉzéh:áD—QÒš‘.¤+§UD"™‰9ºí»zć'"ÒÝñ»+^0®°t"­N4îÀÑC±ŠÈzõêIÓ¦MƒL±«G|8 gµ¶i\žˆHÓ9$¿AýH|WÅ:.wçÁêDaçìŸ0ì+FæWWE¤© ÿ^ #3B½‘pN$^nXºñî?Ÿ¬H€H€H€"'0‘HÖ,’¦XÃþœ‰H«sQ§N½WËdwô¶ˆt·=88ØSûMZgË!ƒæ,I쩃³êN±öÙÞ}sVΠ4‰x°¯BÞ”ÿýW»Å(Á鉈twügpâ\Pû=´!q·&Öq$è¬n0’!¹ŠÕñv” ‰FŠÜ9‘!µ‰#rÞÿ}]7D¤áá7x¹‚‚µ gÜÝy°¾ 0gŒZæq6¡‘¦¬3°Cöddëu5 ¤5ÁÏI€H€H€H€žS‰Q8efÏÇ ã*Š3‰sñàŠbB_ÍdY39zˉt·=1¸ª[E‚WŒ±. œF¸Rp­ަ£Ä5Ökì¹Ús‚“túôiÉœ9³í6ˆqöŽ‚ý›+VÔ?㨠ˆ”°‘¨Ûñ»+^ÐŽƒAÒ8ØfonHl 0«ˆÄ>=ì×3É ´ÌºÆþ^ã Y÷-b_!>3®¯ýK•àD¤}Sû6ñrÆ$‘¹råŠm_¬}ò¡àøÁ}D§„ YƒÖ¤Qø~àHOæaíoGÁË d̵f‚ÅzÅ kÆ_WD$æû!‹+¤ÿ—ÉmM@ä"þ…H€H€H€HÀma*"Ñ+<(ÿõ×_Zl!ÔÏœçLDbá¨Q£ô€ °àæÀåÃÙuæH |æ-én{ƒE(8ï"ÃŒ ‚™Wq܉ÕñCH'ö™—Iˆ ªÆ1N˃@))ö!a‘)pyq¦Æqö²À‚1ÂkQ¬{Á®T©RZXmݺU‡áâgˆ¢°‘îŒßñ‚q#éŽÙ±pS!Ôá [°°ÿÙŸ ¾¹rå6s^©½H±? ¡Ü¸áÜû÷ï·q6ß Ìƒ)ömB!£©}›Öl°¸ÇÂ@œ± ÷ÕdZuÆ¢çˆba Ø×‰õ WÚ¼ÄA¦e¸zXžÌœWSôî9ÂqñýÆ‹ ìïE¸õOWD¤YÓ Љt>§ ½ÌÞië ·ÿ•ä$@$@$@$@A„¹ˆÄùppQʆGSœ‰Hˆ -8iöÅì‹Dˆœ·D¤»íáa›W pAGA"$dÁC¹³‡2¤„ |±H(â¨Àéó фр=ªFü˜{pœ"ÎŽï°‘îŒßñ‚öáÙrí ’A:+VA‡Àö⎡½›5d9‘oÝv´FpdœQû6q¼…5K)¾_x‰áŒÜ:¬CóBþ¯ø~ 4H‹;Oçûq, ¯£A?kÖ,›+’ˆbŽÒqT'¾{ ñ…L°_`~H$@$@$@$ð0‘Θ—¶8ŽgÂ%CØ^•*Ut’¸tÈêê-‰þ¹ÓD$öÅÁ…ÛwË$$±3\„Œš‚{‘ÄŽá¹sç´{ˆ: J0Îܹs»¼TÑþï¿ÿ®].Œ¡‰È@ Ñm-ØÇ‰s‘ì!–pŸàÈÁÁÄÃ6\ɰ‘èOhÆï©xA{p›±G!»8|ë oÀÉY±ŠH¬¹åË— ’Ä`ž¦ 7ûs±í æa ¸Ž0^ @Ì#Ù’¾tèÐAßâLDb  ýï¿ÿ^»tÖ6±ÇϚɴýŸpæ°à.¢nÀàøÝ»wO'¢ÐÆyŽxÁ€—àòî»ï9sÑó€.xéÇýâÅ‹ºëØ\´hQý݆#oJH"Ò\g!RBbâ¼qÔ‡+aæ.Ùx! €&à5ÙxbO\Ë•+§9·_ìßÄC= Ž!ÞXü‡€UDbî¬ÇFÁž’ €÷ PDºÁŽ ’Ù X“Y«²i‡ç^²ø²³úÏHØS    ð.ŠH7x"¼°~ýúúN„6¶mÛVòçϯCoݺ%Ø ÷ ¢¸²ÇÑnð–0$@†pY5 €_ ˆtsú°góÇ r7öÛY‡`’™ã4ÜlŠ·EŠÈ€Î&I€H€H€H€ü‚E¤Ó´mÛ6AÖI!a²\â8$n)T¨>žÄÕs =èo ‘a•U’ ŠÈ€˜F‚H€H€H€H€H€‡Edøpf+$@$@$@$@$@$("b9     ‘áÙ­ @@ ˆ ˆiä H€H€H€H€H€H |PD†g¶B$@$@$@$@$@A€"2 ¦‘ƒ      ð!@>œÙ ŠÈ€˜F‚H€H€H€H€H€‡Edøpf+$@$@$@$@$@$("b9     ‘áÙ­ @@ ˆ ˆiä H€H€H€H€H€H |PD†g¶B$@$@$@$@$@A€"2 ¦‘ƒ      ð!@>œÙ ŠÈ€˜F‚H€H€H€H€H€‡Edøpf+$@$@$@$@$@$("b9     ‘áÙ­ @@ ˆ ˆiä H€H€H€H€H€H |PD†g¶B$@$@$@$@$@A€"2 ¦‘ƒ      ð!@>œÙ ŠÈ€˜F‚H€H€H€H€H€‡Edøpf+$@$@$@$@$@$("b9     ‘áÙ­ @@ ˆ ˆiä H€H€H€H€H€H |PD†g¶B$@$@$@$@$@A€"2 ¦‘ƒ      ð!@>œÙ ŠÈ€˜F‚H€H€H€H€H€‡Edøpf+$@$@$@$@$@$("b9     ‘áÙ­ @@ ˆ ˆiä H€H€H€H€H€H |PD†g¶B$@$@$@$@$@A€"2 ¦‘ƒ      ð!@>œÙ ŠÈ€˜F‚H€H€H€H€H€‡Edøpf+$@$@$@$@$@$("b9     ‘áÙ­ @@ ˆ ˆiä H€H€H€H€H€H |ø•ˆ¬Q£†sçÎéö»té"+Vt©/‘.aŠÐ‹ì×^‚ $EŠR´hQ©R¥Š¤M›Öíþ;V–-[&É’%“9sæ¸U³µšuíVü‰H€H€H€H€¼H ÌE¤}_sçÎ-Æ “˜1c†z¡yØv&ú¾ÿþ{-R¥J¥û‘}4=<wïÞÝmél š5k&uëÖÕ‡FDž={V ‘´Ç¾˜¤<Öú°×BÓZ&L¨ö˜0]ˆQ8±ö¥S§NR¹reýk#"“$I"‰'–cÇŽéß¡XË-lGãLŒÝ¸qC6l(pwáRþøãzŸ.ä!CdãÆ/t /Æ/+W®Ô:*‘2d¬»'Ož¼p ^>|õÕW¶½˜Î^`8ê÷ÚµkeøðáÛ1b„[njØÒgí$@$@$@$@‘…@˜‹H<\# ‰I<¨¹¶lÙRj֬鑈„Ó1zäÈ›k‰‡ö¹sçJœ8qB%"á@B¢4jÔHJ–,)‡Ö®$R¸ªV‰ëàVeÉ’E.\(wîÜÑ÷öë×OJ•*¥Cx!,àRÆŽ[ÿœ.]:ít"´n~Ž#†MDšWµjUín! êgq@pa¡-Z´ÐN#ʤI“´«'.#Ê+¯¼¢“>!éÒäÉ“µÀüðÃåµ×^ÓëÖ¸èxYУG}\F¼P€£p˜ñ"`ÕªU²cÇ}MµjÕ´KÙ³gOÙ¹s§¾oàÀº^Ô —óÝwßu ¯$    /3鬟S/„Ðs7œ¢î×_•èÑ£ëf°ÇòèÑ£¶n¼Ð8‘Ök! úì3yZ¯yë­·¤]»vº=ë°oBƺkuG÷ìÙ£÷ê¡7É*,°Ocaq@p"îï‚°7ë¢É–LÖà^½zÉ?ÿü# Ô{(oß¾mn޲³â¥Ö"œN„0ïÚµËæ^âe³QB#"­×âÅBµ!`YH€H€H€H€H ¢ „›ˆÄC6ö66oÞÜv6£'"ÒzNäçŸ.+V¬Ð,Û·o¯¥ÐˆH„¦Âù1Bõ`ã{ï½gÛçè¬>G¿GΙ3g;·p³àrYÅÃ=û:'"á*Â-DÓˆpUì£Å^Cg/<¦N¢ˆD½pÀ·lÙbs¥M¦ŽÐŠH¸Ÿÿ†€E‰õ‚5‰3NYH€H€H€H€H ¢„™ˆÄÞ3$ü‰?$•ûh-“£A IDATÞ‘_kÖ¬ÑU›³C#"q§,X°@~ûí7Á:S°¯±téÒNE©£væÍ›'ß|óM°sjB_¹Sµ ü¹]g"YZ!¾ŒÞH„‡$"NŒp×àœH$Õˉõƒðä%JèPYs–$ßôéÓuÛ¡q"q=Bk±–°¶ØÅ^Nô GÔ° @D3iŸÕÑà¹Ô³®g+ö›H€H€H€HÀ D¨ˆD*BQQ8‚ ‰g°ÍG"Ÿáa hpÆü¡/Gˆ¢ŽëÖ­ÓûÙPp. ä ´Ð‘hÃ!òp‹àLÁ}ºråŠ 4H'WAÂü Ñýëׯëö±×±X±búgŒ a‰º(‘Þûò8:'rÆ ¶½©=z´äÈ‘C7 ç¡­(Ø‹ãWð'@ìmÄË S0Ÿ—.]ÒmР®Dz`½™ãh°~±rÙ²e¶— ®ˆHkÒŽ Ñ[©R%Ý'ì³5ÉŸÐ/ô•…H€H€H€H€"‚@„ŠH„ "iˆýÑyòä±ïHD::bîŸ9«"û/M©W¯ž>·Ï^´åÍ›Wïvû‚PÜO>ùD ÀЈHԃ̚hËѱ!Ä E¤÷–½‘ŽjDÒ„BÀ›—ó‹ós$>³fð5×bm"„/¬ëá²p]‘ÖplÜ a —ÔÑ~M$×ÁY¢I“&õ8ÖD$@$@$@$@¡ ¡"ýÄ<’—ÀýÁ>/ý‡Ñ¸@ŽD$ŽÁyŒ+„ÛW‡ÃCœY Žp@„V©REgTu$Úð9Â_‘ý€à€Ó„£ΊZ‰{p¤’ìàhô‡×clp‘°o…"2«5„KíEdܸquˆ*^ £®£„4’¿ÿþ»vq þ޹Á9žp!Q è¦M›&8¿/?p®'Ö\ÅŠõ1/X˜cûñÁè:qƤ+"{6qLŽ”ÁÚûâ‹/t{¨Îõµk×tfÖB… é—.ö™ƒ½G5‘ @ȼ."Cn’W ø+ŠH9ö›H€H€H€H€H€"€Ed@g“$@$@$@$@$@$à¯("ýuæØo     ˆ‘M’ €¿ ˆô×™c¿I€H€H€H€H€H PDFt6I$@$@$@$@$@þJ€"Ò_gŽý&      @ÐÙ$ ø+ŠH9ö›H€H€H€H€H€"€Ed@g“$@$@$@$@$@$à¯("ýuæØo     ˆ‘M’ €¿ ˆô×™c¿I€H€H€H€H€H PDFt6I$@$@$@$@$@þJ€"Ò_gŽý&      @ÐÙ$ ø+ŠH9ö›H€H€H€H€H€"€Ed@g“$@$@$@$@$@$à¯("ýuæØo     ˆ‘M’ €¿𪈼víš¿r`¿}€@’$It/¸Ž|`2ØðC1bÄGùaÏÙe ˆ&Àgˆž¶VÌÚövý^‘Þîë#     ð-‘¾5ì ø4ŠHŸžvŽH€H€H€H€H€|‹E¤oÍ{C$@$@$@$@$@>M€"Ò§§‡#     ß"@é[óÁÞ €O ˆôééaçH€H€H€H€H€HÀ·PDúÖ|°7$@$@$@$@$@$àÓ("}zzØ9     ð-‘¾5ì ø4ŠHŸžvŽH€H€H€H€H€|‹E¤oÍ{C$@$@$@$@$@>M€"Ò§§‡#     ß"@é[óÁÞ €O ˆôééaçH€H€H€H€H€HÀ·PDúÖ|°7$@$@$@$@$@$àÓ("}zzØ9     ð-‘¾5ì ø4ŠHŸžvŽH€H€H€H€H€|‹E¤oÍ{C$@$@$@$@$@>M€"Ò§§‡#     ß"@é[óÁÞ €O ˆôééaç‘Àúõëe̘1R°`A8p` Qú-Þ+ço>) {4¾Nߢ£ÖÈ‘þoHæ¤qC]×ëŽÈkÈÑo¸t¶HïJ9¤LÖd^CH {ƒÕã§Ï$Vç_å§^‘Ò†Ô¤GŸ»ÂÔþšQ«É´-'dWÏ×%F´(Á¶Ÿ´ç"S«€4y%£Ãë6¹,õgþ)K[—’iy4–@»Ù•¹ ´1s<$@$@C€"2b¸³U/ؾ}»ôéÓÇVk”(Q$A‚’+W.©W¯žäÉ“ÇË-º_ÝÔ©Så§Ÿ~’¸qãÊüùó}Õîß’%KäêÕ«’9sfiÚ´©)RD ŸM›6MÊ–-+Ñ¢EÔ AV¥Jiܸ±DU_wòäI™>}ºìÞ½[ÿ®X±bºäÉ“ëϯ\¹"_ýµüóÏ?òðáCí„â~8£¦dÉ’Éœ9s´óxëÖ­ T»uë&*TÛ·oË7ß|#7nˆÏœ9sJ“&MlΪ©«Aƒ3fLùù矵XN—.¬X±BkðàÁºî;wJÏž=µ FëÖ­ÓâºD‰Ò½{÷fõÒí‚0Ä»ÎÉéë÷$wª2©n!y9S}íƒÇO¥ÃüòËîsZ –Ï–BF(wèõ n‘)•s¥’%•<à®=tYjL+½~ûWŠdH,ó›½"“7—o¶”ýnI²x1eЛ¹m¡Œ®ˆHgýÈ$ŽnsâÆ£ºß/Ø­ûU&Kr™ý~QÝ–µ4Q·zþ4º9>[áp µ¦m•Ò/%“s7ïé~÷¯’KÚ•Í*ñº-”Å­JJ•Ü©t•{ÎÝ”‚ÃWË>•$[Šxrüê]ióÃNÙtô²$KêJ+«æÖ×8b…:Ư?*cÖ– ·îKñLIe‚rñr©9@¹yÿ±túy—𛳒4nLi]æ%é»h¯Ìkúr°á¬hCÞÉ«Åò¶×$Vô¨ÒíõÒþÕ,ºÞß÷]·¾Þ"wFV“Ø1ž¯õ ŽÊð•åä *úï`ú•úæ Žà™ë÷¥t–dòu½B’éÿ!ÇöbÆþïÎxĉM;‘£jä—§¯Ëœ¿NI‚XÑå³·Ô÷çÿ¢Ò~Màï寭—õ_Õsü÷Ék’9Y<™Þ°ˆUk EilùlÙ~µŽ Öµú«­<[ã…µo~±òÀEé­¸þ«æ3kòxºÕÔY¾ÿ‚T›ü‡üÓ£‚ž¬Á\ƒWÈǯe“å²Ê®³7däªC²úà%¹qÿ‘”ÏžBf4,ªæþùš|V¿Hzôû>ýýz;oj™T¯°|úû~™ýç)=7ƒß:î2_¬ÓõŒTßK¬› ‰ãÈç5óK…)lõÚ‡o»rG:þ¼[}÷.Iâ81¤M™,Ò£bý nœBá$@$@$ PDrG"NdëÖ­åÎ;òÑGIõêÕeìØ±²lÙ2íNBhA BN˜0A2eÊdx€!š2eJ9tèf±‰ÐØÓ§OKÇŽåîÝ»'Nyò䉊ãÇ×÷A¬A´%MšTR¤H¡ë@¨*„¬½ˆìСƒ9rD \\Ÿ8qb- *$;wÖŸAÐÆŽ[ý6l˜äÏŸßVÚ„XF x!q/®ýî»ïtŸ ŽÑvÅŠ¥K—.Ò«W/-rQ~ýõW-B­e© ?„HÁƒnÚD±¥Ë/{ä¨zpÝ×§¢¾ ÉS¶—%­JIܘÑäumíBé$‰zP­6å}ÏWï’h*6ûàðà‰zÀ/¬ÅÏÕ»¥µW_Î yÓ$”YžÔõêWI2&‰+®ˆHgýH• –n³Ï¢å%Â*Á÷ðÉSiøÍ_Ò²tf-L“—yà2=¶øJ´Kd]û²:Ñ“£*X){/Ö¼ðRžû€%ûdážsj­T°ós5w+”à ñÜ<¼°Pù  §îlÝ.W&Ï–{{ʳ{÷IÊÇD‰[âäÉ!ÉÛ4“¸E_üÿaë®ßt‡"Òo¦Š Ž€}bëµHZƒÏU«VɨQ£VSºtiéÛ·¯C÷Ûo¿é°O”E‹IóæÍåüùóR³fM.‹‚Q„œ"ã*\BÐüQ†}5jÔ7Þx¾/ÏU‰ ®Ë—/—lٲɗ_~©ï]¸p¡Àp&ñ3ÂWá0ÂiDˆªµ˜káJÂy|ï½÷$a„ڙÄý®¸n­ÃC7ÄÓµ{d— áƒkxF=ÜWŸú‡àa·yÉÌÒ²TfI¡B5Qœ‰HG™RO^»+SUæÎ5Ê;té¶®ï µ'®MÙ,/<8ãAûÐÅÛº 8tÁõÃÑ1„Ò®?rEßkJ×_vkw©Uéç{T‚+Qìè¶}“¸Î WëÝÛä%fù‘j£ÈFxÛ³2bu'Sn­)×î=TB¯¸v­f|öÓª¶ÏìEd7år}³õ¤íó=½^×.—#Ù]]»lÿEÙ©æÕUi?¯M¢Äß*œntH"28ö}4|—(_Y¹yΜHk_3ˆHpžþÇ í.WNdºDq¤ÝüTÈéMÙÖå5¹¨Ý|CWÙX5)žQFTË'Ñ?þEÿÎ:·>Öó‡Rˆé ÷(§/­|¯B³M¸[°óŒü´ó¬üyòºjã¾v¢/ ~S_âh]bdJG‹R3§ [”·T¨«3Wfq%lñ‚¾Þ*7kÁh^BdSkýoõ"¸y° †? KàîöÝr²ygQaI*T>ŠúŸ þÉ4ü ü?Cª®m$I£Úáß~¶H€“‡dÎúÇèÐÕ1bh· a©+W®ÔîŠt†œ½Ê•+;xÈš ! áA‰°Ô .8‘A…s‰÷×Ã9DÁÞGÜ몈üüóÏõ¾FWD¤Ù_i{ì§„pD¢víÚécEÞ|óMiß¾½KKd¬:a•«æÒ±ž<“œjßÄF>%"Qð°üûÞó2V9+;ÏÜßZ–T¡£I\‘p°°¯• /mT,£äI@^´L‡2¶u "/ß~¨®Ÿê¶£+GÙì1sÖg"ÎãæËÙ8`?Ûª¶e´ÃjŠ3'Ò^Db?\Ü®Áï‰DwT¶ÚyÛOË(åxÆ‹M–µ.­÷eÚ·c\½?TÿÒ(7×Z Ò'm:¦Ã‚Ïö\ Ø‹Hˆº»Jð˜’R…>"¬Ò‘ˆÄÞÊÍÇ®jAµLí‰DxkH{"íEä õr!i¯Åòkóòv¾Ô!ŠÈàx8‘fÏihD¤Ø¶J‡*ãEHqµFGÕȧC¦±v òL‰3ºÞ‡»Ë¯zO"¹­%¡ N¨^$ 4Ta³Ë•G¸´ÕÙÆËìEìûFNåN¦”ŸU¨w¯…ÿÊÅ!¡‘†©3YxÄjö›Rg«µ_ï••ˆŒ§BÍípkóÂÇÙºté ^D$ §Zv•;Û¶‹SåCµ'ξ²´ý–ªÐËBÒT¹böÇe¼£X[ë6BÖ•=‘Ö3ñb6¿WeTbì×Û¢Äd™/ÖÛB–Ñ6αDâ#kb{‰Ì¶8»ñÄÿCCr"­c²ç"‰ààNV{L]-p½³«d:3=ÏÖk_V©9®¯DäÆNå¤äçk•ƒXXêI§ÛAb&Ú‹û¦n9®B¹÷z]DÂ…ÇK—Yê;±kÏáãpC÷ôªâùœöóà*'^G‘ÀÁ2Õäé-DÈP<úÃZˆ+¦ i-(¾æÝõé>RDúôô°s®p”X¡ pßP°§0{öìÚ‰„#‰‚=Žpé°á§Ø³hD$>ÇÞI¸fo%Žá(^¼¸{:uÒ{át"!’ë@Èa$Ä$œ?$ªAöWˆ7´aĬ#'÷-^¼X÷ ‚Bû‘çèÑ£ú÷±bÅ’è~Áí,P €CWÓÊ .(2½¢ ³g϶ÿRb8YTøåpÚwøòm•Er¿ÎèiœH$©¤‡”Ë–\g E’Žº…Óé¬=•ë‚}ipqY; :¸`H2óƒ ¼¯ÄÜO¸6„ˆ< BW±ïlmû2R6ëóÌ·ö%¸~¸""!0p§_å\Aªv4g"‚k GSUÔèÕ‡e»Ê. ¡‡¤(í~Ü©ÇS@íaÜ~êšT´Y±)¥÷Ð9j§¯nH( |ò»íÄuéôZVí:æºR'&£8EWoUQǺ×4Ç*´ÒY@CH-’·¼”,®à¨D„6"Ëèu%`Ó«ý˜p…ácn >V‰X¼`À¾U$¨Á>S¬$¶AAæZd‹Å@Œß:WŽGXˆHìI\¢óiJèãE÷$QüàÎ:+peßž¼E¹‰¹Tš úÜS¬Ínrèû ©ï¸‘°láÚÿ«=R¬Ò©—Ⱦ[O » *tBBßNdµOmbÞ;«}É—Uòœ¿ÕžGdÓµç~VíýÅ÷øUµ†úUÎ)ñ•˺\‰_¼4@"žàæÁ9~B$`%°¿ÐëÊ€üÎçέ"ºˆ' æ*j‚ø’cãˆ÷€"ÒÏ'ÝNÀ‘ˆ„°C˜sçÎÙö*š#> 0/^¼¨…#„!â$I’Ä&Êþ Çn~ã9^}õ¿=t'NœÐîâ®]»´Ð,Z´¨>âŽ&ÚÀ^E„¢ž9sFàŠ–*UJ;£8^ш„ð„Û¸cÇ=$çA’„¤Îœ9SŸ+iŽø@_òæÍ«¯sT—ýšøàƒ4ëN\1Ü8b¡…Š8zGL@Ø•U.Â@Ί̞}ïS{µ®ëì¬uÔ^8d²ÄÃ,’Ž Cæ_긅’™“ÉŠ¶¥ŠHdgmöív]WZˆÐÙYjfe%H º ˜‰ôœz6IIìÇ\?\‘e”_§ JÿÙSq\Çñ+wuˆª9 m9jÏ#cÕõ]Eb™ÌJ„7z9£ôR- ÜÓvêxìYE6R}·1@e  IDBøü£BqožÔ e´ ëĦ Ëh5¯Øÿ×Deð¬–/vj­"Gµ¼œ1±žSôÙj‡ª£Cbþ?Á²øV¿QgW…Ck\°‘áy‡¬RfÏäžr¾Qb«¾b¬ÙßèèßR¸¾Ÿ*q¸[ÙðOW3ä<òõ¦ã:QÎë1#!TqåF¾¬2½bO#Ž&ARfdF­[8½Nôã ‰þÎPÂk a¬ÈÔj°í¹cLÕ‹ì‘]ä²Î&ŒäDx‰€ïppóàˆG$ð"-"YüŽ@®þÛ ïw÷‘SDúÈD°¾AÀˆ2ˆÁY³fùF§<èD,Bmá¦Nš4I‡Ø²üGIUJŽY'HÊ ÜRsæbd3ÆøŠJ>Ó¥|vnŠ‚—ß«ý©•°»<ä-í–úz1{"ªµjÎãôõ>³$PDúç,SDz>o‘ž3d DÀgÏ_†‹0VœO‰Xs6¤«}Ç!ä³T˜c „‰bÿW¸¨H¶¨cÅÆåƒ³Èã5óˆ1¿ªÂ “«c0º¿ž];ÀpÏ+§!Àû•K·E9ëpé|™Æó@q&^xœPßϰì/^2`° „L€"2dF¾xE¤ç³Bé9CÖ@ED^»vMg‚E¨-ÂpqŽ%öTºZ "­GD¸zŸ?]W.[2fyTZ—ɬÃüÖ¾âOÝU_1Öײ§°‰ÈL*6øýzJ IDATÇûŸˆ|>n®OÔžÞÃê¥ÁCµ71ÚY==5‘J…h_æùK'¦MDW¡¬aÙ_sBª¯/ŽÄ("ýsò)"=Ÿ7ŠHϲ    HH€"Ò?'"Òóy£ˆôœ!k    ˆ„("ýsÒ)"=Ÿ7ŠHϲ    HH€"Ò?'"Òóy£ˆôœ!k    ˆ„("ýsÒ)"=Ÿ7ŠHϲ    HH€"Ò?'"Òóy£ˆôœ!k    ˆ„("ýsÒ)"=Ÿ·‘£G–¿þúKÆŒ#©S§ö|DÔpéÒ%iܸ±¼ûî»òÁxPo%   t‘þ9ÑžÏ[˜‰Èýû÷Ë‚ dÏž=róæMI˜0¡-ZT6lD,vïÞ]þý÷_7nœdÍšUèÆR¿~}éÝ»·”-[6È(  ;wî”_~ùÅáèëÖ­«ë:th¨éPD†o    HK ,DdÂ*$ýˆ~A˜>{øH_¼$w¶ý#—gÌ•‡'NGZæÞ8E¤çÃDDþðÃ2cÆ ‰7®”*UJ‹FÉ-[¶ÈíÛ·å믿–)RèÞ?|øPÿ.iÒ¤¶Ñœ>}ZZ´hAéùü²†0"pðàA騱£®}Ö¬Y¶õlßÜ… ä‹/¾½{÷JŒ1¤GòÙgŸÉƒôýUªT £†MµŸ|ò‰Ä‹Oºté6 øP­‘i¬>„]! ¿"Ö"òáñSšGÔñ%z²$úç§·ïȱ&äÁ¡£~ÅÊ—:Kéùlx]DnܸQ, ”>}úH‚ l½¼ÿ¾v‹/lÏáb~üñÇ&"áf6kÖÌsº¬!\9rDÚµk§Û1b„äÏŸ_®_¿. 4п9r¤äË—Ïæpãwp¾³gÏîvÿ\‘pÚwïÞ-Q£FÕBáÛ~ø!E¤ÛäÃïFŠÈðcÍ–H€HÀ_ „©ˆ|òTö~݆&f¦ô’qÂ0‰™1Üü}œîþ‰¿b‹ð~SDz>^‘OŸ>Õâ;wdÊ”)A¤³®b/äòåËå×_•˜1cj‡®µÀÁY¸p¡þ•;á¬o½õ–”)SFzõê¤^ˆŒÌ™3ÛB_M8+Bn³dÉ"sçΕ“'OJòäÉåí·ß–ZµjyNœ5xÀãÇ¥fÍš‚?»·aÃ2dˆn /ðbb¢‚áÐXWî”gϞɡC‡Bt"=z$ÕªUÓMtêÔI*W®¬®Q£†OŠHŒ+J”(Á"‰LÂ*2Õïï!  O ÞÉ?l();¶‡Ë‘Zÿ1Ò§•”m›Iü’Å$JìXrß!¹4q¦ Ýa›¦T]ZK²&uåÜà±òèÜE}}¬¬™åñ•«rå›äêw?Kª$E‹÷µP}xú¬œ1Aîlþ3èsyºÔ’⣦¿Ì+-nypô„\÷‹\ÿõw}]ôÉ$ÇŠ”}UŽ5j+÷vý÷\'nyéÛ¯äéÝ{r \ yöࡾ'Þ+…%yóF'.Aèî?wÈ…±“åÑésa²Ì("=ÇêU‰ë:è‡äV­Z¹Ô;{ rëÖ­2oÞ<½/2Ož<-Z4)R¤ˆ®ÏˆH„Ä:*p¤²eËdOdhEd’$I$zôèú¡NêÚµkeß¾}òþûïë=,¾G mÛ¶rôèQ©S§Žvú°>̾Ù%Jèu³téRí@fÊ”I&Mš¤—Ó§O·¹…ÅŠÓ÷ãÅYoÛ¶m“~ýú \vüW»vm)Y²d™,Y2íœÃiÏ;·®£k×®A@aO0BY‰ÈàúaÄ/*Ã÷"Q¢DºžM›6‰+„ô“'OdòäÉÚõìÙ³§œ?^(¹råzaÒ0D €ÓöíÛµKš3gNýÂæçŸ–k×®éûðÊ!ƒ¾ß‘° îz„°ÿöÛoràÀ-P+Uª¤_4AÈ£àÞùóçëv|o‘Ø ®1 ú>qâDÍ4~üøòÎ;ïè—öBwõêÕ2aÂùî»ï$V¬Xú^¼ÐjÔ¨‘n«|ùòzÎÀcÅŠòçŸj†X3qâÄѼN:%yóæÕ/°ÛÙX}oå³G$@$@I ¼E¤‚÷vï“cïµÑC+›dž6V…¼Æ“'7nÊÓ;÷$FÚT"ŸÈ‰ÖÝåÎÖíú:sïã —%zªärÿa" чr翵˜»·÷ ÄT¢4Zâ„:tö`¥ºªÎ»úšX9²ÊKÓU[ ãË3õì¶pʵù¿É¹O>×?gž1Vâ-(—§~+ÇMµMQʶHòVïËÍ¥«åtOõï×|SÒPÛdÔ³Á£ó%ªúÿòhIÉ“«×äpÍfòäÚ ¯O1E¤çH½*"ñ€öùçŸë}_¯½öš­wØ —È<¸á?{‰ßÉYb<ÔW *䑈D8ä Aƒl}Ä:¤±¿íûï¿·=¨zŽŸ5x‹ÐÊ•+µ¸ëß¿¿~™qñâE]=6ÌÄ’=AT@,`ï-œË»wï깆Ã]ÈñãÇk¡ñ‰õ†Ÿ±ŽS¦L)íÛ·×/¬{"‚xÃÈ«W¯jÁeú€~À͆p±‘!õ{‹!ŽÑ7ÔùòË/Ë{ï½§Û@?06„×Âí„ÂߑЪ[·nzüβ CXacóæÍµ…3 ‘ºdÉNž.]:-7oÞ,S§NÕÂÏ^D­wv=Ä‹Úa ¡gÎ9{ö¬… Ö{%!²j„îEÈ*Ü3¸fƉÄQ3pÆM±ŠH8th.–uß­«"Ò•~|ûí·ºÿpZË•+§û†uˆPN8£põ°·ãÀx\)VÍ15ßS“uÕš ãèÛ·¯¨Va…ïuH×C˜›R„C ÃõKD' rŽ þ½@¸ª»›ŒÖ~@CðCXÚ—3gÎè±Ïœ9S RˆmD -û±"<.(¸šÌÐß|óüý÷ßú% E¤+«ˆ× @ä&Ö"{ceËlƒ|cÑ 9Óo¸¨ð)ý»$uÞ–4ýƒÏ˜~è͆zo¡3™~DIX¥ü "2˼¯%vžryò¹8~š$®QUÒ~Ò]‡±î+úF‰OѪ±¤P{,‘MöpµÆú³L_”xjæù¡ãäêÜ’¤^uIÓ§“ÜX²JÎôüL_“nX_IôæɃìWÂ[½QÏ닌"Òs¤^‘x…ÃàlO$ÂÒç"rÕªUÚerbëù4°O ÀƒP@Aè'öÙA8bÍ!t ¡™.÷DØ'Ü<„(;‘&$ÚˆHkúi‘øqË ŠÌþCWE¤+ýصk—vÓ¤I#¯¼òŠND‘ƒÐ]„wÂAÃ÷¡´9räp §½°Ây­p!ÇŽäÈT–8qbîjV!]‰_$©ª^½ºæ‚º!!MAv]„C#„µjÕªú{†0T„¨Z ’oÁuT à1oø.#öH"L…"Ò¥%Á‹H€H€BI LE¤%;kú‘JèU./‘#uU‘zy’¤Î;JDvÖ?ßýk§ÃÞŸìô<ìÔk"R…Ãî+²ˆÔ{u“Û›þ”“jofÆ C%~ÙÊ©T¡¬k6꾦ÞO'óqÖÿGgÏË™¾ÃB9+!_N2£®ðªˆ„ÓWGy`ö Y‹«"ÒâlOdhHˆ8K)´gÙY…³šD-“CFWß#`=$¨š6mš‘p¨æj2íΞ=[wnÞºuë´»õå—_êpVˆ¸eõêÕÓ"Ó‰õ…µZ¬ „Æ"”3‰õ…ϰ?/U\é„„Æ1¡˜ÛMŸ>½Þ{ ñŒ¶] gµ:‘ˆÀwÞoý?3ÛVÒõË–-Ó¡ªØ7ié tý´ŠHS7ÂU±gŽ+ΙſøÞ!ÒŠÃ^jG¸µeE½)‘!Ñãç$@$@î/‰ì«Ù~£÷("y ’Ø Ä-VH2O£Eå¡**lT9wΊ§"ÒÎz¤Ö*Cì1[SÇ+øj ½û"Q|'çÒªÄð µ%çªùòLõa·eEIѺ©ú¯‰<0 îzdÅõ`ˆïDD.™¬°àý—àæÈ‹½§W®\±õû4ñ9BM‘tB¡®´¨¡É(¨ ‰wð'\I¼82Åi?Vß\õì @D/‰1¦îÙ^’6¬¥²–^—Co7Ò™S‘„&ûâ9‘ywûn}väã‹—5’¸/’ûÿÐ{Q<‘¨Ãì“D&×S÷×Y[T|U2Œ¨ûbuq½ÙoyyÚwúxû}—13¤ÓuJôh*äõ¹0f’<»ÿ@‹åx%ŠÈíÁ'Ótwî)"Ý%÷ß}^‘¨Õpð _ºtiý‰‡B8 H‚$!Ø×…â(±Îƒtâ„®áaGˆ,ZÝ‘Øë„,x¨Ä'ŽAFLÔ‡Ð8“HÈH„Ñ!$»pMðP qÇÉEX|“ÖÖ Žê@2ìçƒAÓ¡aʉ'´ë…pQ8x8†bÉ_P\‘¸ëûõ°nñ²!³Žë © ÂNá–¡~ô5¤~ /p谇߼Œ±eb~‡Ï àÊö"bnàâÅ‹µ€L:µÞ{‰DC(‚I…ÈD‚Ÿà®‡8FDBÁÁ®+þ<|ø°‘%FB$ÊÂ÷¬@ÚM•J¥%WßQÜý®p#‘áÉŽÉÕ$ؘµ&ºB}p—­/ŒP—;"Ò~¬¾¹êÙ+  ˆ$ž"2ZâD’]eR?ž>×ñÂèçÆFœy$ÓäQUÙ×ï¡ ®®Å‘o¿˜¢¯ó†ˆŒ;»:NdŒîÄÞ%d£'Oªë¿¾`‰œ02Èt$z«’ÊÜÚ[Ÿýˆ ¬ö"C§îÑN‹Ðg÷îË£ËW%†Ê%VL9ñ‘:¢ÄîœJoÌ7E¤çÃDD¢[9Å46<,ã.Ä Uˆ9‘¸ýx }ªï]²d‰\½zU2gÎ,M›6µ¹²¦ÞjÕªé1cü\ÕZà²vîÜYºté¢û×}nÕª•*THV¯^-&Lï¾ûN¿<0lÁ¾E‹zN1ßÍš5ÓëëÀzžÞ}÷][ÐÝ™3gJªT©|àÃ. xƒ€‘êű.òòØ\|¶5UQãÇ•›~óÙ.úKÇ("ýe¦ØÏ` „FD¢" ÈË—/ëz§Nª×°aC-¾ÆŒ#¹rå’]»vI=$a„Z>|Ø©ˆ„H¹yó¦¤L™RÚ·o/·oßÖÂ%A‚Z8¡n8˜Ÿ|ò‰v*ˆtÖ§téÒÙÄ®8H:þ¼®BíBìÞºuKÿ<{öl}ÝØ±cµàŒ=º ž“'Oêë!ˆ2eÊôB½è7ö|¢@hgË–M‹®S§NéßA0ƒƒ'Nl›#"Í/Ò§O¯…Æ ! göã?Ö , L=z¤ë›>}º$I’Äíq®Y³FFŒ¡›GÛ˜SÔ!\µjU=o¾(ànD¶é¯x¥K—–6mÚè¾üøãZ4¢oè/„ëÖ­õxP°Ç/ \1ˆHOQ„8C0ãó^½zIÉ’%u80E¤mÉð §Zv•;[w(0C èÄz)£DO•B2LzþÜÀâ>ŠH÷ÙñN"àlO¤1èªlpÈ Ž là2B€uíÚU^ýuéÝ»·ìرC7n, 43fÈ?ü •+WÖÂÊ*˜ìH·‘#GjG ®ß¥K—¤\¹rZˆþý÷ßZX ˜}š®ôÉ8tpA!ád¡W®\Ñ®"öhÂmCÿP&Nœ¨…#\2ˆFü®Ÿ©®+>³¯7Mš4Ú‘Û·oŸ¼ñÆZôA@¶lÙR×ëlO¤• DÓ«¯¾jã–:ujý3 \B8¨YpFáÜáï^+V|¡?®Œã‚ã áhD#„*X 8Ë´óxзo_-ú¬Å‘ÀÃüÕ«WO;½p4áöÂM2dˆñõë×µHDˆ„ \©R%[ÕæEæœ"Ò‡þ±`WH€HÀ‹îþùœlÞEÕøLýZR©I J/öRU˜œ¨Q$jܸ’a¢Š,+ÇKGÞj("#ïÜÔÈ­"2®úÂ8ˆ #ج²~ýúrãÆ ºˆHÒš?~ínÁQ„ûh¬àD$ÂK!BQ !ôL»%J”Ð?C´Â1|ÿý÷µëéJŸŒØƒ‰ä,(IK8/epËP Ñþ¨Q£Î17ˆ)GõB$mذA‡´Bü„VDa°UÞxñâévP º,X Co!(ŸQDúÝ”±ÃŽ„&œÕ*"á6BÜ@ Ô¬YS Jˆ;8x  p±Gî^p"ÒZïÅ‹µC†¡h/"ÓéHDÚ÷)´â ®DäèÑ£uûp­%Ož<ÚYuT/Ž¡X¿~½ÁÄ>BwEäâÅ‹eüøñAÿÓO?éÄ<»è’•)SF‡‹^»vÍæúº3NˆHÃaªE5Î#ö\†Tœ‰HˆGìׄk‹×{$ñ6ú5âHD†« 7”NdH³ÀÏI€H€H€ü‰E¤?Íûꔀ·D$ÀQ!¨ g.\(*Tnݺé¶]‘‡4a–¯½öš¾á¬f(öá¬Á [wÄÄÜU´ Qˆgݸ""!ò ªˆB¨ª}qÄÄ^Dš£Q Î!*±â ¢ÝjwÆ ÇBN,Âtq„öš"¼Ž!Š;ᬸó‡yÃü¡À¡…pK¼x07>³‘•ÿ|ùòéë("ù @  ˆ ¤ÙŒÄc îˆ<Ì#ÁŠ+®šVìq„Øë„ðNWE$®µ&|Á><“åûqè¼5±Ž·E$Ü>8‘+W®ÔýÆÞDˆ$‹ÁY…®ˆHÜ ~HʃŒ²f“µ:~®ˆÈãÇëÄ4(¸Ùn OD$ƹ|ùr ‰…ðßéÓ§õËÌ›«‰uàŽB F‹M¾þúk$ nªÉ¤‹ð[ô•´ƒûÀû`ñç/¿ü"+V¬Ð!½2d ˆŒÄÿ6qè$@$@$ˆ("qV#ᘜ%Ö ˆ¸Š®ŠHˆ„”ÂMÄ‘ØWg„DhD$Ú6G| ñ Ä \MGG|„…ˆ4G|@d!¼±xñâ‚} Öl¨Ö½–öá¬úŽL$–ÁGˆêܹsÛV™+"cŸ$²¤B„!œ{$=‘¨Ǫ@C¬"„!»˜Ã¬Y³ºtÄö7"¤¬Î;§!œÕ¸™f°H°³nÝ:=¸ª¦@DâEG„°"û-êË›7¯¾„Nd$üG‰C&  &@À“Ë¡‘ D*çïIDAT„L 4á­Q5û0­"/+e–…H€H€H€Ed Ï0ÇG$,WDä… ôžH„Æ"®²µ8J¬Cì$@$@$@$¨("uf9. —„$"‘´{S¦L©õ@vVûBéj^D$@$@$ ("d"9      ‘áA™m @€ ˆ ‰ä0H€H€H€H€H€H <PD†e¶A$@$@$@$@$@B€"2@&’à      ð @”Ù ŠÈ™HƒH€H€H€H€H€ƒEdxPf$@$@$@$@$@$ ("d"9      ‘áA™m @€ ˆ ‰ä0H€H€H€H€H€H <PD†e¶A$@$@$@$@$@B€"2@&’à      ð @”Ù ÿÂ4gœ h¹EIEND®B`‚uv-0.9.17+ds1/docs/assets/uv_gui_script_hello_world.png000066400000000000000000000076411520155276700231630ustar00rootroot00000000000000‰PNG  IHDR(f÷@›æ pHYs%%IR$ðSIDATxœíÝYPwžÀñ_«µÔ­µdI ‘1GŒøHbÁÆ9&•q<5ãTmÅU™+[ãÔ>%»[S®ÙÊN^ò°©©š²³»qöaY§Æ³Ùd’ñ1Ž“qb‚m Œn1 t [êCÚl0v@:†ßçV÷Ÿ¿ºøZÝdD8N@­, |>ŸÚÓ@h )--Õ¨=„Ö" !`x©ÃCHB*ÀðR†‡ 0<„T€á!¤ª¸Ãõ÷÷·´´|þç?çrPSS{à¯ÿÊét÷[ ´ 'ŸF£§OŸ gí5žD*;08r½{( ’y‘ I’ðjóAG,†Ú“](‚ ÚÚÚ¾s·¶¶¶ezRK OQ”t: N$“ñhtê³?~ì½á7Ø*ˆ‰+õRÅó¼V«-Ö\‘Zò £ö4¡µµuWCãýÛkkkÛÕÐØÚÚºXRx½½½Í-mëj<¶Êz1¿Ñu>¸A„ì­´óO>ù¤ÉdÂÛ<ô=´}ûö _7ݧ½éê.|Ý´}ûöå˜@UäóyI’ºººÎ7·”ÖïÖ—8#ÙD$'g!'çS“*»wï6 Å.BÅrŸö–»:(8~âÔ‘·~ïoïЃã>k˜+ÑžÓé\ÔbÀ´x<ÞÔÔtèСM›7ëõzÇóÎ;ï ‹¢¸è±N€ƒ'§¿è?<½zûë;=x÷0W´4°˜÷j.ëLЍµµ•Ö²­­­Kܧ0N§³Àð$I ƒgÏž=tèÃáxõÕWÛÚÚ’Édaó¸]Ûtqý‡=s»µÃìmh…,êßqµ'»P°¢¦ß!]ôïît: §ÓYð§e³ÙÎÎÎìß¿ÿ­·Þ*l„Öš¥~ZI’Å\GAhÍXRxF„矾®®®XBh-XÒë•F£1›ÍÏ=÷œËå*Ö„Z –t‡*~",BêÀðR†‡ 0<„T€á!¤ !`x©ÃCHB*ÀðR†‡ (øàƒÜn·Ú3AhMðz½0žÛí^üßJA/5R†‡ 0<„T€á!¤ !`x©ÃCHB*ÀðR†‡ 0<„T€á!¤ !`x©ÃCHB*ÀðR†‡ 0<„T€á!¤‚‚?ŠY 4 }7üàI—ÓTª[ÈCsD“£èË=Tey¼n=»°£îe É?Ò;îü‰Ëa­*¹{ä-ë9БbM~moÖñ“­v+Ï,æùÎ7à‚ÎBó(øOh:wö½O.NeøÐáDèZÓ¿øåÙ«×cbZYàQs%FZ½>ø´}ìš_¼µI™Œû¯|ñþoO¼|É/%¥<ä³qÉûEóå–ÿnñ‡’Ò"ŸïÌ©BZ™óèŸ2B«ãRÓî¬,xS×ÐèðxàÖ¦dB ù²·Û?r­7”ÎÈ ËR(`ˆœËfe¨‚_ê*‚Õc²p‚“ „•H4 ¤'‰‰›ºmu`±$ÆB9IHg3QoošR´uëV‹á!5­ÈÏ_^91ŽE"I ,ÅðÖ2³^»¨£@C1œµÌ¤×ν;ãÍZ³Í›¤ãQ¿”wQrl<´xvYFlÔÍñ´X!‚˜Í„»5ä6¾ÆmdµJZÊ$&ýS)I‘€5Zy³ÕÌ¥ålzj,.k…&’Iš·$3¹™ßtÆá )2æD4„fE“jþêØŸþ÷øÅ€ Ô:ܯ¼½o‡èEF‡{ç+oïÙán¬˜½'g× ®zª%›,ï Éë„ß—ì§ i-ëKÈmá¡äfRŒ±²õFAŠˆN^ÿê½ßœè¸š¾B­ýáÏ÷ý;j°êSc]¿y.ÈDâeÌåËvÏþÊ'è ̘pòÛãB‰kï³o‚_†“‡V¥%†O„¯_<þ¯Þ¯ÍŸ³w6N/ñMN‚ÉI„:޵Üp¶göUs@Ź|àê@¸„•îý‡ã#‰pwDZ?v“ôö}OÛA›óeⱑ?j—ͦÙ늤‘fKÊ„›ÞüäXH–L¡h– ek«ÖÕÙ\ÑÑÑpZŒú&ù‰Ñ«e®-ëìnZÔB`àRSÛ™ AëN·›ÝeLCr0ö^ýðÓòŸ>¹ù!Q#†‚Þ¶AKYnÃö†gJ·Õhß>Y"Ì>Ü!HÚœ¯7åïβ´ŠÖˆ%†—Î&Gû/~ÖÃÒì·Ëë9€l"¨gM&”ÉøÄ7_ü®#¶yWùOìÀäoòÝèy¯wÂedjÍ÷øÖQW¢;öoýùû«a|m´óü»¿:=bÕ·ïÜâÔÎ\Ð×Ñ ï(û‰äØXL´Ž…sº€nÓÓ¦*ªªÇ õÇ2áø8Œ U{êËlù,ÈÞž–®ógÆ_þÓs ÕÏ8¢jþðèg'?ÿ}{c½Ù • Dsƒió¾WÿB¨0ø{Z òY¼=íß4}þáWÏz6xŒ£¡æw^L]N¼ÇBh¦%†g1Ùw½ðw¯m©/­7ÞÙ˜è;ÿ~Ë•sq¾]`l?Û=uåÿŽQ@Ê ‰1M>ºí±ZÜ+¼dB ¥ò£‚½²®X ¿ÞP‘zj×±^kx2ò:€·ˆ,ËU×í<Ó«»ÜÞ™„öá´¹Ý^ö MY 0e»C:ðÕKÁÒ“ÅÀƒ,C(KZcZó¶jÎi x<5á+Ã1ž W™ÝUY#èØÙgI–!Š1'«k÷l2o°EÏS/Âxw„[Ú EkÄã(Æ(T¬hÃCµ%w6&²Ã¶žˆk@Qò’F„Êò¶G«ì °F¨¬{üaäÀü+J^RD0h–ÓO¯½êHÖXb”ZŠËÏÏÚ]C3zgד#‡FÆ„¡¼vGÙ7§ÕmV³«,30:ô[ÈÜúm§ƒ|dIQ(Y£ÓéI†h“Ž£M†LNÉ+ ,Ãhy=­™³ô›Ïƒ$)6§³9RÇ$m*átz#DVÃ21Z~Ë¿¸BR­g ¬jû³ûãž2‘ÄíÇý=÷9Šd ®ˆ™d r,@>­d‘§ð,§ƒ¹1P4e6Âk÷_íó˵›«*tZÐÙæŠ Ý×=¾žqšÓ•ÿÈd·é€ÈES¤Då2é”"Š´r"•„X¨Õj˜[×”÷@@ÑT.­I‡bI%-‚™TäD$™NÇìE8ehõ[þ 9žtvª}|ðú¹æPwÝÛ{ YèyBkÊ ¼âY ëyfÿ¶Ïºo^ýŸwÿí"ÇëH`MPùDc½Rõ]G}=ìmÿŸCv`sq9ÊãO¹·Ö×èaNw@Ð@;ö’*·þ“¤›1X×  ¥H«–sU—Å;%k°rƒ‹×ÚHÐí®yls<ûêÜo?m×·eHFâJ¥ó…Ý&“ÉÞçZ Ý5¸ÃÞ?œýßÃ%«Ìè•ñ”¾îû „n+8< €Ž·­³»óOÏ~Çðœ‡JxËÆÆ—9vfèøM£v:¬VGm©Éê.—í&#¥!‰ùº~üãs° [÷þ½góÆÒŠyæCmÕuž†:c}݆ ·qzñ¥„å××6ÖnªÝ µÖ­çÙ ¢êñF=ŸëþÍ‰ŽŽÐ`\O¼´»ñåÆj ˜ dîL HêÛ- ITTmÝ©‘ƒ§þóòµ®Ø€ ¸öîuº¬4ÛܳÐ<§ÓùÑG-þ£˜s™D0™ŒJ”æggþ´ÝõF9 Ç·߃ 8«`¢Jhej,žçxÆbä©L:tŸ£(’Ö›6ž½çO¶’Žf’1ZÇ8‹YG99›™›’Ù e.3³ZJsgïï\!®¤Äh1óÄ­w®ä9ž¹×)‘MEÇ‚IQÎ$ɘÍtF¡ÅÜ]g¡Yzzz^|ñÅ‚ÃCb:<\ýFHB*ÀðR†‡ 0<„T€á!¤ !`x©ÃCHB*ÀðR†‡ 0<„T€á!¤ !PàõzÕžBkÅtn„ÓéT{&­9ÿì­xr›¹ˆIEND®B`‚uv-0.9.17+ds1/docs/assets/uv_gui_script_hello_world_pyqt.png000066400000000000000000000043501520155276700242320ustar00rootroot00000000000000‰PNG  IHDRuÉ‘ pHYs%%IR$ðšIDATxœíÝOLéÇñç{1ÄÁ»U»²Wm*㬴=ØQÑ^Z‚X·R8$RoVÕeÃeãKH9¦Bj.°²œÜCÿI+”¸Ç +Y&êaEJìa»QvE–! !±=3{°±ÇÆ63æ±à÷Q0~=~Iüe^¿8‰ðûý|"züøq«§pD©Õs8j3DÀ Q0CTÌ3DÀ Q0CTÌ3ÅΠ'OžÜ½—8Èà þæ×@à g8,lEu÷^¢÷WØ?é7O6ÊŽÜþç¿F?ú£¢Øz8€CÍî³Ü-nÙ´98üÓ7­Ÿ¦uñßÿdt]GTpØ}–wú|?ùQgí1¦i­N‘ÿè»§[uÌ š ø‡dCéŸïëK‘J¥z{{k[ZZêëëkÄåàÒ±¶öø¯ÿ^&É‚„d É‘d É t´ÿ~ø¬®ë;;;¹ÁÇãñ|ôç;$Ì?ýáû¼‹Íg•£üZ+•J8÷ÅýÏktµ´´tvà\*•jÄD%IJêë I6$ÅŠ!)†$ç?p»ßI§Ó²,—ßG˜®Ž ç|öÓÛÛûÅýÏkt•+ªvuálK½Ó«œô*^¯rò„ëä ×I¯r¢Cîh—Ý®ÊçqudÜÞ4Ç<(tµ´´TvS£‹"ûQmï¼""¯Wñz]o´‰—æÎæ«gßnonìhOw´íÌNÅ{¹½iW{&w_€fªØUŠ"§W*O»üôÅÖ7[Ï3ÛºHË.]VtIÑ%©òºÜÕžq·áJ­QÖUsŠ"§Q}µñíó̶ìÖ%·.¹tIÑ%E—]HFÅñî¶´»íeõó­N ññBÅ#  10µºwüÀ”£9Ãqfíª9E‘Ó¨ÒôJr’Ë•Ü/]VtYÉJU£zù†§FTµ _¥ÅØ’âægiôÚ•úNÈB8ÑÂyBÁ¾ûìœE%»ôâ’OɿВ]H&™ÔÖÖæóù|>_gg§ÛíÎf³üüêÛ=uNmøêd?ÍÜ.Vµ:?»Ø?yu¸Îó±0håDaWáUqߢœEe]òÉ–®$É0MóÅ‹›››ß¿¹¹¹ùüùv:yÿí_öý¸Þ¨(¹d­jáFl±ÿR$TïéಮúšÖ•Ã+•eÉ'®TrVÃ4MÃȇ6 Ó0 ]× C7ŒÊ+C;JªZ¸=C£×® )°«ìuTÓºrx¥²,ùdE—å¬,geYR®%#—ÓîÇyõÏ.tåÚ(Í\ŸZÍ7u¡¥K?8L*îL4§+ï¨H§_¾ÿVHSH¦†L!L! !Ì7NÃ4^Kä£ÚïuEèÝ÷ˆ¾üß* ï^€-/½W1|a”ffçhv¦òš[jìõ5aßÂATáðÏÆÞy«ì iš¦a†!I’$G*,í¼R¾0J3± W>&¢Õ©gÊG\ì?={û/ýK?°aßÝóFwe7*íÙöWÿÿ®¾ÇОmW½mø³G“_žþPäZ½óhòúéXɈPäR,¶8zç>šj„£·õß××·ï»Ôs]5è]êÂï÷ïûhšö·¿ÿ#“©ÿ­±¿ûmäÔ©SÞq p´[QQ6›Õu½îGREÁqì.ÿEÁßÛ°ÿš3DÀ Q0CTÌ3DÀ Q0CTÌ3DÀ Q0CTÌ"ÚÚÂÿÊÀW*fˆ €¢`†¨˜!*fˆ €¢`†¨˜!*fˆ €¢`†¨˜!*fˆ €¢`†¨˜!*fˆ €¢`†¨˜!*fˆ €¢`†¨˜!*fˆ €¢`Ö¨1Ÿïüôúž[®æd^«™ÂkéàQU|–5ã©·ç1Ö§ÏW:K4æñ×§Ïûwv8¼ñò¯+¬RòÖÝbCk+IÚ{D wµbvp|⨂C#*%WÖv?OÌÇÕhT-=BêÈP°“@²°G“¢Ê-•|>»ë%ËøêËÈ`¨›hy5ëúê2uG"Ý¥G¨;´ÛT•sæW‘»·Vš^"V¸£ïbÜö ÇT3¢ZŸ>ß3N4MÓæ¢ñ‹û½Ø²Ž×LÐxO•{ F¢ÅåÞÚJ2ì «Ö# Ú8gr¼ç2ÝÔ4MÓ&Ë$ó]ŒGç´¼¹h¿ plðD•ïñ•°~?O|:žT'nމˆ?™P“ãŸÖºZ•Œ§àØÍê÷è €‰ù¸î¢àЈZWÙøòU^‰àЈš»i}u9w§`¨»xd÷}y<©N|RíÉ uS2OZ.;]a5'­Ñ8;§u2‘(Y¶4±âÌÞ7p5gK=8vïÁ6{n…kÿ<¶d|Ï8UÝš#¢ÜÓžJ~ÆQ©ìç²ÎÎi=ý¤6-¼$œ`Kö#ü~ÿÇ[= €#âÌ™3‡ømJ¯'DÀ Q0CTÌ3DÀ Q0CTÌ3DÀ Q0CTÌ3DÀ Q0CTÌ3DÀ Q0CTÌ3DÀ Q0CTÌ3DÀ Q0CTÌ3á÷û[=€#åvS5”»êÆIEND®B`‚uv-0.9.17+ds1/docs/concepts/000077500000000000000000000000001520155276700155055ustar00rootroot00000000000000uv-0.9.17+ds1/docs/concepts/authentication/000077500000000000000000000000001520155276700205245ustar00rootroot00000000000000uv-0.9.17+ds1/docs/concepts/authentication/certificates.md000066400000000000000000000036271520155276700235230ustar00rootroot00000000000000# TLS certificates By default, uv loads certificates from the bundled `webpki-roots` crate. The `webpki-roots` are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS, where reading the system trust store incurs a significant delay). ## System certificates In some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store. To instruct uv to use the system's trust store, run uv with the `--native-tls` command-line flag, or set the `UV_NATIVE_TLS` environment variable to `true`. ## Custom certificates If a direct path to the certificate is required (e.g., in CI), set the `SSL_CERT_FILE` environment variable to the path of the certificate bundle, to instruct uv to use that file instead of the system's trust store. If client certificate authentication (mTLS) is desired, set the `SSL_CLIENT_CERT` environment variable to the path of the PEM formatted file containing the certificate followed by the private key. ## Insecure hosts If you're using a setup in which you want to trust a self-signed certificate or otherwise disable certificate verification, you can instruct uv to allow insecure connections to dedicated hosts via the `allow-insecure-host` configuration option. For example, adding the following to `pyproject.toml` will allow insecure connections to `example.com`: ```toml [tool.uv] allow-insecure-host = ["example.com"] ``` `allow-insecure-host` expects to receive a hostname (e.g., `localhost`) or hostname-port pair (e.g., `localhost:8080`), and is only applicable to HTTPS connections, as HTTP connections are inherently insecure. Use `allow-insecure-host` with caution and only in trusted environments, as it can expose you to security risks due to the lack of certificate verification. uv-0.9.17+ds1/docs/concepts/authentication/cli.md000066400000000000000000000036431520155276700216230ustar00rootroot00000000000000# The `uv auth` CLI uv provides a high-level interface for storing and retrieving credentials from services. ## Logging in to a service To add credentials for service, use the `uv auth login` command: ```console $ uv auth login example.com ``` This will prompt for the credentials. The credentials can also be provided using the `--username` and `--password` options, or the `--token` option for services which use a `__token__` or arbitrary username. !!! note We recommend providing the secret via stdin. Use `-` to indicate the value should be read from stdin, e.g., for `--password`: ```console $ echo 'my-password' | uv auth login example.com --password - ``` The same pattern can be used with `--token`. Once credentials are added, uv will use them for packaging operations that require fetching content from the given service. At this time, only HTTPS Basic authentication is supported. The credentials will not yet be used for Git requests. !!! note The credentials will not be validated, i.e., incorrect credentials will not fail. ## Logging out of a service To remove credentials, use the `uv auth logout` command: ```console $ uv auth logout example.com ``` !!! note The credentials will not be invalidated with the remote server, i.e., they will only be removed from local storage not rendered unusable. ## Showing credentials for a service To show the credential stored for a given URL, use the `uv auth token` command: ```console $ uv auth token example.com ``` If a username was used to log in, it will need to be provided as well, e.g.: ```console $ uv auth token --username foo example.com ``` ## Configuring the storage backend Credentials are persisted to the uv [credentials store](./http.md#the-uv-credentials-store). By default, credentials are written to a plaintext file. An encrypted system-native storage backend can be enabled with `UV_PREVIEW_FEATURES=native-auth`. uv-0.9.17+ds1/docs/concepts/authentication/git.md000066400000000000000000000056361520155276700216430ustar00rootroot00000000000000# Git credentials uv allows packages to be installed from private Git repositories using SSH or HTTP authentication. ## SSH authentication To authenticate using an SSH key, use the `ssh://` protocol: - `git+ssh://git@/...` (e.g., `git+ssh://git@github.com/astral-sh/uv`) - `git+ssh://git@/...` (e.g., `git+ssh://git@github.com-key-2/astral-sh/uv`) SSH authentication requires using the username `git`. See the [GitHub SSH documentation](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh) for more details on how to configure SSH. ### HTTP authentication To authenticate over HTTP Basic authentication using a password or token: - `git+https://:@/...` (e.g., `git+https://git:github_pat_asdf@github.com/astral-sh/uv`) - `git+https://@/...` (e.g., `git+https://github_pat_asdf@github.com/astral-sh/uv`) - `git+https://@/...` (e.g., `git+https://git@github.com/astral-sh/uv`) !!! note When using a GitHub personal access token, the username is arbitrary. GitHub doesn't allow you to use your account name and password in URLs like this, although other hosts may. If there are no credentials present in the URL and authentication is needed, the [Git credential helper](#git-credential-helpers) will be queried. ## Persistence of credentials When using `uv add`, uv _will not_ persist Git credentials to the `pyproject.toml` or `uv.lock`. These files are often included in source control and distributions, so it is generally unsafe to include credentials in them. If you have a Git credential helper configured, your credentials may be automatically persisted, resulting in successful subsequent fetches of the dependency. However, if you do not have a Git credential helper or the project is used on a machine without credentials seeded, uv will fail to fetch the dependency. You _may_ force uv to persist Git credentials by passing the `--raw` option to `uv add`. However, we strongly recommend setting up a [credential helper](#git-credential-helpers) instead. ## Git credential helpers Git credential helpers are used to store and retrieve Git credentials. See the [Git documentation](https://git-scm.com/doc/credential-helpers) to learn more. If you're using GitHub, the simplest way to set up a credential helper is to [install the `gh` CLI](https://github.com/cli/cli#installation) and use: ```console $ gh auth login ``` See the [`gh auth login`](https://cli.github.com/manual/gh_auth_login) documentation for more details. !!! note When using `gh auth login` interactively, the credential helper will be configured automatically. But when using `gh auth login --with-token`, as in the uv [GitHub Actions guide](../../guides/integration/github.md#private-repos), the [`gh auth setup-git`](https://cli.github.com/manual/gh_auth_setup-git) command will need to be run afterwards to configure the credential helper. uv-0.9.17+ds1/docs/concepts/authentication/http.md000066400000000000000000000070611520155276700220310ustar00rootroot00000000000000# HTTP credentials uv supports credentials over HTTP when querying package registries. Authentication can come from the following sources, in order of precedence: - The URL, e.g., `https://:@/...` - A [netrc](#netrc-files) configuration file - The uv credentials store - A [keyring provider](#keyring-providers) (off by default) Authentication may be used for hosts specified in the following contexts: - `[index]` - `index-url` - `extra-index-url` - `find-links` - `package @ https://...` ## netrc files [`.netrc`](https://everything.curl.dev/usingcurl/netrc) files are a long-standing plain text format for storing credentials on a system. Reading credentials from `.netrc` files is always enabled. The target file path will be loaded from the `NETRC` environment variable if defined, falling back to `~/.netrc` if not. ## The uv credentials store uv can read and write credentials from a store using the [`uv auth` commands](./cli.md). Credentials are stored in a plaintext file in uv's state directory, e.g., `~/.local/share/uv/credentials/credentials.toml` on Unix. This file is currently not intended to be edited manually. !!! note A secure, system native storage mechanism is in [preview](../preview.md) — it is still experimental and being actively developed. In the future, this will become the default storage mechanism. When enabled, uv will use the secret storage mechanism native to your operating system. On macOS, it uses the Keychain Services. On Windows, it uses the Windows Credential Manager. On Linux, it uses the DBus-based Secret Service API. Currently, uv only searches the native store for credentials it has added to the secret store — it will not retrieve credentials persisted by other applications. Set `UV_PREVIEW_FEATURES=native-auth` to use this storage mechanism. ## Keyring providers A keyring provider is a concept from `pip` allowing retrieval of credentials from an interface matching the popular [keyring](https://github.com/jaraco/keyring) Python package. The "subprocess" keyring provider invokes the `keyring` command to fetch credentials. uv does not support additional keyring provider types at this time. Set `--keyring-provider subprocess`, `UV_KEYRING_PROVIDER=subprocess`, or `tool.uv.keyring-provider = "subprocess"` to use the provider. ## Persistence of credentials If authentication is found for a single index URL or net location (scheme, host, and port), it will be cached for the duration of the command and used for other queries to that index or net location. Authentication is not cached across invocations of uv. When using `uv add`, uv _will not_ persist index credentials to the `pyproject.toml` or `uv.lock`. These files are often included in source control and distributions, so it is generally unsafe to include credentials in them. However, uv _will_ persist credentials for direct URLs, i.e., `package @ https://username:password:example.com/foo.whl`, as there is not currently a way to otherwise provide those credentials. If credentials were attached to an index URL during `uv add`, uv may fail to fetch dependencies from indexes which require authentication on subsequent operations. See the [index authentication documentation](../indexes.md#authentication) for details on persistent authentication for indexes. ## Learn more See the [index authentication documentation](../indexes.md#authentication) for details on authenticating index URLs. See the [`pip` compatibility guide](../../pip/compatibility.md#registry-authentication) for details on differences from `pip`. uv-0.9.17+ds1/docs/concepts/authentication/index.md000066400000000000000000000005161520155276700221570ustar00rootroot00000000000000# Authentication Authentication is required when working with private repositories or package indexes. Learn more about authentication in uv: - [Using the `uv auth` CLI](./cli.md) - [HTTP authentication](./http.md) - [Git authentication](./git.md) - [TLS certificates](./certificates.md) - [Third-party services](./third-party.md) uv-0.9.17+ds1/docs/concepts/authentication/third-party.md000066400000000000000000000015521520155276700233200ustar00rootroot00000000000000# Third-party services ## Authentication with alternative package indexes See the [alternative indexes integration guide](../../guides/integration/alternative-indexes.md) for details on authentication with popular alternative Python package indexes. ## Hugging Face support uv supports automatic authentication for the Hugging Face Hub. Specifically, if the `HF_TOKEN` environment variable is set, uv will propagate it to requests to `huggingface.co`. This is particularly useful for accessing private scripts in Hugging Face Datasets. For example, you can run the following command to execute the script `main.py` script from a private dataset: ```console $ HF_TOKEN=hf_... uv run https://huggingface.co/datasets///resolve//main.py ``` You can disable automatic Hugging Face authentication by setting the `UV_NO_HF_TOKEN=1` environment variable. uv-0.9.17+ds1/docs/concepts/build-backend.md000066400000000000000000000232611520155276700205170ustar00rootroot00000000000000# The uv build backend A build backend transforms a source tree (i.e., a directory) into a source distribution or a wheel. uv supports all build backends (as specified by [PEP 517](https://peps.python.org/pep-0517/)), but also provides a native build backend (`uv_build`) that integrates tightly with uv to improve performance and user experience. ## Choosing a build backend The uv build backend is a great choice for most Python projects. It has reasonable defaults, with the goal of requiring zero configuration for most users, but provides flexible configuration to accommodate most Python project structures. It integrates tightly with uv, to improve messaging and user experience. It validates project metadata and structures, preventing common mistakes. And, finally, it's very fast. The uv build backend currently **only supports pure Python code**. An alternative backend is required to build a [library with extension modules](../concepts/projects/init.md#projects-with-extension-modules). !!! tip While the backend supports a number of options for configuring your project structure, when build scripts or a more flexible project layout are required, consider using the [hatchling](https://hatch.pypa.io/latest/config/build/#build-system) build backend instead. ## Using the uv build backend To use uv as a build backend in an existing project, add `uv_build` to the [`[build-system]`](../concepts/projects/config.md#build-systems) section in your `pyproject.toml`: ```toml title="pyproject.toml" [build-system] requires = ["uv_build>=0.9.17,<0.10.0"] build-backend = "uv_build" ``` !!! note The uv build backend follows the same [versioning policy](../reference/policies/versioning.md) as uv. Including an upper bound on the `uv_build` version ensures that your package continues to build correctly as new versions are released. To create a new project that uses the uv build backend, use `uv init`: ```console $ uv init ``` When the project is built, e.g., with [`uv build`](../guides/package.md), the uv build backend will be used to create the source distribution and wheel. ## Bundled build backend The build backend is published as a separate package (`uv_build`) that is optimized for portability and small binary size. However, the `uv` executable also includes a copy of the build backend, which will be used during builds performed by uv, e.g., during `uv build`, if its version is compatible with the `uv_build` requirement. If it's not compatible, a compatible version of the `uv_build` package will be used. Other build frontends, such as `python -m build`, will always use the `uv_build` package, typically choosing the latest compatible version. ## Modules Python packages are expected to contain one or more Python modules, which are directories containing an `__init__.py`. By default, a single root module is expected at `src//__init__.py`. For example, the structure for a project named `foo` would be: ```text pyproject.toml src └── foo └── __init__.py ``` uv normalizes the package name to determine the default module name: the package name is lowercased and dots and dashes are replaced with underscores, e.g., `Foo-Bar` would be converted to `foo_bar`. The `src/` directory is the default directory for module discovery. These defaults can be changed with the `module-name` and `module-root` settings. For example, to use a `FOO` module in the root directory, as in the project structure: ```text pyproject.toml FOO └── __init__.py ``` The correct build configuration would be: ```toml title="pyproject.toml" [tool.uv.build-backend] module-name = "FOO" module-root = "" ``` ## Namespace packages Namespace packages are intended for use-cases where multiple packages write modules into a shared namespace. Namespace package modules are identified by a `.` in the `module-name`. For example, to package the module `bar` in the shared namespace `foo`, the project structure would be: ```text pyproject.toml src └── foo └── bar └── __init__.py ``` And the `module-name` configuration would be: ```toml title="pyproject.toml" [tool.uv.build-backend] module-name = "foo.bar" ``` !!! important The `__init__.py` file is not included in `foo`, since it's the shared namespace module. It's also possible to have a complex namespace package with more than one root module, e.g., with the project structure: ```text pyproject.toml src ├── foo │ └── __init__.py └── bar └── __init__.py ``` While we do not recommend this structure (i.e., you should use a workspace with multiple packages instead), it is supported by setting `module-name` to a list of names: ```toml title="pyproject.toml" [tool.uv.build-backend] module-name = ["foo", "bar"] ``` For packages with many modules or complex namespaces, the `namespace = true` option can be used to avoid explicitly declaring each module name, e.g.: ```toml title="pyproject.toml" [tool.uv.build-backend] namespace = true ``` !!! warning Using `namespace = true` disables safety checks. Using an explicit list of module names is strongly recommended outside of legacy projects. The `namespace` option can also be used with `module-name` to explicitly declare the root, e.g., for the project structure: ```text pyproject.toml src └── foo ├── bar │ └── __init__.py └── baz └── __init__.py ``` The recommended configuration would be: ```toml title="pyproject.toml" [tool.uv.build-backend] module-name = "foo" namespace = true ``` ## Stub packages The build backend also supports building type stub packages, which are identified by the `-stubs` suffix on the package or module name, e.g., `foo-stubs`. The module name for type stub packages must end in `-stubs`, so uv will not normalize the `-` to an underscore. Additionally, uv will search for a `__init__.pyi` file. For example, the project structure would be: ```text pyproject.toml src └── foo-stubs └── __init__.pyi ``` Type stub modules are also supported for [namespace packages](#namespace-packages). ## File inclusion and exclusion The build backend is responsible for determining which files in a source tree should be packaged into the distributions. To determine which files to include in a source distribution, uv first adds the included files and directories, then removes the excluded files and directories. This means that exclusions always take precedence over inclusions. By default, uv excludes `__pycache__`, `*.pyc`, and `*.pyo`. When building a source distribution, the following files and directories are included: - The `pyproject.toml` - The [module](#modules) under [`tool.uv.build-backend.module-root`](../reference/settings.md#build-backend_module-root). - The files referenced by `project.license-files` and `project.readme`. - All directories under [`tool.uv.build-backend.data`](../reference/settings.md#build-backend_data). - All files matching patterns from [`tool.uv.build-backend.source-include`](../reference/settings.md#build-backend_source-include). From these, items matching [`tool.uv.build-backend.source-exclude`](../reference/settings.md#build-backend_source-exclude) and the [default excludes](../reference/settings.md#build-backend_default-excludes) are removed. When building a wheel, the following files and directories are included: - The [module](#modules) under [`tool.uv.build-backend.module-root`](../reference/settings.md#build-backend_module-root) - The files referenced by `project.license-files`, which are copied into the `.dist-info` directory. - The `project.readme`, which is copied into the project metadata. - All directories under [`tool.uv.build-backend.data`](../reference/settings.md#build-backend_data), which are copied into the `.data` directory. From these, [`tool.uv.build-backend.source-exclude`](../reference/settings.md#build-backend_source-exclude), [`tool.uv.build-backend.wheel-exclude`](../reference/settings.md#build-backend_wheel-exclude) and the default excludes are removed. The source dist excludes are applied to avoid source tree to wheel builds including more files than source tree to source distribution to wheel build. There are no specific wheel includes. There must only be one top level module, and all data files must either be under the module root or in the appropriate [data directory](../reference/settings.md#build-backend_data). Most packages store small data in the module root alongside the source code. !!! tip When using the uv build backend through a frontend that is not uv, such as pip or `python -m build`, debug logging can be enabled through environment variables with `RUST_LOG=uv=debug` or `RUST_LOG=uv=verbose`. When used through uv, the uv build backend shares the verbosity level of uv. ### Include and exclude syntax Includes are anchored, which means that `pyproject.toml` includes only `/pyproject.toml` and not `/bar/pyproject.toml`. To recursively include all files under a directory, use a `/**` suffix, e.g. `src/**`. Recursive inclusions are also anchored, e.g., `assets/**/sample.csv` includes all `sample.csv` files in `/assets` or any of its children. !!! note For performance and reproducibility, avoid patterns without an anchor such as `**/sample.csv`. Excludes are not anchored, which means that `__pycache__` excludes all directories named `__pycache__` regardless of its parent directory. All children of an exclusion are excluded as well. To anchor a directory, use a `/` prefix, e.g., `/dist` will exclude only `/dist`. All fields accepting patterns use the reduced portable glob syntax from [PEP 639](https://peps.python.org/pep-0639/#add-license-FILES-key), with the addition that characters can be escaped with a backslash. uv-0.9.17+ds1/docs/concepts/cache.md000066400000000000000000000242261520155276700171000ustar00rootroot00000000000000# Caching ## Dependency caching uv uses aggressive caching to avoid re-downloading (and re-building) dependencies that have already been accessed in prior runs. The specifics of uv's caching semantics vary based on the nature of the dependency: - **For registry dependencies** (like those downloaded from PyPI), uv respects HTTP caching headers. - **For direct URL dependencies**, uv respects HTTP caching headers, and also caches based on the URL itself. - **For Git dependencies**, uv caches based on the fully-resolved Git commit hash. As such, `uv pip compile` will pin Git dependencies to a specific commit hash when writing the resolved dependency set. - **For local dependencies**, uv caches based on the last-modified time of the source archive (i.e., the local `.whl` or `.tar.gz` file). For directories, uv caches based on the last-modified time of the `pyproject.toml`, `setup.py`, or `setup.cfg` file. If you're running into caching issues, uv includes a few escape hatches: - To clear the cache entirely, run `uv cache clean`. To clear the cache for a specific package, run `uv cache clean `. For example, `uv cache clean ruff` will clear the cache for the `ruff` package. - To force uv to revalidate cached data for all dependencies, pass `--refresh` to any command (e.g., `uv sync --refresh` or `uv pip install --refresh ...`). - To force uv to revalidate cached data for a specific dependency pass `--refresh-package` to any command (e.g., `uv sync --refresh-package ruff` or `uv pip install --refresh-package ruff ...`). - To force uv to ignore existing installed versions, pass `--reinstall` to any installation command (e.g., `uv sync --reinstall` or `uv pip install --reinstall ...`). (Consider running `uv cache clean ` first, to ensure that the cache is cleared prior to reinstallation.) As a special case, uv will always rebuild and reinstall any local directory dependencies passed explicitly on the command-line (e.g., `uv pip install .`). ## Dynamic metadata By default, uv will _only_ rebuild and reinstall local directory dependencies (e.g., editables) if the `pyproject.toml`, `setup.py`, or `setup.cfg` file in the directory root has changed, or if a `src` directory is added or removed. This is a heuristic and, in some cases, may lead to fewer re-installs than desired. To incorporate additional information into the cache key for a given package, you can add cache key entries under [`tool.uv.cache-keys`](https://docs.astral.sh/uv/reference/settings/#cache-keys), which covers both file paths and Git commit hashes. Setting [`tool.uv.cache-keys`](https://docs.astral.sh/uv/reference/settings/#cache-keys) will replace defaults, so any necessary files (like `pyproject.toml`) should still be included in the user-defined cache keys. For example, if a project specifies dependencies in `pyproject.toml` but uses [`setuptools-scm`](https://pypi.org/project/setuptools-scm/) to manage its version, and should thus be rebuilt whenever the commit hash or dependencies change, you can add the following to the project's `pyproject.toml`: ```toml title="pyproject.toml" [tool.uv] cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true } }] ``` If your dynamic metadata incorporates information from the set of Git tags, you can expand the cache key to include the tags: ```toml title="pyproject.toml" [tool.uv] cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true } }] ``` Similarly, if a project reads from a `requirements.txt` to populate its dependencies, you can add the following to the project's `pyproject.toml`: ```toml title="pyproject.toml" [tool.uv] cache-keys = [{ file = "pyproject.toml" }, { file = "requirements.txt" }] ``` Globs are supported for `file` keys, following the syntax of the [`glob`](https://docs.rs/glob/0.3.1/glob/struct.Pattern.html) crate. For example, to invalidate the cache whenever a `.toml` file in the project directory or any of its subdirectories is modified, use the following: ```toml title="pyproject.toml" [tool.uv] cache-keys = [{ file = "**/*.toml" }] ``` !!! note The use of globs can be expensive, as uv may need to walk the filesystem to determine whether any files have changed. This may, in turn, requiring traversal of large or deeply nested directories. Similarly, if a project relies on an environment variable, you can add the following to the project's `pyproject.toml` to invalidate the cache whenever the environment variable changes: ```toml title="pyproject.toml" [tool.uv] cache-keys = [{ file = "pyproject.toml" }, { env = "MY_ENV_VAR" }] ``` Finally, to invalidate a project whenever a specific directory (like `src`) is created or removed, add the following to the project's `pyproject.toml`: ```toml title="pyproject.toml" [tool.uv] cache-keys = [{ file = "pyproject.toml" }, { dir = "src" }] ``` Note that the `dir` key will only track changes to the directory itself, and not arbitrary changes within the directory. As an escape hatch, if a project uses `dynamic` metadata that isn't covered by `tool.uv.cache-keys`, you can instruct uv to _always_ rebuild and reinstall it by adding the project to the `tool.uv.reinstall-package` list: ```toml title="pyproject.toml" [tool.uv] reinstall-package = ["my-package"] ``` This will force uv to rebuild and reinstall `my-package` on every run, regardless of whether the package's `pyproject.toml`, `setup.py`, or `setup.cfg` file has changed. ## Cache safety It's safe to run multiple uv commands concurrently, even against the same virtual environment. uv's cache is designed to be thread-safe and append-only, and thus robust to multiple concurrent readers and writers. uv applies a file-based lock to the target virtual environment when installing, to avoid concurrent modifications across processes. Note that it's _never_ safe to modify the cache directly (e.g., by removing a file or directory). ## Clearing the cache uv provides a few different mechanisms for removing entries from the cache: - `uv cache clean` removes _all_ cache entries from the cache directory, clearing it out entirely. - `uv cache clean ruff` removes all cache entries for the `ruff` package, useful for invalidating the cache for a single or finite set of packages. - `uv cache prune` removes all _unused_ cache entries. For example, the cache directory may contain entries created in previous uv versions that are no longer necessary and can be safely removed. `uv cache prune` is safe to run periodically, to keep the cache directory clean. uv blocks cache-modifying operations while other uv commands are running. By default, those `uv cache` commands have a 5 min timeout waiting for other uv processes to terminate to avoid deadlocks. This timeout can be changed with [`UV_LOCK_TIMEOUT`](../reference/environment.md#uv_lock_timeout). In cases where it is known that no other uv processes are reading or writing from the cache, `--force` can be used to ignore the lock. ## Caching in continuous integration It's common to cache package installation artifacts in continuous integration environments (like GitHub Actions or GitLab CI) to speed up subsequent runs. By default, uv caches both the wheels that it builds from source and the pre-built wheels that it downloads directly, to enable high-performance package installation. However, in continuous integration environments, persisting pre-built wheels may be undesirable. With uv, it turns out that it's often faster to _omit_ pre-built wheels from the cache (and instead re-download them from the registry on each run). On the other hand, caching wheels that are built from source tends to be worthwhile, since the wheel building process can be expensive, especially for extension modules. To support this caching strategy, uv provides a `uv cache prune --ci` command, which removes all pre-built wheels and unzipped source distributions from the cache, but retains any wheels that were built from source. We recommend running `uv cache prune --ci` at the end of your continuous integration job to ensure maximum cache efficiency. For an example, see the [GitHub integration guide](../guides/integration/github.md#caching). ## Cache directory uv determines the cache directory according to, in order: 1. A temporary cache directory, if `--no-cache` was requested. 2. The specific cache directory specified via `--cache-dir`, `UV_CACHE_DIR`, or [`tool.uv.cache-dir`](../reference/settings.md#cache-dir). 3. A system-appropriate cache directory, e.g., `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on Unix and `%LOCALAPPDATA%\uv\cache` on Windows !!! note uv _always_ requires a cache directory. When `--no-cache` is requested, uv will still use a temporary cache for sharing data within that single invocation. In most cases, `--refresh` should be used instead of `--no-cache` — as it will update the cache for subsequent operations but not read from the cache. It is important for performance for the cache directory to be located on the same file system as the Python environment uv is operating on. Otherwise, uv will not be able to link files from the cache into the environment and will instead need to fallback to slow copy operations. ## Cache versioning The uv cache is composed of a number of buckets (e.g., a bucket for wheels, a bucket for source distributions, a bucket for Git repositories, and so on). Each bucket is versioned, such that if a release contains a breaking change to the cache format, uv will not attempt to read from or write to an incompatible cache bucket. For example, uv 0.4.13 included a breaking change to the core metadata bucket. As such, the bucket version was increased from v12 to v13. Within a cache version, changes are guaranteed to be both forwards- and backwards-compatible. Since changes in the cache format are accompanied by changes in the cache version, multiple versions of uv can safely read and write to the same cache directory. However, if the cache version changed between a given pair of uv releases, then those releases may not be able to share the same underlying cache entries. For example, it's safe to use a single shared cache for uv 0.4.12 and uv 0.4.13, though the cache itself may contain duplicate entries in the core metadata bucket due to the change in cache version. uv-0.9.17+ds1/docs/concepts/configuration-files.md000066400000000000000000000130271520155276700220010ustar00rootroot00000000000000# Configuration files uv supports persistent configuration files at both the project- and user-level. Specifically, uv will search for a `pyproject.toml` or `uv.toml` file in the current directory, or in the nearest parent directory. !!! note For `tool` commands, which operate at the user level, local configuration files will be ignored. Instead, uv will exclusively read from user-level configuration (e.g., `~/.config/uv/uv.toml`) and system-level configuration (e.g., `/etc/uv/uv.toml`). In workspaces, uv will begin its search at the workspace root, ignoring any configuration defined in workspace members. Since the workspace is locked as a single unit, configuration is shared across all members. If a `pyproject.toml` file is found, uv will read configuration from the `[tool.uv]` table. For example, to set a persistent index URL, add the following to a `pyproject.toml`: ```toml title="pyproject.toml" [[tool.uv.index]] url = "https://test.pypi.org/simple" default = true ``` (If there is no such table, the `pyproject.toml` file will be ignored, and uv will continue searching in the directory hierarchy.) uv will also search for `uv.toml` files, which follow an identical structure, but omit the `[tool.uv]` prefix. For example: ```toml title="uv.toml" [[index]] url = "https://test.pypi.org/simple" default = true ``` !!! note `uv.toml` files take precedence over `pyproject.toml` files, so if both `uv.toml` and `pyproject.toml` files are present in a directory, configuration will be read from `uv.toml`, and `[tool.uv]` section in the accompanying `pyproject.toml` will be ignored. uv will also discover `uv.toml` configuration files in the user- and system-level [configuration directories](../reference/storage.md#configuration-directories), e.g., user-level configuration in `~/.config/uv/uv.toml`, and system-level configuration at `/etc/uv/uv.toml` on macOS and Linux. !!! important User- and system-level configuration files cannot use the `pyproject.toml` format. If project-, user-, and system-level configuration files are found, the settings will be merged, with project-level configuration taking precedence over the user-level configuration, and user-level configuration taking precedence over the system-level configuration. (If multiple system-level configuration files are found, e.g., at both `/etc/uv/uv.toml` and `$XDG_CONFIG_DIRS/uv/uv.toml`, only the first-discovered file will be used, with XDG taking priority.) For example, if a string, number, or boolean is present in both the project- and user-level configuration tables, the project-level value will be used, and the user-level value will be ignored. If an array is present in both tables, the arrays will be concatenated, with the project-level settings appearing earlier in the merged array. Settings provided via environment variables take precedence over persistent configuration, and settings provided via the command line take precedence over both. uv accepts a `--no-config` command-line argument which, when provided, disables the discovery of any persistent configuration. uv also accepts a `--config-file` command-line argument, which accepts a path to a `uv.toml` to use as the configuration file. When provided, this file will be used in place of _any_ discovered configuration files (e.g., user-level configuration will be ignored). ## Settings See the [settings reference](../reference/settings.md) for an enumeration of the available settings. ## `.env` `uv run` can load environment variables from dotenv files (e.g., `.env`, `.env.local`, `.env.development`), powered by the [`dotenvy`](https://github.com/allan2/dotenvy) crate. To load a `.env` file from a dedicated location, set the `UV_ENV_FILE` environment variable, or pass the `--env-file` flag to `uv run`. For example, to load environment variables from a `.env` file in the current working directory: ```console $ echo "MY_VAR='Hello, world!'" > .env $ uv run --env-file .env -- python -c 'import os; print(os.getenv("MY_VAR"))' Hello, world! ``` The `--env-file` flag can be provided multiple times, with subsequent files overriding values defined in previous files. To provide multiple files via the `UV_ENV_FILE` environment variable, separate the paths with a space (e.g., `UV_ENV_FILE="/path/to/file1 /path/to/file2"`). To disable dotenv loading (e.g., to override `UV_ENV_FILE` or the `--env-file` command-line argument), set the `UV_NO_ENV_FILE` environment variable to `1`, or pass the`--no-env-file` flag to `uv run`. If the same variable is defined in the environment and in a `.env` file, the value from the environment will take precedence. ## Configuring the pip interface A dedicated [`[tool.uv.pip]`](../reference/settings.md#pip) section is provided for configuring _just_ the `uv pip` command line interface. Settings in this section will not apply to `uv` commands outside the `uv pip` namespace. However, many of the settings in this section have corollaries in the top-level namespace which _do_ apply to the `uv pip` interface unless they are overridden by a value in the `uv.pip` section. The `uv.pip` settings are designed to adhere closely to pip's interface and are declared separately to retain compatibility while allowing the global settings to use alternate designs (e.g., `--no-build`). As an example, setting the `index-url` under `[tool.uv.pip]`, as in the following `pyproject.toml`, would only affect the `uv pip` subcommands (e.g., `uv pip install`, but not `uv sync`, `uv lock`, or `uv run`): ```toml title="pyproject.toml" [tool.uv.pip] index-url = "https://test.pypi.org/simple" ``` uv-0.9.17+ds1/docs/concepts/index.md000066400000000000000000000010411520155276700171320ustar00rootroot00000000000000# Concepts overview Read the concept documents to learn more about uv's features: - [Projects](./projects/index.md) - [Tools](./tools.md) - [Python versions](./python-versions.md) - [Configuration files](./configuration-files.md) - [Package indexes](./indexes.md) - [Resolution](./resolution.md) - [The uv build backend](./build-backend.md) - [Authentication](./authentication/index.md) - [Caching](./cache.md) - [The pip interface](../pip/index.md) Looking for a quick introduction to features? See the [guides](../guides/index.md) instead. uv-0.9.17+ds1/docs/concepts/indexes.md000066400000000000000000000305041520155276700174700ustar00rootroot00000000000000# Package indexes By default, uv uses the [Python Package Index (PyPI)](https://pypi.org) for dependency resolution and package installation. However, uv can be configured to use other package indexes, including private indexes, via the `[[tool.uv.index]]` configuration option (and `--index`, the analogous command-line option). ## Defining an index To include an additional index when resolving dependencies, add a `[[tool.uv.index]]` entry to your `pyproject.toml`: ```toml [[tool.uv.index]] # Optional name for the index. name = "pytorch" # Required URL for the index. url = "https://download.pytorch.org/whl/cpu" ``` Indexes are prioritized in the order in which they’re defined, such that the first index listed in the configuration file is the first index consulted when resolving dependencies, with indexes provided via the command line taking precedence over those in the configuration file. By default, uv includes the Python Package Index (PyPI) as the "default" index, i.e., the index used when a package is not found on any other index. To exclude PyPI from the list of indexes, set `default = true` on another index entry (or use the `--default-index` command-line option): ```toml [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cpu" default = true ``` The default index is always treated as lowest priority, regardless of its position in the list of indexes. Index names may only contain alphanumeric characters, dashes, underscores, and periods, and must be valid ASCII. When providing an index on the command line (with `--index` or `--default-index`) or through an environment variable (`UV_INDEX` or `UV_DEFAULT_INDEX`), names are optional but can be included using the `=` syntax, as in: ```shell # On the command line. $ uv lock --index pytorch=https://download.pytorch.org/whl/cpu # Via an environment variable. $ UV_INDEX=pytorch=https://download.pytorch.org/whl/cpu uv lock ``` ## Pinning a package to an index A package can be pinned to a specific index by specifying the index in its `tool.uv.sources` entry. For example, to ensure that `torch` is _always_ installed from the `pytorch` index, add the following to your `pyproject.toml`: ```toml [tool.uv.sources] torch = { index = "pytorch" } [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cpu" ``` Similarly, to pull from a different index based on the platform, you can provide a list of sources disambiguated by environment markers: ```toml title="pyproject.toml" [project] dependencies = ["torch"] [tool.uv.sources] torch = [ { index = "pytorch-cu118", marker = "sys_platform == 'darwin'"}, { index = "pytorch-cu124", marker = "sys_platform != 'darwin'"}, ] [[tool.uv.index]] name = "pytorch-cu118" url = "https://download.pytorch.org/whl/cu118" [[tool.uv.index]] name = "pytorch-cu124" url = "https://download.pytorch.org/whl/cu124" ``` An index can be marked as `explicit = true` to prevent packages from being installed from that index unless explicitly pinned to it. For example, to ensure that `torch` is installed from the `pytorch` index, but all other packages are installed from PyPI, add the following to your `pyproject.toml`: ```toml [tool.uv.sources] torch = { index = "pytorch" } [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cpu" explicit = true ``` Named indexes referenced via `tool.uv.sources` must be defined within the project's `pyproject.toml` file; indexes provided via the command-line, environment variables, or user-level configuration will not be recognized. If an index is marked as both `default = true` and `explicit = true`, it will be treated as an explicit index (i.e., only usable via `tool.uv.sources`) while also removing PyPI as the default index. ## Searching across multiple indexes By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-index`). For example, if an internal index is specified via `[[tool.uv.index]]`, uv's behavior is such that if a package exists on that internal index, it will _always_ be installed from that internal index, and never from PyPI. The intent is to prevent "dependency confusion" attacks, in which an attacker publishes a malicious package on PyPI with the same name as an internal package, thus causing the malicious package to be installed instead of the internal package. See, for example, [the `torchtriton` attack](https://pytorch.org/blog/compromised-nightly-dependency/) from December 2022. To opt in to alternate index behaviors, use the`--index-strategy` command-line option, or the `UV_INDEX_STRATEGY` environment variable, which supports the following values: - `first-index` (default): Search for each package across all indexes, limiting the candidate versions to those present in the first index that contains the package. - `unsafe-first-match`: Search for each package across all indexes, but prefer the first index with a compatible version, even if newer versions are available on other indexes. - `unsafe-best-match`: Search for each package across all indexes, and select the best version from the combined set of candidate versions. While `unsafe-best-match` is the closest to pip's behavior, it exposes users to the risk of "dependency confusion" attacks. ## Authentication Most private package indexes require authentication to access packages, typically via a username and password (or access token). !!! tip See the [alternative index guide](../guides/integration/alternative-indexes.md) for details on authenticating with specific private index providers, e.g., from AWS, Azure, or GCP. ### Providing credentials directly Credentials can be provided directly via environment variables or by embedding them in the URL. For example, given an index named `internal-proxy` that requires a username (`public`) and password (`koala`), define the index (without credentials) in your `pyproject.toml`: ```toml [[tool.uv.index]] name = "internal-proxy" url = "https://example.com/simple" ``` From there, you can set the `UV_INDEX_INTERNAL_PROXY_USERNAME` and `UV_INDEX_INTERNAL_PROXY_PASSWORD` environment variables, where `INTERNAL_PROXY` is the uppercase version of the index name, with non-alphanumeric characters replaced by underscores: ```sh export UV_INDEX_INTERNAL_PROXY_USERNAME=public export UV_INDEX_INTERNAL_PROXY_PASSWORD=koala ``` By providing credentials via environment variables, you can avoid storing sensitive information in the plaintext `pyproject.toml` file. Alternatively, credentials can be embedded directly in the index definition: ```toml [[tool.uv.index]] name = "internal" url = "https://public:koala@pypi-proxy.corp.dev/simple" ``` For security purposes, credentials are _never_ stored in the `uv.lock` file; as such, uv _must_ have access to the authenticated URL at installation time. ### Using credential providers In addition to providing credentials directly, uv supports discovery of credentials from netrc and keyring. See the [HTTP authentication](./authentication/http.md) documentation for details on setting up specific credential providers. By default, uv will attempt an unauthenticated request before querying providers. If the request fails, uv will search for credentials. If credentials are found, an authenticated request will be attempted. !!! note If a username is set, uv will search for credentials before making an unauthenticated request. Some indexes (e.g., GitLab) will forward unauthenticated requests to a public index, like PyPI — which means that uv will not search for credentials. This behavior can be changed per-index, using the `authenticate` setting. For example, to always search for credentials: ```toml hl_lines="4" [[tool.uv.index]] name = "example" url = "https://example.com/simple" authenticate = "always" ``` When `authenticate` is set to `always`, uv will eagerly search for credentials and error if credentials cannot be found. ### Ignoring error codes when searching across indexes When using the [first-index strategy](#searching-across-multiple-indexes), uv will stop searching across indexes if an HTTP 401 Unauthorized or HTTP 403 Forbidden status code is encountered. The one exception is that uv will ignore 403s when searching the `pytorch` index (since this index returns a 403 when a package is not present). To configure which error codes are ignored for an index, use the `ignored-error-codes` setting. For example, to ignore 403s (but not 401s) for a private index: ```toml [[tool.uv.index]] name = "private-index" url = "https://private-index.com/simple" authenticate = "always" ignore-error-codes = [403] ``` uv will always continue searching across indexes when it encounters a `404 Not Found`. This cannot be overridden. ### Disabling authentication To prevent leaking credentials, authentication can be disabled for an index: ```toml hl_lines="4" [[tool.uv.index]] name = "example" url = "https://example.com/simple" authenticate = "never" ``` When `authenticate` is set to `never`, uv will never search for credentials for the given index and will error if credentials are provided directly. ### Customizing cache control headers By default, uv will respect the cache control headers provided by the index. For example, PyPI serves package metadata with a `max-age=600` header, thereby allowing uv to cache package metadata for 10 minutes; and wheels and source distributions with a `max-age=365000000, immutable` header, thereby allowing uv to cache artifacts indefinitely. To override the cache control headers for an index, use the `cache-control` setting: ```toml [[tool.uv.index]] name = "example" url = "https://example.com/simple" cache-control = { api = "max-age=600", files = "max-age=365000000, immutable" } ``` The `cache-control` setting accepts an object with two optional keys: - `api`: Controls caching for Simple API requests (package metadata). - `files`: Controls caching for artifact downloads (wheels and source distributions). The values for these keys are strings that follow the [HTTP Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) syntax. For example, to force uv to always revalidate package metadata, set `api = "no-cache"`: ```toml [[tool.uv.index]] name = "example" url = "https://example.com/simple" cache-control = { api = "no-cache" } ``` This setting is most commonly used to override the default cache control headers for private indexes that otherwise disable caching, often unintentionally. We typically recommend following PyPI's approach to caching headers, i.e., setting `api = "max-age=600"` and `files = "max-age=365000000, immutable"`. ## "Flat" indexes By default, `[[tool.uv.index]]` entries are assumed to be PyPI-style registries that implement the [PEP 503](https://peps.python.org/pep-0503/) Simple Repository API. However, uv also supports "flat" indexes, which are local directories or HTML pages that contain flat lists of wheels and source distributions. In pip, such indexes are specified using the `--find-links` option. To define a flat index in your `pyproject.toml`, use the `format = "flat"` option: ```toml [[tool.uv.index]] name = "example" url = "/path/to/directory" format = "flat" ``` Flat indexes support the same feature set as Simple Repository API indexes (e.g., `explicit = true`); you can also pin a package to a flat index using `tool.uv.sources`. ## `--index-url` and `--extra-index-url` In addition to the `[[tool.uv.index]]` configuration option, uv supports pip-style `--index-url` and `--extra-index-url` command-line options for compatibility, where `--index-url` defines the default index and `--extra-index-url` defines additional indexes. These options can be used in conjunction with the `[[tool.uv.index]]` configuration option, and follow the same prioritization rules: - The default index is always treated as lowest priority, whether defined via the legacy `--index-url` argument, the recommended `--default-index` argument, or a `[[tool.uv.index]]` entry with `default = true`. - Indexes are consulted in the order in which they’re defined, either via the legacy `--extra-index-url` argument, the recommended `--index` argument, or `[[tool.uv.index]]` entries. In effect, `--index-url` and `--extra-index-url` can be thought of as unnamed `[[tool.uv.index]]` entries, with `default = true` enabled for the former. In that context, `--index-url` maps to `--default-index`, and `--extra-index-url` maps to `--index`. uv-0.9.17+ds1/docs/concepts/preview.md000066400000000000000000000056351520155276700175210ustar00rootroot00000000000000# Preview features uv includes opt-in preview features to provide an opportunity for community feedback and increase confidence that changes are a net-benefit before enabling them for everyone. ## Enabling preview features To enable all preview features, use the `--preview` flag: ```console $ uv run --preview ... ``` Or, set the `UV_PREVIEW` environment variable: ```console $ UV_PREVIEW=1 uv run ... ``` To enable specific preview features, use the `--preview-features` flag: ```console $ uv run --preview-features foo ... ``` The `--preview-features` flag can be repeated to enable multiple features: ```console $ uv run --preview-features foo --preview-features bar ... ``` Or, features can be provided in a comma separated list: ```console $ uv run --preview-features foo,bar ... ``` The `UV_PREVIEW_FEATURES` environment variable can be used similarly, e.g.: ```console $ UV_PREVIEW_FEATURES=foo,bar uv run ... ``` For backwards compatibility, enabling preview features that do not exist will warn, but not error. ## Using preview features Often, preview features can be used without changing any preview settings if the behavior change is gated by some sort of user interaction, For example, while `pylock.toml` support is in preview, you can use `uv pip install` with a `pylock.toml` file without additional configuration because specifying the `pylock.toml` file indicates you want to use the feature. However, a warning will be displayed that the feature is in preview. The preview feature can be enabled to silence the warning. Other preview features change behavior without changes to your use of uv. For example, when the `python-upgrade` feature is enabled, the default behavior of `uv python install` changes to allow uv to upgrade Python versions transparently. This feature requires enabling the preview flag for proper usage. ## Available preview features The following preview features are available: - `add-bounds`: Allows configuring the [default bounds for `uv add`](../reference/settings.md#add-bounds) invocations. - `json-output`: Allows `--output-format json` for various uv commands. - `package-conflicts`: Allows defining workspace conflicts at the package level. - `pylock`: Allows installing from `pylock.toml` files. - `python-install-default`: Allows [installing `python` and `python3` executables](./python-versions.md#installing-python-executables). - `python-upgrade`: Allows [transparent Python version upgrades](./python-versions.md#upgrading-python-versions). - `format`: Allows using `uv format`. - `native-auth`: Enables storage of credentials in a [system-native location](../concepts/authentication/http.md#the-uv-credentials-store). - `workspace-metadata`: Allows using `uv workspace metadata`. - `workspace-dir`: Allows using `uv workspace dir`. - `workspace-list`: Allows using `uv workspace list`. ## Disabling preview features The `--no-preview` option can be used to disable preview features. uv-0.9.17+ds1/docs/concepts/projects/000077500000000000000000000000001520155276700173365ustar00rootroot00000000000000uv-0.9.17+ds1/docs/concepts/projects/build.md000066400000000000000000000057431520155276700207700ustar00rootroot00000000000000# Building distributions To distribute your project to others (e.g., to upload it to an index like PyPI), you'll need to build it into a distributable format. Python projects are typically distributed as both source distributions (sdists) and binary distributions (wheels). The former is typically a `.tar.gz` or `.zip` file containing the project's source code along with some additional metadata, while the latter is a `.whl` file containing pre-built artifacts that can be installed directly. !!! important When using `uv build`, uv acts as a [build frontend](https://peps.python.org/pep-0517/#terminology-and-goals) and only determines the Python version to use and invokes the build backend. The details of the builds, such as the included files and the distribution filenames, are determined by the build backend, as defined in [`[build-system]`](./config.md#build-systems). Information about build configuration can be found in the respective tool's documentation. ## Using `uv build` `uv build` can be used to build both source distributions and binary distributions for your project. By default, `uv build` will build the project in the current directory, and place the built artifacts in a `dist/` subdirectory: ```console $ uv build $ ls dist/ example-0.1.0-py3-none-any.whl example-0.1.0.tar.gz ``` You can build the project in a different directory by providing a path to `uv build`, e.g., `uv build path/to/project`. `uv build` will first build a source distribution, and then build a binary distribution (wheel) from that source distribution. You can limit `uv build` to building a source distribution with `uv build --sdist`, a binary distribution with `uv build --wheel`, or build both distributions from source with `uv build --sdist --wheel`. ## Build constraints `uv build` accepts `--build-constraint`, which can be used to constrain the versions of any build requirements during the build process. When coupled with `--require-hashes`, uv will enforce that the requirement used to build the project match specific, known hashes, for reproducibility. For example, given the following `constraints.txt`: ```text setuptools==68.2.2 --hash=sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a ``` Running the following would build the project with the specified version of `setuptools`, and verify that the downloaded `setuptools` distribution matches the specified hash: ```console $ uv build --build-constraint constraints.txt --require-hashes ``` ## Preventing publish to PyPI If you have internal packages that you do not want to be published, you can mark them as private: ```toml [project] classifiers = ["Private :: Do Not Upload"] ``` This setting makes PyPI reject your uploaded package from publishing. It does not affect security or privacy settings on alternative registries. We also recommend only generating [per-project PyPI API tokens](https://pypi.org/help/#apitoken): Without a PyPI token matching the project, it can't be accidentally published. uv-0.9.17+ds1/docs/concepts/projects/config.md000066400000000000000000000556371520155276700211450ustar00rootroot00000000000000# Configuring projects ## Python version requirement Projects may declare the Python versions supported by the project in the `project.requires-python` field of the `pyproject.toml`. It is recommended to set a `requires-python` value: ```toml title="pyproject.toml" hl_lines="4" [project] name = "example" version = "0.1.0" requires-python = ">=3.12" ``` The Python version requirement determines the Python syntax that is allowed in the project and affects selection of dependency versions (they must support the same Python version range). ## Entry points [Entry points](https://packaging.python.org/en/latest/specifications/entry-points/#entry-points) are the official term for an installed package to advertise interfaces. These include: - [Command line interfaces](#command-line-interfaces) - [Graphical user interfaces](#graphical-user-interfaces) - [Plugin entry points](#plugin-entry-points) !!! important Using the entry point tables requires a [build system](#build-systems) to be defined. ### Command-line interfaces Projects may define command line interfaces (CLIs) for the project in the `[project.scripts]` table of the `pyproject.toml`. For example, to declare a command called `hello` that invokes the `hello` function in the `example` module: ```toml title="pyproject.toml" [project.scripts] hello = "example:hello" ``` Then, the command can be run from a console: ```console $ uv run hello ``` ### Graphical user interfaces Projects may define graphical user interfaces (GUIs) for the project in the `[project.gui-scripts]` table of the `pyproject.toml`. !!! important These are only different from [command-line interfaces](#command-line-interfaces) on Windows, where they are wrapped by a GUI executable so they can be started without a console. On other platforms, they behave the same. For example, to declare a command called `hello` that invokes the `app` function in the `example` module: ```toml title="pyproject.toml" [project.gui-scripts] hello = "example:app" ``` ### Plugin entry points Projects may define entry points for plugin discovery in the [`[project.entry-points]`](https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/#using-package-metadata) table of the `pyproject.toml`. For example, to register the `example-plugin-a` package as a plugin for `example`: ```toml title="pyproject.toml" [project.entry-points.'example.plugins'] a = "example_plugin_a" ``` Then, in `example`, plugins would be loaded with: ```python title="example/__init__.py" from importlib.metadata import entry_points for plugin in entry_points(group='example.plugins'): plugin.load() ``` !!! note The `group` key can be an arbitrary value, it does not need to include the package name or "plugins". However, it is recommended to namespace the key by the package name to avoid collisions with other packages. ## Build systems A build system determines how the project should be packaged and installed. Projects may declare and configure a build system in the `[build-system]` table of the `pyproject.toml`. uv uses the presence of a build system to determine if a project contains a package that should be installed in the project virtual environment. If a build system is not defined, uv will not attempt to build or install the project itself, just its dependencies. If a build system is defined, uv will build and install the project into the project environment. The `--build-backend` option can be provided to `uv init` to create a packaged project with an appropriate layout. The `--package` option can be provided to `uv init` to create a packaged project with the default build system. !!! note While uv will not build and install the current project without a build system definition, the presence of a `[build-system]` table is not required in other packages. For legacy reasons, if a build system is not defined, then `setuptools.build_meta:__legacy__` is used to build the package. Packages you depend on may not explicitly declare their build system but are still installable. Similarly, if you [add a dependency on a local project](./dependencies.md#path) or install it with `uv pip`, uv will attempt to build and install it regardless of the presence of a `[build-system]` table. Build systems are used to power the following features: - Including or excluding files from distributions - Editable installation behavior - Dynamic project metadata - Compilation of native code - Vendoring shared libraries To configure these features, refer to the documentation of your chosen build system. ## Project packaging As discussed in [build systems](#build-systems), a Python project must be built to be installed. This process is generally referred to as "packaging". You probably need a package if you want to: - Add commands to the project - Distribute the project to others - Use a `src` and `test` layout - Write a library You probably _do not_ need a package if you are: - Writing scripts - Building a simple application - Using a flat layout While uv usually uses the declaration of a [build system](#build-systems) to determine if a project should be packaged, uv also allows overriding this behavior with the [`tool.uv.package`](../../reference/settings.md#package) setting. Setting `tool.uv.package = true` will force a project to be built and installed into the project environment. If no build system is defined, uv will use the setuptools legacy backend. Setting `tool.uv.package = false` will force a project package _not_ to be built and installed into the project environment. uv will ignore a declared build system when interacting with the project; however, uv will still respect explicit attempts to build the project such as invoking `uv build`. ## Project environment path The `UV_PROJECT_ENVIRONMENT` environment variable can be used to configure the project virtual environment path (`.venv` by default). If a relative path is provided, it will be resolved relative to the workspace root. If an absolute path is provided, it will be used as-is, i.e., a child directory will not be created for the environment. If an environment is not present at the provided path, uv will create it. This option can be used to write to the system Python environment, though it is not recommended. `uv sync` will remove extraneous packages from the environment by default and, as such, may leave the system in a broken state. To target the system environment, set `UV_PROJECT_ENVIRONMENT` to the prefix of the Python installation. For example, on Debian-based systems, this is usually `/usr/local`: ```console $ python -c "import sysconfig; print(sysconfig.get_config_var('prefix'))" /usr/local ``` To target this environment, you'd export `UV_PROJECT_ENVIRONMENT=/usr/local`. !!! important If an absolute path is provided and the setting is used across multiple projects, the environment will be overwritten by invocations in each project. This setting is only recommended for use for a single project in CI or Docker images. !!! note By default, uv does not read the `VIRTUAL_ENV` environment variable during project operations. A warning will be displayed if `VIRTUAL_ENV` is set to a different path than the project's environment. The `--active` flag can be used to opt-in to respecting `VIRTUAL_ENV`. The `--no-active` flag can be used to silence the warning. ## Build isolation By default, uv builds all packages in isolated virtual environments alongside their declared build dependencies, as per [PEP 517](https://peps.python.org/pep-0517/). Some packages are incompatible with this approach to build isolation, be it intentionally or unintentionally. For example, packages like [`flash-attn`](https://pypi.org/project/flash-attn/) and [`deepspeed`](https://pypi.org/project/deepspeed/) need to build against the same version of PyTorch that is installed in the project environment; by building them in an isolated environment, they may inadvertently build against a different version of PyTorch, leading to runtime errors. In other cases, packages may accidentally omit necessary dependencies in their declared build dependency list. For example, [`cchardet`](https://pypi.org/project/cchardet/) requires `cython` to be installed in the project environment prior to installing `cchardet`, but does not declare it as a build dependency. To address these issues, uv supports two separate approaches to modifying the build isolation behavior: 1. **Augmenting the list of build dependencies**: This allows you to install a package in an isolated environment, but with additional build dependencies that are not declared by the package itself via the [`extra-build-dependencies`](../../reference/settings.md#extra-build-dependencies) setting. For packages like `flash-attn`, you can even enforce that those build dependencies (like `torch`) match the version of the package that is or will be installed in the project environment. 1. **Disabling build isolation for specific packages**: This allows you to install a package without building it in an isolated environment. When possible, we recommend augmenting the build dependencies rather than disabling build isolation entirely, as the latter approach requires that the build dependencies are installed in the project environment _prior_ to installing the package itself, which can lead to more complex installation steps, the inclusion of extraneous packages in the project environment, and difficulty in reproducing the project environment in other contexts. ### Augmenting build dependencies To augment the list of build dependencies for a specific package, add it to the [`extra-build-dependencies`](../../reference/settings.md#extra-build-dependencies) list in your `pyproject.toml`. For example, to build `cchardet` with `cython` as an additional build dependency, include the following in your `pyproject.toml`: ```toml title="pyproject.toml" [project] name = "project" version = "0.1.0" description = "..." readme = "README.md" requires-python = ">=3.12" dependencies = ["cchardet"] [tool.uv.extra-build-dependencies] cchardet = ["cython"] ``` To ensure that a build dependency matches the version of the package that is or will be installed in the project environment, set `match-runtime = true` in the `extra-build-dependencies` table. For example, to build `deepspeed` with `torch` as an additional build dependency, include the following in your `pyproject.toml`: ```toml title="pyproject.toml" [project] name = "project" version = "0.1.0" description = "..." readme = "README.md" requires-python = ">=3.12" dependencies = ["deepspeed", "torch"] [tool.uv.extra-build-dependencies] deepspeed = [{ requirement = "torch", match-runtime = true }] ``` This will ensure that `deepspeed` is built with the same version of `torch` that is installed in the project environment. Similarly, to build `flash-attn` with `torch` as an additional build dependency, include the following in your `pyproject.toml`: ```toml title="pyproject.toml" [project] name = "project" version = "0.1.0" description = "..." readme = "README.md" requires-python = ">=3.12" dependencies = ["flash-attn", "torch"] [tool.uv.extra-build-dependencies] flash-attn = [{ requirement = "torch", match-runtime = true }] [tool.uv.extra-build-variables] flash-attn = { FLASH_ATTENTION_SKIP_CUDA_BUILD = "TRUE" } ``` !!! note The `FLASH_ATTENTION_SKIP_CUDA_BUILD` environment variable ensures that `flash-attn` is installed from a compatible, pre-built wheel, rather than attempting to build it from source, which requires access to the CUDA development toolkit. If the CUDA toolkit is not available, the environment variable can be omitted, and `flash-attn` will be installed from a pre-built wheel if one is available for the current platform, Python version, and PyTorch version. Similarly, [`deep_gemm`](https://github.com/deepseek-ai/DeepGEMM) follows the same pattern: ```toml title="pyproject.toml" [project] name = "project" version = "0.1.0" description = "..." readme = "README.md" requires-python = ">=3.12" dependencies = ["deep_gemm", "torch"] [tool.uv.sources] deep_gemm = { git = "https://github.com/deepseek-ai/DeepGEMM" } [tool.uv.extra-build-dependencies] deep_gemm = [{ requirement = "torch", match-runtime = true }] ``` The use of `extra-build-dependencies` and `extra-build-variables` are tracked in the uv cache, such that changes to these settings will trigger a reinstall and rebuild of the affected packages. For example, in the case of `flash-attn`, upgrading the version of `torch` used in your project would subsequently trigger a rebuild of `flash-attn` with the new version of `torch`. #### Dynamic metadata The use of `match-runtime = true` is only available for packages like `flash-attn` that declare static metadata. If static metadata is unavailable, uv is required to build the package during the dependency resolution phase; as such, uv cannot determine the version of the build dependency that would ultimately be installed in the project environment. In other words, if `flash-attn` did not declare static metadata, uv would not be able to determine the version of `torch` that would be installed in the project environment, since it would need to build `flash-attn` prior to resolving the `torch` version. As a concrete example, [`axolotl`](https://pypi.org/project/axolotl/) is a popular package that requires augmented build dependencies, but does not declare static metadata, as the package's dependencies vary based on the version of `torch` that is installed in the project environment. In this case, users should instead specify the exact version of `torch` that they intend to use in their project, and then augment the build dependencies with that version. For example, to build `axolotl` against `torch==2.6.0`, include the following in your `pyproject.toml`: ```toml title="pyproject.toml" [project] name = "project" version = "0.1.0" description = "..." readme = "README.md" requires-python = ">=3.12" dependencies = ["axolotl[deepspeed, flash-attn]", "torch==2.6.0"] [tool.uv.extra-build-dependencies] axolotl = ["torch==2.6.0"] deepspeed = ["torch==2.6.0"] flash-attn = ["torch==2.6.0"] ``` Similarly, older versions of `flash-attn` did not declare static metadata, and thus would not have supported `match-runtime = true` out of the box. Unlike `axolotl`, though, `flash-attn` did not vary its dependencies based on dynamic properties of the build environment. As such, users could instead provide the `flash-attn` metadata upfront via the [`dependency-metadata`](../../reference/settings.md#dependency-metadata) setting, thereby forgoing the need to build the package during the dependency resolution phase. For example, to provide the `flash-attn` metadata upfront: ```toml title="pyproject.toml" [[tool.uv.dependency-metadata]] name = "flash-attn" version = "2.6.3" requires-dist = ["torch", "einops"] ``` !!! tip To determine the package metadata for a package like `flash-attn`, navigate to the appropriate Git repository, or look it up on [PyPI](https://pypi.org/project/flash-attn) and download the package's source distribution. The package requirements can typically be found in the `setup.py` or `setup.cfg` file. (If the package includes a built distribution, you can unzip it to find the `METADATA` file; however, the presence of a built distribution would negate the need to provide the metadata upfront, since it would already be available to uv.) The `version` field in `tool.uv.dependency-metadata` is optional for registry-based dependencies (when omitted, uv will assume the metadata applies to all versions of the package), but _required_ for direct URL dependencies (like Git dependencies). ### Disabling build isolation Installing packages without build isolation requires that the package's build dependencies are installed in the project environment _prior_ to building the package itself. For example, historically, to install `cchardet` without build isolation, you would first need to install the `cython` and `setuptools` packages in the project environment, followed by a separate invocation to install `cchardet` without build isolation: ```console $ uv venv $ uv pip install cython setuptools $ uv pip install cchardet --no-build-isolation ``` uv simplifies this process by allowing you to specify packages that should not be built in isolation via the `no-build-isolation-package` setting in your `pyproject.toml` and the `--no-build-isolation-package` flag in the command line. Further, when a package is marked for disabling build isolation, uv will perform a two-phase install, first installing any packages that support build isolation, followed by those that do not. As a result, if a project's build dependencies are included as project dependencies, uv will automatically install them before installing the package that requires build isolation to be disabled. For example, to install `cchardet` without build isolation, include the following in your `pyproject.toml`: ```toml title="pyproject.toml" [project] name = "project" version = "0.1.0" description = "..." readme = "README.md" requires-python = ">=3.12" dependencies = ["cchardet", "cython", "setuptools"] [tool.uv] no-build-isolation-package = ["cchardet"] ``` When running `uv sync`, uv will first install `cython` and `setuptools` in the project environment, followed by `cchardet` (without build isolation): ```console $ uv sync --extra build + cchardet==2.1.7 + cython==3.1.3 + setuptools==80.9.0 ``` Similarly, to install `flash-attn` without build isolation, include the following in your `pyproject.toml`: ```toml title="pyproject.toml" [project] name = "project" version = "0.1.0" description = "..." readme = "README.md" requires-python = ">=3.12" dependencies = ["flash-attn", "torch"] [tool.uv] no-build-isolation-package = ["flash-attn"] ``` When running `uv sync`, uv will first install `torch` in the project environment, followed by `flash-attn` (without build isolation). As `torch` is both a project dependency and a build dependency, the version of `torch` is guaranteed to be consistent between the build and runtime environments. A downside of the above approach is that it requires the build dependencies to be installed in the project environment, which is appropriate for `flash-attn` (which requires `torch` both at build-time and runtime), but not for `cchardet` (which only requires `cython` at build-time). To avoid including build dependencies in the project environment, uv supports a two-step installation process that allows you to separate the build dependencies from the packages that require them. For example, the build dependencies for `cchardet` can be isolated to an optional `build` group, as in: ```toml title="pyproject.toml" [project] name = "project" version = "0.1.0" description = "..." readme = "README.md" requires-python = ">=3.12" dependencies = ["cchardet"] [project.optional-dependencies] build = ["setuptools", "cython"] [tool.uv] no-build-isolation-package = ["cchardet"] ``` Given the above, a user would first sync with the `build` optional group, and then without it to remove the build dependencies: ```console $ uv sync --extra build + cchardet==2.1.7 + cython==3.1.3 + setuptools==80.9.0 $ uv sync - cython==3.1.3 - setuptools==80.9.0 ``` Some packages, like `cchardet`, only require build dependencies for the _installation_ phase of `uv sync`. Others require their build dependencies to be present even just to resolve the project's dependencies during the _resolution_ phase. In such cases, the build dependencies can be installed prior to running any `uv lock` or `uv sync` commands, using the lower lower-level `uv pip` API. For example, given: ```toml title="pyproject.toml" [project] name = "project" version = "0.1.0" description = "..." readme = "README.md" requires-python = ">=3.12" dependencies = ["flash-attn"] [tool.uv] no-build-isolation-package = ["flash-attn"] ``` You could run the following sequence of commands to sync `flash-attn`: ```console $ uv venv $ uv pip install torch setuptools $ uv sync ``` Alternatively, users can instead provide the `flash-attn` metadata upfront via the [`dependency-metadata`](../../reference/settings.md#dependency-metadata) setting, thereby forgoing the need to build the package during the dependency resolution phase. For example, to provide the `flash-attn` metadata upfront: ```toml title="pyproject.toml" [[tool.uv.dependency-metadata]] name = "flash-attn" version = "2.6.3" requires-dist = ["torch", "einops"] ``` ## Editable mode By default, the project will be installed in editable mode, such that changes to the source code are immediately reflected in the environment. `uv sync` and `uv run` both accept a `--no-editable` flag, which instructs uv to install the project in non-editable mode. `--no-editable` is intended for deployment use-cases, such as building a Docker container, in which the project should be included in the deployed environment without a dependency on the originating source code. ## Conflicting dependencies uv resolves all project dependencies together, including optional dependencies ("extras") and dependency groups. If dependencies declared in one section are not compatible with those in another section, uv will fail to resolve the requirements of the project with an error. uv supports explicit declaration of conflicting dependency groups. For example, to declare that the `optional-dependency` groups `extra1` and `extra2` are incompatible: ```toml title="pyproject.toml" [tool.uv] conflicts = [ [ { extra = "extra1" }, { extra = "extra2" }, ], ] ``` Or, to declare the development dependency groups `group1` and `group2` incompatible: ```toml title="pyproject.toml" [tool.uv] conflicts = [ [ { group = "group1" }, { group = "group2" }, ], ] ``` See the [resolution documentation](../resolution.md#conflicting-dependencies) for more. ## Limited resolution environments If your project supports a more limited set of platforms or Python versions, you can constrain the set of solved platforms via the `environments` setting, which accepts a list of PEP 508 environment markers. For example, to constrain the lockfile to macOS and Linux, and exclude Windows: ```toml title="pyproject.toml" [tool.uv] environments = [ "sys_platform == 'darwin'", "sys_platform == 'linux'", ] ``` See the [resolution documentation](../resolution.md#limited-resolution-environments) for more. ## Required environments If your project _must_ support a specific platform or Python version, you can mark that platform as required via the `required-environments` setting. For example, to require that the project supports Intel macOS: ```toml title="pyproject.toml" [tool.uv] required-environments = [ "sys_platform == 'darwin' and platform_machine == 'x86_64'", ] ``` The `required-environments` setting is only relevant for packages that do not publish a source distribution (like PyTorch), as such packages can _only_ be installed on environments covered by the set of pre-built binary distributions (wheels) published by that package. See the [resolution documentation](../resolution.md#required-environments) for more. uv-0.9.17+ds1/docs/concepts/projects/dependencies.md000066400000000000000000000702151520155276700223130ustar00rootroot00000000000000# Managing dependencies ## Dependency fields Dependencies of the project are defined in several fields: - [`project.dependencies`](#project-dependencies): Published dependencies. - [`project.optional-dependencies`](#optional-dependencies): Published optional dependencies, or "extras". - [`dependency-groups`](#dependency-groups): Local dependencies for development. - [`tool.uv.sources`](#dependency-sources): Alternative sources for dependencies during development. !!! note The `project.dependencies` and `project.optional-dependencies` fields can be used even if project isn't going to be published. `dependency-groups` are a recently standardized feature and may not be supported by all tools yet. uv supports modifying the project's dependencies with `uv add` and `uv remove`, but dependency metadata can also be updated by editing the `pyproject.toml` directly. ## Adding dependencies To add a dependency: ```console $ uv add httpx ``` An entry will be added in the `project.dependencies` field: ```toml title="pyproject.toml" hl_lines="4" [project] name = "example" version = "0.1.0" dependencies = ["httpx>=0.27.2"] ``` The [`--dev`](#development-dependencies), [`--group`](#dependency-groups), or [`--optional`](#optional-dependencies) flags can be used to add dependencies to an alternative field. The dependency will include a constraint, e.g., `>=0.27.2`, for the most recent, compatible version of the package. The kind of bound can be adjusted with [`--bounds`](../../reference/settings.md#add-bounds), or the constraint can be provided directly: ```console $ uv add "httpx>=0.20" ``` When adding a dependency from a source other than a package registry, uv will add an entry in the sources field. For example, when adding `httpx` from GitHub: ```console $ uv add "httpx @ git+https://github.com/encode/httpx" ``` The `pyproject.toml` will include a [Git source entry](#git): ```toml title="pyproject.toml" hl_lines="8-9" [project] name = "example" version = "0.1.0" dependencies = [ "httpx", ] [tool.uv.sources] httpx = { git = "https://github.com/encode/httpx" } ``` If a dependency cannot be used, uv will display an error.: ```console $ uv add "httpx>9999" × No solution found when resolving dependencies: ╰─▶ Because only httpx<=1.0.0b0 is available and your project depends on httpx>9999, we can conclude that your project's requirements are unsatisfiable. ``` ### Importing dependencies from requirements files Dependencies declared in a `requirements.txt` file can be added to the project with the `-r` option: ``` uv add -r requirements.txt ``` See the [pip migration guide](../../guides/migration/pip-to-project.md#importing-requirements-files) for more details. ## Removing dependencies To remove a dependency: ```console $ uv remove httpx ``` The `--dev`, `--group`, or `--optional` flags can be used to remove a dependency from a specific table. If a [source](#dependency-sources) is defined for the removed dependency, and there are no other references to the dependency, it will also be removed. ## Changing dependencies To change an existing dependency, e.g., to use a different constraint for `httpx`: ```console $ uv add "httpx>0.1.0" ``` !!! note In this example, we are changing the constraints for the dependency in the `pyproject.toml`. The locked version of the dependency will only change if necessary to satisfy the new constraints. To force the package version to update to the latest within the constraints, use `--upgrade-package `, e.g.: ```console $ uv add "httpx>0.1.0" --upgrade-package httpx ``` See the [lockfile](./sync.md#upgrading-locked-package-versions) documentation for more details on upgrading packages. Requesting a different dependency source will update the `tool.uv.sources` table, e.g., to use `httpx` from a local path during development: ```console $ uv add "httpx @ ../httpx" ``` ## Platform-specific dependencies To ensure that a dependency is only installed on a specific platform or on specific Python versions, use [environment markers](https://peps.python.org/pep-0508/#environment-markers). For example, to install `jax` on Linux, but not on Windows or macOS: ```console $ uv add "jax; sys_platform == 'linux'" ``` The resulting `pyproject.toml` will then include the environment marker in the dependency definition: ```toml title="pyproject.toml" hl_lines="6" [project] name = "project" version = "0.1.0" requires-python = ">=3.11" dependencies = ["jax; sys_platform == 'linux'"] ``` Similarly, to include `numpy` on Python 3.11 and later: ```console $ uv add "numpy; python_version >= '3.11'" ``` See Python's [environment marker](https://peps.python.org/pep-0508/#environment-markers) documentation for a complete enumeration of the available markers and operators. !!! tip Dependency sources can also be [changed per-platform](#platform-specific-sources). ## Project dependencies The `project.dependencies` table represents the dependencies that are used when uploading to PyPI or building a wheel. Individual dependencies are specified using [dependency specifiers](https://packaging.python.org/en/latest/specifications/dependency-specifiers/) syntax, and the table follows the [PEP 621](https://packaging.python.org/en/latest/specifications/pyproject-toml/) standard. `project.dependencies` defines the list of packages that are required for the project, along with the version constraints that should be used when installing them. Each entry includes a dependency name and version. An entry may include extras or environment markers for platform-specific packages. For example: ```toml title="pyproject.toml" [project] name = "albatross" version = "0.1.0" dependencies = [ # Any version in this range "tqdm >=4.66.2,<5", # Exactly this version of torch "torch ==2.2.2", # Install transformers with the torch extra "transformers[torch] >=4.39.3,<5", # Only install this package on older python versions # See "Environment Markers" for more information "importlib_metadata >=7.1.0,<8; python_version < '3.10'", "mollymawk ==0.1.0" ] ``` ## Dependency sources The `tool.uv.sources` table extends the standard dependency tables with alternative dependency sources, which are used during development. Dependency sources add support for common patterns that are not supported by the `project.dependencies` standard, like editable installations and relative paths. For example, to install `foo` from a directory relative to the project root: ```toml title="pyproject.toml" hl_lines="7" [project] name = "example" version = "0.1.0" dependencies = ["foo"] [tool.uv.sources] foo = { path = "./packages/foo" } ``` The following dependency sources are supported by uv: - [Index](#index): A package resolved from a specific package index. - [Git](#git): A Git repository. - [URL](#url): A remote wheel or source distribution. - [Path](#path): A local wheel, source distribution, or project directory. - [Workspace](#workspace-member): A member of the current workspace. !!! important Sources are only respected by uv. If another tool is used, only the definitions in the standard project tables will be used. If another tool is being used for development, any metadata provided in the source table will need to be re-specified in the other tool's format. ### Index To add Python package from a specific index, use the `--index` option: ```console $ uv add torch --index pytorch=https://download.pytorch.org/whl/cpu ``` uv will store the index in `[[tool.uv.index]]` and add a `[tool.uv.sources]` entry: ```toml title="pyproject.toml" [project] dependencies = ["torch"] [tool.uv.sources] torch = { index = "pytorch" } [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cpu" ``` !!! tip The above example will only work on x86-64 Linux, due to the specifics of the PyTorch index. See the [PyTorch guide](../../guides/integration/pytorch.md) for more information about setting up PyTorch. Using an `index` source _pins_ a package to the given index — it will not be downloaded from other indexes. When defining an index, an `explicit` flag can be included to indicate that the index should _only_ be used for packages that explicitly specify it in `tool.uv.sources`. If `explicit` is not set, other packages may be resolved from the index, if not found elsewhere. ```toml title="pyproject.toml" hl_lines="4" [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cpu" explicit = true ``` ### Git To add a Git dependency source, prefix a Git-compatible URL with `git+`. For example: ```console $ # Install over HTTP(S). $ uv add git+https://github.com/encode/httpx $ # Install over SSH. $ uv add git+ssh://git@github.com/encode/httpx ``` ```toml title="pyproject.toml" hl_lines="5" [project] dependencies = ["httpx"] [tool.uv.sources] httpx = { git = "https://github.com/encode/httpx" } ``` Specific Git references can be requested, e.g., a tag: ```console $ uv add git+https://github.com/encode/httpx --tag 0.27.0 ``` ```toml title="pyproject.toml" hl_lines="7" [project] dependencies = ["httpx"] [tool.uv.sources] httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" } ``` Or, a branch: ```console $ uv add git+https://github.com/encode/httpx --branch main ``` ```toml title="pyproject.toml" hl_lines="7" [project] dependencies = ["httpx"] [tool.uv.sources] httpx = { git = "https://github.com/encode/httpx", branch = "main" } ``` Or, a revision (commit): ```console $ uv add git+https://github.com/encode/httpx --rev 326b9431c761e1ef1e00b9f760d1f654c8db48c6 ``` ```toml title="pyproject.toml" hl_lines="7" [project] dependencies = ["httpx"] [tool.uv.sources] httpx = { git = "https://github.com/encode/httpx", rev = "326b9431c761e1ef1e00b9f760d1f654c8db48c6" } ``` A `subdirectory` may be specified if the package isn't in the repository root: ```console $ uv add git+https://github.com/langchain-ai/langchain#subdirectory=libs/langchain ``` ```toml title="pyproject.toml" [project] dependencies = ["langchain"] [tool.uv.sources] langchain = { git = "https://github.com/langchain-ai/langchain", subdirectory = "libs/langchain" } ``` Support for [Git LFS](https://git-lfs.com) is also configurable per source. By default, Git LFS objects will not be fetched. ```console $ uv add --lfs git+https://github.com/astral-sh/lfs-cowsay ``` ```toml title="pyproject.toml" [project] dependencies = ["lfs-cowsay"] [tool.uv.sources] lfs-cowsay = { git = "https://github.com/astral-sh/lfs-cowsay", lfs = true } ``` - When `lfs = true`, uv will always fetch LFS objects for this Git source. - When `lfs = false`, uv will never fetch LFS objects for this Git source. - When omitted, the `UV_GIT_LFS` environment variable is used for all Git sources without an explicit `lfs` configuration. !!! important Ensure Git LFS is installed and configured on your system before attempting to install sources using Git LFS, otherwise a build failure can occur. ### URL To add a URL source, provide a `https://` URL to either a wheel (ending in `.whl`) or a source distribution (typically ending in `.tar.gz` or `.zip`; see [here](../../concepts/resolution.md#source-distribution) for all supported formats). For example: ```console $ uv add "https://files.pythonhosted.org/packages/5c/2d/3da5bdf4408b8b2800061c339f240c1802f2e82d55e50bd39c5a881f47f0/httpx-0.27.0.tar.gz" ``` Will result in a `pyproject.toml` with: ```toml title="pyproject.toml" hl_lines="5" [project] dependencies = ["httpx"] [tool.uv.sources] httpx = { url = "https://files.pythonhosted.org/packages/5c/2d/3da5bdf4408b8b2800061c339f240c1802f2e82d55e50bd39c5a881f47f0/httpx-0.27.0.tar.gz" } ``` URL dependencies can also be manually added or edited in the `pyproject.toml` with the `{ url = }` syntax. A `subdirectory` may be specified if the source distribution isn't in the archive root. ### Path To add a path source, provide the path of a wheel (ending in `.whl`), a source distribution (typically ending in `.tar.gz` or `.zip`; see [here](../../concepts/resolution.md#source-distribution) for all supported formats), or a directory containing a `pyproject.toml`. For example: ```console $ uv add /example/foo-0.1.0-py3-none-any.whl ``` Will result in a `pyproject.toml` with: ```toml title="pyproject.toml" [project] dependencies = ["foo"] [tool.uv.sources] foo = { path = "/example/foo-0.1.0-py3-none-any.whl" } ``` The path may also be a relative path: ```console $ uv add ./foo-0.1.0-py3-none-any.whl ``` Or, a path to a project directory: ```console $ uv add ~/projects/bar/ ``` !!! important When using a directory as a path dependency, uv will attempt to build and install the target as a package by default. See the [virtual dependency](#virtual-dependencies) documentation for details. An [editable installation](#editable-dependencies) is not used for path dependencies by default. An editable installation may be requested for project directories: ```console $ uv add --editable ../projects/bar/ ``` Which will result in a `pyproject.toml` with: ```toml title="pyproject.toml" [project] dependencies = ["bar"] [tool.uv.sources] bar = { path = "../projects/bar", editable = true } ``` !!! tip For multiple packages in the same repository, [_workspaces_](./workspaces.md) may be a better fit. ### Workspace member To declare a dependency on a workspace member, add the member name with `{ workspace = true }`. All workspace members must be explicitly stated. Workspace members are always [editable](#editable-dependencies) . See the [workspace](./workspaces.md) documentation for more details on workspaces. ```toml title="pyproject.toml" [project] dependencies = ["foo==0.1.0"] [tool.uv.sources] foo = { workspace = true } [tool.uv.workspace] members = [ "packages/foo" ] ``` ### Platform-specific sources You can limit a source to a given platform or Python version by providing [dependency specifiers](https://packaging.python.org/en/latest/specifications/dependency-specifiers/)-compatible environment markers for the source. For example, to pull `httpx` from GitHub, but only on macOS, use the following: ```toml title="pyproject.toml" hl_lines="8" [project] dependencies = ["httpx"] [tool.uv.sources] httpx = { git = "https://github.com/encode/httpx", tag = "0.27.2", marker = "sys_platform == 'darwin'" } ``` By specifying the marker on the source, uv will still include `httpx` on all platforms, but will download the source from GitHub on macOS, and fall back to PyPI on all other platforms. ### Multiple sources You can specify multiple sources for a single dependency by providing a list of sources, disambiguated by [PEP 508](https://peps.python.org/pep-0508/#environment-markers)-compatible environment markers. For example, to pull in different `httpx` tags on macOS vs. Linux: ```toml title="pyproject.toml" hl_lines="6-7" [project] dependencies = ["httpx"] [tool.uv.sources] httpx = [ { git = "https://github.com/encode/httpx", tag = "0.27.2", marker = "sys_platform == 'darwin'" }, { git = "https://github.com/encode/httpx", tag = "0.24.1", marker = "sys_platform == 'linux'" }, ] ``` This strategy extends to using different indexes based on environment markers. For example, to install `torch` from different PyTorch indexes based on the platform: ```toml title="pyproject.toml" hl_lines="6-7" [project] dependencies = ["torch"] [tool.uv.sources] torch = [ { index = "torch-cpu", marker = "platform_system == 'Darwin'"}, { index = "torch-gpu", marker = "platform_system == 'Linux'"}, ] [[tool.uv.index]] name = "torch-cpu" url = "https://download.pytorch.org/whl/cpu" explicit = true [[tool.uv.index]] name = "torch-gpu" url = "https://download.pytorch.org/whl/cu124" explicit = true ``` ### Disabling sources To instruct uv to ignore the `tool.uv.sources` table (e.g., to simulate resolving with the package's published metadata), use the `--no-sources` flag: ```console $ uv lock --no-sources ``` The use of `--no-sources` will also prevent uv from discovering any [workspace members](#workspace-member) that could satisfy a given dependency. ## Optional dependencies It is common for projects that are published as libraries to make some features optional to reduce the default dependency tree. For example, Pandas has an [`excel` extra](https://pandas.pydata.org/docs/getting_started/install.html#excel-files) and a [`plot` extra](https://pandas.pydata.org/docs/getting_started/install.html#visualization) to avoid installation of Excel parsers and `matplotlib` unless someone explicitly requires them. Extras are requested with the `package[]` syntax, e.g., `pandas[plot, excel]`. Optional dependencies are specified in `[project.optional-dependencies]`, a TOML table that maps from extra name to its dependencies, following [dependency specifiers](#dependency-specifiers) syntax. Optional dependencies can have entries in `tool.uv.sources` the same as normal dependencies. ```toml title="pyproject.toml" [project] name = "pandas" version = "1.0.0" [project.optional-dependencies] plot = [ "matplotlib>=3.6.3" ] excel = [ "odfpy>=1.4.1", "openpyxl>=3.1.0", "python-calamine>=0.1.7", "pyxlsb>=1.0.10", "xlrd>=2.0.1", "xlsxwriter>=3.0.5" ] ``` To add an optional dependency, use the `--optional ` option: ```console $ uv add httpx --optional network ``` !!! note If you have optional dependencies that conflict with one another, resolution will fail unless you explicitly [declare them as conflicting](./config.md#conflicting-dependencies). Sources can also be declared as applying only to a specific optional dependency. For example, to pull `torch` from different PyTorch indexes based on an optional `cpu` or `gpu` extra: ```toml title="pyproject.toml" [project] dependencies = [] [project.optional-dependencies] cpu = [ "torch", ] gpu = [ "torch", ] [tool.uv.sources] torch = [ { index = "torch-cpu", extra = "cpu" }, { index = "torch-gpu", extra = "gpu" }, ] [[tool.uv.index]] name = "torch-cpu" url = "https://download.pytorch.org/whl/cpu" [[tool.uv.index]] name = "torch-gpu" url = "https://download.pytorch.org/whl/cu124" ``` ## Development dependencies Unlike optional dependencies, development dependencies are local-only and will _not_ be included in the project requirements when published to PyPI or other indexes. As such, development dependencies are not included in the `[project]` table. Development dependencies can have entries in `tool.uv.sources` the same as normal dependencies. To add a development dependency, use the `--dev` flag: ```console $ uv add --dev pytest ``` uv uses the `[dependency-groups]` table (as defined in [PEP 735](https://peps.python.org/pep-0735/)) for declaration of development dependencies. The above command will create a `dev` group: ```toml title="pyproject.toml" [dependency-groups] dev = [ "pytest >=8.1.1,<9" ] ``` The `dev` group is special-cased; there are `--dev`, `--only-dev`, and `--no-dev` flags to toggle inclusion or exclusion of its dependencies. See `--no-default-groups` to disable all default groups instead. Additionally, the `dev` group is [synced by default](#default-groups). ### Dependency groups Development dependencies can be divided into multiple groups, using the `--group` flag. For example, to add a development dependency in the `lint` group: ```console $ uv add --group lint ruff ``` Which results in the following `[dependency-groups]` definition: ```toml title="pyproject.toml" [dependency-groups] dev = [ "pytest" ] lint = [ "ruff" ] ``` Once groups are defined, the `--all-groups`, `--no-default-groups`, `--group`, `--only-group`, and `--no-group` options can be used to include or exclude their dependencies. !!! tip The `--dev`, `--only-dev`, and `--no-dev` flags are equivalent to `--group dev`, `--only-group dev`, and `--no-group dev` respectively. uv requires that all dependency groups are compatible with each other and resolves all groups together when creating the lockfile. If dependencies declared in one group are not compatible with those in another group, uv will fail to resolve the requirements of the project with an error. !!! note If you have dependency groups that conflict with one another, resolution will fail unless you explicitly [declare them as conflicting](./config.md#conflicting-dependencies). ### Nesting groups A dependency group can include other dependency groups, e.g.: ```toml title="pyproject.toml" [dependency-groups] dev = [ {include-group = "lint"}, {include-group = "test"} ] lint = [ "ruff" ] test = [ "pytest" ] ``` An included group's dependencies cannot conflict with the other dependencies declared in a group. ### Default groups By default, uv includes the `dev` dependency group in the environment (e.g., during `uv run` or `uv sync`). The default groups to include can be changed using the `tool.uv.default-groups` setting. ```toml title="pyproject.toml" [tool.uv] default-groups = ["dev", "foo"] ``` To enable all dependencies groups by default, use `"all"` instead of listing group names: ```toml title="pyproject.toml" [tool.uv] default-groups = "all" ``` !!! tip To disable this behaviour during `uv run` or `uv sync`, use `--no-default-groups`. To exclude a specific default group, use `--no-group `. ### Group `requires-python` By default, dependency groups must be compatible with your project's `requires-python` range. If a dependency group requires a different range of Python versions than your project, you can specify a `requires-python` for the group in `[tool.uv.dependency-groups]`, e.g.: ```toml title="pyproject.toml" hl_lines="9-10" [project] name = "example" version = "0.0.0" requires-python = ">=3.10" [dependency-groups] dev = ["pytest"] [tool.uv.dependency-groups] dev = {requires-python = ">=3.12"} ``` ### Legacy `dev-dependencies` Before `[dependency-groups]` was standardized, uv used the `tool.uv.dev-dependencies` field to specify development dependencies, e.g.: ```toml title="pyproject.toml" [tool.uv] dev-dependencies = [ "pytest" ] ``` Dependencies declared in this section will be combined with the contents in the `dependency-groups.dev`. Eventually, the `dev-dependencies` field will be deprecated and removed. !!! note If a `tool.uv.dev-dependencies` field exists, `uv add --dev` will use the existing section instead of adding a new `dependency-groups.dev` section. ## Build dependencies If a project is structured as [Python package](./config.md#build-systems), it may declare dependencies that are required to build the project, but not required to run it. These dependencies are specified in the `[build-system]` table under `build-system.requires`, following [PEP 518](https://peps.python.org/pep-0518/). For example, if a project uses `setuptools` as its build backend, it should declare `setuptools` as a build dependency: ```toml title="pyproject.toml" [project] name = "pandas" version = "0.1.0" [build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" ``` By default, uv will respect `tool.uv.sources` when resolving build dependencies. For example, to use a local version of `setuptools` for building, add the source to `tool.uv.sources`: ```toml title="pyproject.toml" [project] name = "pandas" version = "0.1.0" [build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" [tool.uv.sources] setuptools = { path = "./packages/setuptools" } ``` When publishing a package, we recommend running `uv build --no-sources` to ensure that the package builds correctly when `tool.uv.sources` is disabled, as is the case when using other build tools, like [`pypa/build`](https://github.com/pypa/build). ## Editable dependencies A regular installation of a directory with a Python package first builds a wheel and then installs that wheel into your virtual environment, copying all source files. When the package source files are edited, the virtual environment will contain outdated versions. Editable installations solve this problem by adding a link to the project within the virtual environment (a `.pth` file), which instructs the interpreter to include the source files directly. There are some limitations to editables (mainly: the build backend needs to support them, and native modules aren't recompiled before import), but they are useful for development, as the virtual environment will always use the latest changes to the package. uv uses editable installation for workspace packages by default. To add an editable dependency, use the `--editable` flag: ```console $ uv add --editable ./path/foo ``` Or, to opt-out of using an editable dependency in a workspace: ```console $ uv add --no-editable ./path/foo ``` ## Virtual dependencies uv allows dependencies to be "virtual", in which the dependency itself is not installed as a [package](./config.md#project-packaging), but its dependencies are. By default, dependencies are never virtual. A dependency with a [`path` source](#path) can be virtual if it explicitly sets [`tool.uv.package = false`](../../reference/settings.md#package). Unlike working _in_ the dependent project with uv, the package will be built even if a [build system](./config.md#build-systems) is not declared. To treat a dependency as virtual, set `package = false` on the source: ```toml title="pyproject.toml" [project] dependencies = ["bar"] [tool.uv.sources] bar = { path = "../projects/bar", package = false } ``` If a dependency sets `tool.uv.package = false`, it can be overridden by declaring `package = true` on the source: ```toml title="pyproject.toml" [project] dependencies = ["bar"] [tool.uv.sources] bar = { path = "../projects/bar", package = true } ``` Similarly, a dependency with a [`workspace` source](#workspace-member) can be virtual if it explicitly sets [`tool.uv.package = false`](../../reference/settings.md#package). The workspace member will be built even if a [build system](./config.md#build-systems) is not declared. Workspace members that are _not_ dependencies can be virtual by default, e.g., if the parent `pyproject.toml` is: ```toml title="pyproject.toml" [project] name = "parent" version = "1.0.0" dependencies = [] [tool.uv.workspace] members = ["child"] ``` And the child `pyproject.toml` excluded a build system: ```toml title="pyproject.toml" [project] name = "child" version = "1.0.0" dependencies = ["anyio"] ``` Then the `child` workspace member would not be installed, but the transitive dependency `anyio` would be. In contrast, if the parent declared a dependency on `child`: ```toml title="pyproject.toml" [project] name = "parent" version = "1.0.0" dependencies = ["child"] [tool.uv.sources] child = { workspace = true } [tool.uv.workspace] members = ["child"] ``` Then `child` would be built and installed. ## Dependency specifiers uv uses standard [dependency specifiers](https://packaging.python.org/en/latest/specifications/dependency-specifiers/), originally defined in [PEP 508](https://peps.python.org/pep-0508/). A dependency specifier is composed of, in order: - The dependency name - The extras you want (optional) - The version specifier - An environment marker (optional) The version specifiers are comma separated and added together, e.g., `foo >=1.2.3,<2,!=1.4.0` is interpreted as "a version of `foo` that's at least 1.2.3, but less than 2, and not 1.4.0". Specifiers are padded with trailing zeros if required, so `foo ==2` matches foo 2.0.0, too. A star can be used for the last digit with equals, e.g., `foo ==2.1.*` will accept any release from the 2.1 series. Similarly, `~=` matches where the last digit is equal or higher, e.g., `foo ~=1.2` is equal to `foo >=1.2,<2`, and `foo ~=1.2.3` is equal to `foo >=1.2.3,<1.3`. Extras are comma-separated in square bracket between name and version, e.g., `pandas[excel,plot] ==2.2`. Whitespace between extra names is ignored. Some dependencies are only required in specific environments, e.g., a specific Python version or operating system. For example to install the `importlib-metadata` backport for the `importlib.metadata` module, use `importlib-metadata >=7.1.0,<8; python_version < '3.10'`. To install `colorama` on Windows (but omit it on other platforms), use `colorama >=0.4.6,<5; platform_system == "Windows"`. Markers are combined with `and`, `or`, and parentheses, e.g., `aiohttp >=3.7.4,<4; (sys_platform != 'win32' or implementation_name != 'pypy') and python_version >= '3.10'`. Note that versions within markers must be quoted, while versions _outside_ of markers must _not_ be quoted. uv-0.9.17+ds1/docs/concepts/projects/export.md000066400000000000000000000075211520155276700212060ustar00rootroot00000000000000--- title: Exporting a lockfile description: Exporting a lockfile to different formats --- # Exporting a lockfile uv can export a lockfile to different formats for integration with other tools and workflows. The `uv export` command supports multiple output formats, each suited to different use cases. For more details on lockfiles and how they're created, see the [project layout](./layout.md) and [locking and syncing](./sync.md) documentation. ## Overview of export formats uv supports three export formats: - `requirements.txt`: The traditional pip-compatible [requirements file format](https://pip.pypa.io/en/stable/reference/requirements-file-format/). - `pylock.toml`: The standardized Python lockfile format defined in [PEP 751](https://peps.python.org/pep-0751/). - `CycloneDX`: An industry-standard [Software Bill of Materials (SBOM)](https://cyclonedx.org/) format. The format can be specified with the `--format` flag: ```console $ uv export --format requirements.txt $ uv export --format pylock.toml $ uv export --format cyclonedx1.5 ``` !!! tip By default, `uv export` prints to stdout. Use `--output-file` to write to a file for any format: ```console $ uv export --format requirements.txt --output-file requirements.txt $ uv export --format pylock.toml --output-file pylock.toml $ uv export --format cyclonedx1.5 --output-file sbom.json ``` ## `requirements.txt` format The `requirements.txt` format is the most widely supported format for Python dependencies. It can be used with `pip` and other Python package managers. ### Basic usage ```console $ uv export --format requirements.txt ``` The generated `requirements.txt` file can then be installed via `uv pip install`, or with other tools like `pip`. !!! note In general, we recommend against using both a `uv.lock` and a `requirements.txt` file. The `uv.lock` format is more powerful and includes features that cannot be expressed in `requirements.txt`. If you find yourself exporting a `uv.lock` file, consider opening an issue to discuss your use case. ## `pylock.toml` format [PEP 751](https://peps.python.org/pep-0751/) defines a TOML-based lockfile format for Python dependencies. uv can export your project's dependency lockfile to this format. ### Basic usage ```console $ uv export --format pylock.toml ``` ## CycloneDX SBOM format uv can export your project's dependency lockfile as a Software Bill of Materials (SBOM) in CycloneDX format. SBOMs provide a comprehensive inventory of all software components in your application, which is useful for security auditing, compliance, and supply chain transparency. !!! important Support for exporting to CycloneDX is in [preview](../preview.md), and may change in any future release. ### What is CycloneDX? [CycloneDX](https://cyclonedx.org/) is an industry-standard format for creating Software Bill of Materials. CycloneDX is machine readable and widely supported by security scanning tools, vulnerability databases, and Software Composition Analysis (SCA) platforms. ### Basic usage To export your project's lockfile as a CycloneDX SBOM: ```console $ uv export --format cyclonedx1.5 ``` This will generate a JSON-encoded CycloneDX v1.5 document containing your project and all of its dependencies. ### SBOM Structure The generated SBOM follows the [CycloneDX specification](https://cyclonedx.org/specification/overview/). uv also includes the following custom properties on components: - `uv:package:marker`: Environment markers (e.g., `python_version >= "3.8"`) - `uv:workspace:path`: Relative path for workspace members ## Next steps To learn more about lockfiles and exporting, see the [locking and syncing](./sync.md) documentation and the [command reference](../../reference/cli.md#uv-export). Or, read on to learn how to [build and publish your project to a package index](../../guides/package.md). uv-0.9.17+ds1/docs/concepts/projects/index.md000066400000000000000000000013641520155276700207730ustar00rootroot00000000000000# Projects Projects help manage Python code spanning multiple files. !!! tip Looking for an introduction to creating a project with uv? See the [projects guide](../../guides/projects.md) first. Working on projects is a core part of the uv experience. Learn more about using projects: - [Understanding project structure and files](./layout.md) - [Creating new projects](./init.md) - [Managing project dependencies](./dependencies.md) - [Running commands and scripts in a project](./run.md) - [Using lockfiles and syncing the environment](./sync.md) - [Configuring the project for advanced use cases](./config.md) - [Building distributions to publish a project](./build.md) - [Using workspaces to work on multiple projects at once](./workspaces.md) uv-0.9.17+ds1/docs/concepts/projects/init.md000066400000000000000000000214661520155276700206340ustar00rootroot00000000000000# Creating projects uv supports creating a project with `uv init`. When creating projects, uv supports two basic templates: [**applications**](#applications) and [**libraries**](#libraries). By default, uv will create a project for an application. The `--lib` flag can be used to create a project for a library instead. ## Target directory uv will create a project in the working directory, or, in a target directory by providing a name, e.g., `uv init foo`. The working directory can be modified with the `--directory` option, which will cause the target directory path will be interpreted relative to the specified working directory. If there's already a project in the target directory, i.e., if there's a `pyproject.toml`, uv will exit with an error. ## Applications Application projects are suitable for web servers, scripts, and command-line interfaces. Applications are the default target for `uv init`, but can also be specified with the `--app` flag. ```console $ uv init example-app ``` The project includes a `pyproject.toml`, a sample file (`main.py`), a readme, and a Python version pin file (`.python-version`). ```console $ tree example-app example-app ├── .python-version ├── README.md ├── main.py └── pyproject.toml ``` !!! note Prior to v0.6.0, uv created a file named `hello.py` instead of `main.py`. The `pyproject.toml` includes basic metadata. It does not include a build system, it is not a [package](./config.md#project-packaging) and will not be installed into the environment: ```toml title="pyproject.toml" [project] name = "example-app" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.11" dependencies = [] ``` The sample file defines a `main` function with some standard boilerplate: ```python title="main.py" def main(): print("Hello from example-app!") if __name__ == "__main__": main() ``` Python files can be executed with `uv run`: ```console $ cd example-app $ uv run main.py Hello from example-project! ``` ## Packaged applications Many use-cases require a [package](./config.md#project-packaging). For example, if you are creating a command-line interface that will be published to PyPI or if you want to define tests in a dedicated directory. The `--package` flag can be used to create a packaged application: ```console $ uv init --package example-pkg ``` The source code is moved into a `src` directory with a module directory and an `__init__.py` file: ```console $ tree example-pkg example-pkg ├── .python-version ├── README.md ├── pyproject.toml └── src └── example_pkg └── __init__.py ``` A [build system](./config.md#build-systems) is defined, so the project will be installed into the environment: ```toml title="pyproject.toml" hl_lines="12-14" [project] name = "example-pkg" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.11" dependencies = [] [project.scripts] example-pkg = "example_pkg:main" [build-system] requires = ["uv_build>=0.9.17,<0.10.0"] build-backend = "uv_build" ``` !!! tip The `--build-backend` option can be used to request an alternative build system. A [command](./config.md#entry-points) definition is included: ```toml title="pyproject.toml" hl_lines="9 10" [project] name = "example-pkg" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.11" dependencies = [] [project.scripts] example-pkg = "example_pkg:main" [build-system] requires = ["uv_build>=0.9.17,<0.10.0"] build-backend = "uv_build" ``` The command can be executed with `uv run`: ```console $ cd example-pkg $ uv run example-pkg Hello from example-pkg! ``` ## Libraries A library provides functions and objects for other projects to consume. Libraries are intended to be built and distributed, e.g., by uploading them to PyPI. Libraries can be created by using the `--lib` flag: ```console $ uv init --lib example-lib ``` !!! note Using `--lib` implies `--package`. Libraries always require a packaged project. As with a [packaged application](#packaged-applications), a `src` layout is used. A `py.typed` marker is included to indicate to consumers that types can be read from the library: ```console $ tree example-lib example-lib ├── .python-version ├── README.md ├── pyproject.toml └── src └── example_lib ├── py.typed └── __init__.py ``` !!! note A `src` layout is particularly valuable when developing libraries. It ensures that the library is isolated from any `python` invocations in the project root and that distributed library code is well separated from the rest of the project source. A [build system](./config.md#build-systems) is defined, so the project will be installed into the environment: ```toml title="pyproject.toml" hl_lines="12-14" [project] name = "example-lib" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.11" dependencies = [] [build-system] requires = ["uv_build>=0.9.17,<0.10.0"] build-backend = "uv_build" ``` !!! tip You can select a different build backend template by using `--build-backend` with `hatchling`, `uv_build`, `flit-core`, `pdm-backend`, `setuptools`, `maturin`, or `scikit-build-core`. An alternative backend is required if you want to create a [library with extension modules](#projects-with-extension-modules). The created module defines a simple API function: ```python title="__init__.py" def hello() -> str: return "Hello from example-lib!" ``` And you can import and execute it using `uv run`: ```console $ cd example-lib $ uv run python -c "import example_lib; print(example_lib.hello())" Hello from example-lib! ``` ## Projects with extension modules Most Python projects are "pure Python", meaning they do not define modules in other languages like C, C++, FORTRAN, or Rust. However, projects with extension modules are often used for performance sensitive code. Creating a project with an extension module requires choosing an alternative build system. uv supports creating projects with the following build systems that support building extension modules: - [`maturin`](https://www.maturin.rs) for projects with Rust - [`scikit-build-core`](https://github.com/scikit-build/scikit-build-core) for projects with C, C++, FORTRAN, Cython Specify the build system with the `--build-backend` flag: ```console $ uv init --build-backend maturin example-ext ``` !!! note Using `--build-backend` implies `--package`. The project contains a `Cargo.toml` and a `lib.rs` file in addition to the typical Python project files: ```console $ tree example-ext example-ext ├── .python-version ├── Cargo.toml ├── README.md ├── pyproject.toml └── src ├── lib.rs └── example_ext ├── __init__.py └── _core.pyi ``` !!! note If using `scikit-build-core`, you'll see CMake configuration and a `main.cpp` file instead. The Rust library defines a simple function: ```rust title="src/lib.rs" use pyo3::prelude::*; #[pymodule] mod _core { use pyo3::prelude::*; #[pyfunction] fn hello_from_bin() -> String { "Hello from example-ext!".to_string() } } ``` And the Python module imports it: ```python title="src/example_ext/__init__.py" from example_ext._core import hello_from_bin def main() -> None: print(hello_from_bin()) ``` The command can be executed with `uv run`: ```console $ cd example-ext $ uv run example-ext Hello from example-ext! ``` !!! important When creating a project with maturin or scikit-build-core, uv configures [`tool.uv.cache-keys`](https://docs.astral.sh/uv/reference/settings/#cache-keys) to include common source file types. To force a rebuild, e.g. when changing files outside `cache-keys` or when not using `cache-keys`, use `--reinstall`. ## Creating a minimal project If you only want to create a `pyproject.toml`, use the `--bare` option: ```console $ uv init example --bare ``` uv will skip creating a Python version pin file, a README, and any source directories or files. Additionally, uv will not initialize a version control system (i.e., `git`). ```console $ tree example-bare example-bare └── pyproject.toml ``` uv will also not add extra metadata to the `pyproject.toml`, such as the `description` or `authors`. ```toml [project] name = "example" version = "0.1.0" requires-python = ">=3.12" dependencies = [] ``` The `--bare` option can be used with other options like `--lib` or `--build-backend` — in these cases uv will still configure a build system but will not create the expected file structure. When `--bare` is used, additional features can still be used opt-in: ```console $ uv init example --bare --description "Hello world" --author-from git --vcs git --python-pin ``` uv-0.9.17+ds1/docs/concepts/projects/layout.md000066400000000000000000000111671520155276700212030ustar00rootroot00000000000000# Project structure and files ## The `pyproject.toml` Python project metadata is defined in a [`pyproject.toml`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/) file. uv requires this file to identify the root directory of a project. !!! tip `uv init` can be used to create a new project. See [Creating projects](./init.md) for details. A minimal project definition includes a name and version: ```toml title="pyproject.toml" [project] name = "example" version = "0.1.0" ``` Additional project metadata and configuration includes: - [Python version requirement](./config.md#python-version-requirement) - [Dependencies](./dependencies.md) - [Build system](./config.md#build-systems) - [Entry points (commands)](./config.md#entry-points) ## The project environment When working on a project with uv, uv will create a virtual environment as needed. While some uv commands will create a temporary environment (e.g., `uv run --isolated`), uv also manages a persistent environment with the project and its dependencies in a `.venv` directory next to the `pyproject.toml`. It is stored inside the project to make it easy for editors to find — they need the environment to give code completions and type hints. It is not recommended to include the `.venv` directory in version control; it is automatically excluded from `git` with an internal `.gitignore` file. To run a command in the project environment, use `uv run`. Alternatively the project environment can be activated as normal for a virtual environment. When `uv run` is invoked, it will create the project environment if it does not exist yet or ensure it is up-to-date if it exists. The project environment can also be explicitly created with `uv sync`. See the [locking and syncing](./sync.md) documentation for details. It is _not_ recommended to modify the project environment manually, e.g., with `uv pip install`. For project dependencies, use `uv add` to add a package to the environment. For one-off requirements, use [`uvx`](../../guides/tools.md) or [`uv run --with`](./run.md#requesting-additional-dependencies). !!! tip If you don't want uv to manage the project environment, set [`managed = false`](../../reference/settings.md#managed) to disable automatic locking and syncing of the project. For example: ```toml title="pyproject.toml" [tool.uv] managed = false ``` ## The lockfile uv creates a `uv.lock` file next to the `pyproject.toml`. `uv.lock` is a _universal_ or _cross-platform_ lockfile that captures the packages that would be installed across all possible Python markers such as operating system, architecture, and Python version. Unlike the `pyproject.toml`, which is used to specify the broad requirements of your project, the lockfile contains the exact resolved versions that are installed in the project environment. This file should be checked into version control, allowing for consistent and reproducible installations across machines. A lockfile ensures that developers working on the project are using a consistent set of package versions. Additionally, it ensures when deploying the project as an application that the exact set of used package versions is known. The lockfile is [automatically created and updated](./sync.md#automatic-lock-and-sync) during uv invocations that use the project environment, i.e., `uv sync` and `uv run`. The lockfile may also be explicitly updated using `uv lock`. `uv.lock` is a human-readable TOML file but is managed by uv and should not be edited manually. The `uv.lock` format is specific to uv and not usable by other tools. ### Relationship to `pylock.toml` In [PEP 751](https://peps.python.org/pep-0751/), Python standardized a new resolution file format, `pylock.toml`. `pylock.toml` is a resolution output format intended to replace `requirements.txt` (e.g., in the context of `uv pip compile`, whereby a "locked" `requirements.txt` file is generated from a set of input requirements). `pylock.toml` is standardized and tool-agnostic, such that in the future, `pylock.toml` files generated by uv could be installed by other tools, and vice versa. Some of uv's functionality cannot be expressed in the `pylock.toml` format; as such, uv will continue to use the `uv.lock` format within the project interface. However, uv supports `pylock.toml` as an export target and in the `uv pip` CLI. For example: - To export a `uv.lock` to the `pylock.toml` format, run: `uv export -o pylock.toml` - To generate a `pylock.toml` file from a set of requirements, run: `uv pip compile requirements.in -o pylock.toml` - To install from a `pylock.toml` file, run: `uv pip sync pylock.toml` or `uv pip install -r pylock.toml` uv-0.9.17+ds1/docs/concepts/projects/run.md000066400000000000000000000066051520155276700204730ustar00rootroot00000000000000# Running commands in projects When working on a project, it is installed into the virtual environment at `.venv`. This environment is isolated from the current shell by default, so invocations that require the project, e.g., `python -c "import example"`, will fail. Instead, use `uv run` to run commands in the project environment: ```console $ uv run python -c "import example" ``` When using `run`, uv will ensure that the project environment is up-to-date before running the given command. The given command can be provided by the project environment or exist outside of it, e.g.: ```console $ # Presuming the project provides `example-cli` $ uv run example-cli foo $ # Running a `bash` script that requires the project to be available $ uv run bash scripts/foo.sh ``` ## Requesting additional dependencies Additional dependencies or different versions of dependencies can be requested per invocation. The `--with` option is used to include a dependency for the invocation, e.g., to request a different version of `httpx`: ```console $ uv run --with httpx==0.26.0 python -c "import httpx; print(httpx.__version__)" 0.26.0 $ uv run --with httpx==0.25.0 python -c "import httpx; print(httpx.__version__)" 0.25.0 ``` The requested version will be respected regardless of the project's requirements. For example, even if the project requires `httpx==0.24.0`, the output above would be the same. ## Running scripts Scripts that declare inline metadata are automatically executed in environments isolated from the project. See the [scripts guide](../../guides/scripts.md#declaring-script-dependencies) for more details. For example, given a script: ```python title="example.py" # /// script # dependencies = [ # "httpx", # ] # /// import httpx resp = httpx.get("https://peps.python.org/api/peps.json") data = resp.json() print([(k, v["title"]) for k, v in data.items()][:10]) ``` The invocation `uv run example.py` would run _isolated_ from the project with only the given dependencies listed. ## Legacy scripts on Windows Support is provided for [legacy setuptools scripts](https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#scripts). These types of scripts are additional files installed by setuptools in `.venv\Scripts`. Currently only legacy scripts with the `.ps1`, `.cmd`, and `.bat` extensions are supported. For example, below is an example running a Command Prompt script. ```console $ uv run --with nuitka==2.6.7 -- nuitka.cmd --version ``` In addition, you don't need to specify the extension. `uv` will automatically look for files ending in `.ps1`, `.cmd`, and `.bat` in that order of execution on your behalf. ```console $ uv run --with nuitka==2.6.7 -- nuitka --version ``` ## Signal handling uv does not cede control of the process to the spawned command in order to provide better error messages on failure. Consequently, uv is responsible for forwarding some signals to the child process the requested command runs in. On Unix systems, uv will forward most signals (with the exception of SIGKILL, SIGCHLD, SIGIO, and SIGPOLL) to the child process. Since terminals send SIGINT to the foreground process group on Ctrl-C, uv will only forward a SIGINT to the child process if it is sent more than once or the child process group differs from uv's. On Windows, these concepts do not apply and uv ignores Ctrl-C events, deferring handling to the child process so it can exit cleanly. uv-0.9.17+ds1/docs/concepts/projects/sync.md000066400000000000000000000162701520155276700206420ustar00rootroot00000000000000# Locking and syncing Locking is the process of resolving your project's dependencies into a [lockfile](./layout.md#the-lockfile). Syncing is the process of installing a subset of packages from the lockfile into the [project environment](./layout.md#the-project-environment). ## Automatic lock and sync Locking and syncing are _automatic_ in uv. For example, when `uv run` is used, the project is locked and synced before invoking the requested command. This ensures the project environment is always up-to-date. Similarly, commands which read the lockfile, such as `uv tree`, will automatically update it before running. To disable automatic locking, use the `--locked` option: ```console $ uv run --locked ... ``` If the lockfile is not up-to-date, uv will raise an error instead of updating the lockfile. To use the lockfile without checking if it is up-to-date, use the `--frozen` option: ```console $ uv run --frozen ... ``` Similarly, to run a command without checking if the environment is up-to-date, use the `--no-sync` option: ```console $ uv run --no-sync ... ``` ## Checking the lockfile When considering if the lockfile is up-to-date, uv will check if it matches the project metadata. For example, if you add a dependency to your `pyproject.toml`, the lockfile will be considered outdated. Similarly, if you change the version constraints for a dependency such that the locked version is excluded, the lockfile will be considered outdated. However, if you change the version constraints such that the existing locked version is still included, the lockfile will still be considered up-to-date. You can check if the lockfile is up-to-date by passing the `--check` flag to `uv lock`: ```console $ uv lock --check ``` This is equivalent to the `--locked` flag for other commands. !!! important uv will not consider lockfiles outdated when new versions of packages are released — the lockfile needs to be explicitly updated if you want to upgrade dependencies. See the documentation on [upgrading locked package versions](#upgrading-locked-package-versions) for details. ## Creating the lockfile While the lockfile is created [automatically](#automatic-lock-and-sync), the lockfile may also be explicitly created or updated using `uv lock`: ```console $ uv lock ``` ## Syncing the environment While the environment is synced [automatically](#automatic-lock-and-sync), it may also be explicitly synced using `uv sync`: ```console $ uv sync ``` Syncing the environment manually is especially useful for ensuring your editor has the correct versions of dependencies. ### Editable installation When the environment is synced, uv will install the project (and other workspace members) as _editable_ packages, such that re-syncing is not necessary for changes to be reflected in the environment. To opt-out of this behavior, use the `--no-editable` option. !!! note If the project does not define a build system, it will not be installed. See the [build systems](./config.md#build-systems) documentation for details. ### Retaining extraneous packages Syncing is "exact" by default, which means it will remove any packages that are not present in the lockfile. To retain extraneous packages, use the `--inexact` option: ```console $ uv sync --inexact ``` ### Syncing optional dependencies uv reads optional dependencies from the `[project.optional-dependencies]` table. These are frequently referred to as "extras". uv does not sync extras by default. Use the `--extra` option to include an extra. ```console $ uv sync --extra foo ``` To quickly enable all extras, use the `--all-extras` option. See the [optional dependencies](./dependencies.md#optional-dependencies) documentation for details on how to manage optional dependencies. ### Syncing development dependencies uv reads development dependencies from the `[dependency-groups]` table (as defined in [PEP 735](https://peps.python.org/pep-0735/)). The `dev` group is special-cased and synced by default. See the [default groups](./dependencies.md#default-groups) documentation for details on changing the defaults. The `--no-dev` flag can be used to exclude the `dev` group. The `--only-dev` flag can be used to install the `dev` group _without_ the project and its dependencies. Additional groups can be included or excluded with the `--all-groups`, `--no-default-groups`, `--group `, `--only-group `, and `--no-group ` options. The semantics of `--only-group` are the same as `--only-dev`, the project will not be included. However, `--only-group` will also exclude default groups. Group exclusions always take precedence over inclusions, so given the command: ``` $ uv sync --no-group foo --group foo ``` The `foo` group would not be installed. See the [development dependencies](./dependencies.md#development-dependencies) documentation for details on how to manage development dependencies. ## Upgrading locked package versions With an existing `uv.lock` file, uv will prefer the previously locked versions of packages when running `uv sync` and `uv lock`. Package versions will only change if the project's dependency constraints exclude the previous, locked version. To upgrade all packages: ```console $ uv lock --upgrade ``` To upgrade a single package to the latest version, while retaining the locked versions of all other packages: ```console $ uv lock --upgrade-package ``` To upgrade a single package to a specific version: ```console $ uv lock --upgrade-package == ``` In all cases, upgrades are limited to the project's dependency constraints. For example, if the project defines an upper bound for a package then an upgrade will not go beyond that version. !!! note uv applies similar logic to Git dependencies. For example, if a Git dependency references the `main` branch, uv will prefer the locked commit SHA in an existing `uv.lock` file over the latest commit on the `main` branch, unless the `--upgrade` or `--upgrade-package` flags are used. These flags can also be provided to `uv sync` or `uv run` to update the lockfile _and_ the environment. ## Exporting the lockfile If you need to integrate uv with other tools or workflows, you can export `uv.lock` to different formats including `requirements.txt`, `pylock.toml` (PEP 751), and CycloneDX SBOM. ```console $ uv export --format requirements.txt $ uv export --format pylock.toml $ uv export --format cyclonedx1.5 ``` See the [export guide](./export.md) for comprehensive documentation on all export formats and their use cases. ## Partial installations Sometimes it's helpful to perform installations in multiple steps, e.g., for optimal layer caching while building a Docker image. `uv sync` has several flags for this purpose. - `--no-install-project`: Do not install the current project - `--no-install-workspace`: Do not install any workspace members, including the root project - `--no-install-package `: Do not install the given package(s) When these options are used, all the dependencies of the target are still installed. For example, `--no-install-project` will omit the _project_ but not any of its dependencies. If used improperly, these flags can result in a broken environment since a package can be missing its dependencies. uv-0.9.17+ds1/docs/concepts/projects/workspaces.md000066400000000000000000000175031520155276700220470ustar00rootroot00000000000000# Using workspaces Inspired by the [Cargo](https://doc.rust-lang.org/cargo/reference/workspaces.html) concept of the same name, a workspace is "a collection of one or more packages, called _workspace members_, that are managed together." Workspaces organize large codebases by splitting them into multiple packages with common dependencies. Think: a FastAPI-based web application, alongside a series of libraries that are versioned and maintained as separate Python packages, all in the same Git repository. In a workspace, each package defines its own `pyproject.toml`, but the workspace shares a single lockfile, ensuring that the workspace operates with a consistent set of dependencies. As such, `uv lock` operates on the entire workspace at once, while `uv run` and `uv sync` operate on the workspace root by default, though both accept a `--package` argument, allowing you to run a command in a particular workspace member from any workspace directory. ## Getting started To create a workspace, add a `tool.uv.workspace` table to a `pyproject.toml`, which will implicitly create a workspace rooted at that package. !!! tip By default, running `uv init` inside an existing package will add the newly created member to the workspace, creating a `tool.uv.workspace` table in the workspace root if it doesn't already exist. In defining a workspace, you must specify the `members` (required) and `exclude` (optional) keys, which direct the workspace to include or exclude specific directories as members respectively, and accept lists of globs: ```toml title="pyproject.toml" [project] name = "albatross" version = "0.1.0" requires-python = ">=3.12" dependencies = ["bird-feeder", "tqdm>=4,<5"] [tool.uv.sources] bird-feeder = { workspace = true } [tool.uv.workspace] members = ["packages/*"] exclude = ["packages/seeds"] ``` Every directory included by the `members` globs (and not excluded by the `exclude` globs) must contain a `pyproject.toml` file. However, workspace members can be _either_ [applications](./init.md#applications) or [libraries](./init.md#libraries); both are supported in the workspace context. Every workspace needs a root, which is _also_ a workspace member. In the above example, `albatross` is the workspace root, and the workspace members include all projects under the `packages` directory, except `seeds`. By default, `uv run` and `uv sync` operates on the workspace root. For example, in the above example, `uv run` and `uv run --package albatross` would be equivalent, while `uv run --package bird-feeder` would run the command in the `bird-feeder` package. ## Workspace sources Within a workspace, dependencies on workspace members are facilitated via [`tool.uv.sources`](./dependencies.md), as in: ```toml title="pyproject.toml" [project] name = "albatross" version = "0.1.0" requires-python = ">=3.12" dependencies = ["bird-feeder", "tqdm>=4,<5"] [tool.uv.sources] bird-feeder = { workspace = true } [tool.uv.workspace] members = ["packages/*"] [build-system] requires = ["uv_build>=0.9.17,<0.10.0"] build-backend = "uv_build" ``` In this example, the `albatross` project depends on the `bird-feeder` project, which is a member of the workspace. The `workspace = true` key-value pair in the `tool.uv.sources` table indicates the `bird-feeder` dependency should be provided by the workspace, rather than fetched from PyPI or another registry. !!! note Dependencies between workspace members are editable. Any `tool.uv.sources` definitions in the workspace root apply to all members, unless overridden in the `tool.uv.sources` of a specific member. For example, given the following `pyproject.toml`: ```toml title="pyproject.toml" [project] name = "albatross" version = "0.1.0" requires-python = ">=3.12" dependencies = ["bird-feeder", "tqdm>=4,<5"] [tool.uv.sources] bird-feeder = { workspace = true } tqdm = { git = "https://github.com/tqdm/tqdm" } [tool.uv.workspace] members = ["packages/*"] [build-system] requires = ["uv_build>=0.9.17,<0.10.0"] build-backend = "uv_build" ``` Every workspace member would, by default, install `tqdm` from GitHub, unless a specific member overrides the `tqdm` entry in its own `tool.uv.sources` table. !!! note If a workspace member provides `tool.uv.sources` for some dependency, it will ignore any `tool.uv.sources` for the same dependency in the workspace root, even if the member's source is limited by a [marker](dependencies.md#platform-specific-sources) that doesn't match the current platform. ## Workspace layouts The most common workspace layout can be thought of as a root project with a series of accompanying libraries. For example, continuing with the above example, this workspace has an explicit root at `albatross`, with two libraries (`bird-feeder` and `seeds`) in the `packages` directory: ```text albatross ├── packages │ ├── bird-feeder │ │ ├── pyproject.toml │ │ └── src │ │ └── bird_feeder │ │ ├── __init__.py │ │ └── foo.py │ └── seeds │ ├── pyproject.toml │ └── src │ └── seeds │ ├── __init__.py │ └── bar.py ├── pyproject.toml ├── README.md ├── uv.lock └── src └── albatross └── main.py ``` Since `seeds` was excluded in the `pyproject.toml`, the workspace has two members total: `albatross` (the root) and `bird-feeder`. ## When (not) to use workspaces Workspaces are intended to facilitate the development of multiple interconnected packages within a single repository. As a codebase grows in complexity, it can be helpful to split it into smaller, composable packages, each with their own dependencies and version constraints. Workspaces help enforce isolation and separation of concerns. For example, in uv, we have separate packages for the core library and the command-line interface, enabling us to test the core library independently of the CLI, and vice versa. Other common use cases for workspaces include: - A library with a performance-critical subroutine implemented in an extension module (Rust, C++, etc.). - A library with a plugin system, where each plugin is a separate workspace package with a dependency on the root. Workspaces are _not_ suited for cases in which members have conflicting requirements, or desire a separate virtual environment for each member. In this case, path dependencies are often preferable. For example, rather than grouping `albatross` and its members in a workspace, you can always define each package as its own independent project, with inter-package dependencies defined as path dependencies in `tool.uv.sources`: ```toml title="pyproject.toml" [project] name = "albatross" version = "0.1.0" requires-python = ">=3.12" dependencies = ["bird-feeder", "tqdm>=4,<5"] [tool.uv.sources] bird-feeder = { path = "packages/bird-feeder" } [build-system] requires = ["uv_build>=0.9.17,<0.10.0"] build-backend = "uv_build" ``` This approach conveys many of the same benefits, but allows for more fine-grained control over dependency resolution and virtual environment management (with the downside that `uv run --package` is no longer available; instead, commands must be run from the relevant package directory). Finally, uv's workspaces enforce a single `requires-python` for the entire workspace, taking the intersection of all members' `requires-python` values. If you need to support testing a given member on a Python version that isn't supported by the rest of the workspace, you may need to use `uv pip` to install that member in a separate virtual environment. !!! note As Python does not provide dependency isolation, uv can't ensure that a package uses its declared dependencies and nothing else. For workspaces specifically, uv can't ensure that packages don't import dependencies declared by another workspace member. uv-0.9.17+ds1/docs/concepts/python-versions.md000066400000000000000000000456021520155276700212250ustar00rootroot00000000000000# Python versions A Python version is composed of a Python interpreter (i.e. the `python` executable), the standard library, and other supporting files. ## Managed and system Python installations Since it is common for a system to have an existing Python installation, uv supports [discovering](#discovery-of-python-versions) Python versions. However, uv also supports [installing Python versions](#installing-a-python-version) itself. To distinguish between these two types of Python installations, uv refers to Python versions it installs as _managed_ Python installations and all other Python installations as _system_ Python installations. !!! note uv does not distinguish between Python versions installed by the operating system vs those installed and managed by other tools. For example, if a Python installation is managed with `pyenv`, it would still be considered a _system_ Python version in uv. ## Requesting a version A specific Python version can be requested with the `--python` flag in most uv commands. For example, when creating a virtual environment: ```console $ uv venv --python 3.11.6 ``` uv will ensure that Python 3.11.6 is available — downloading and installing it if necessary — then create the virtual environment with it. The following Python version request formats are supported: - `` (e.g., `3`, `3.12`, `3.12.3`) - `` (e.g., `>=3.12,<3.13`) - `` (e.g., `3.13t`, `3.12.0d`) - `+` (e.g., `3.13+freethreaded`, `3.12.0+debug`, `3.14+gil`) - `` (e.g., `cpython` or `cp`) - `@` (e.g., `cpython@3.12`) - `` (e.g., `cpython3.12` or `cp312`) - `` (e.g., `cpython>=3.12,<3.13`) - `----` (e.g., `cpython-3.12.3-macos-aarch64-none`) Additionally, a specific system Python interpreter can be requested with: - `` (e.g., `/opt/homebrew/bin/python3`) - `` (e.g., `mypython3`) - `` (e.g., `/some/environment/`) By default, uv will automatically download Python versions if they cannot be found on the system. This behavior can be [disabled with the `python-downloads` option](#disabling-automatic-python-downloads). ### Python version files The `.python-version` file can be used to create a default Python version request. uv searches for a `.python-version` file in the working directory and each of its parents. If none is found, uv will check the user-level configuration directory. Any of the request formats described above can be used, though use of a version number is recommended for interoperability with other tools. A `.python-version` file can be created in the current directory with the [`uv python pin`](../reference/cli.md/#uv-python-pin) command. A global `.python-version` file can be created in the user configuration directory with the [`uv python pin --global`](../reference/cli.md/#uv-python-pin) command. Discovery of `.python-version` files can be disabled with `--no-config`. uv will not search for `.python-version` files beyond project or workspace boundaries (except the user configuration directory). ## Installing a Python version uv bundles a list of downloadable CPython and PyPy distributions for macOS, Linux, and Windows. !!! tip By default, Python versions are automatically downloaded as needed without using `uv python install`. To install a Python version at a specific version: ```console $ uv python install 3.12.3 ``` To install the latest patch version: ```console $ uv python install 3.12 ``` To install a version that satisfies constraints: ```console $ uv python install '>=3.8,<3.10' ``` To install multiple versions: ```console $ uv python install 3.9 3.10 3.11 ``` To install a specific implementation: ```console $ uv python install pypy ``` All the [Python version request](#requesting-a-version) formats are supported except those that are used for requesting local interpreters such as a file path. By default `uv python install` will verify that a managed Python version is installed or install the latest version. If a `.python-version` file is present, uv will install the Python version listed in the file. A project that requires multiple Python versions may define a `.python-versions` file. If present, uv will install all the Python versions listed in the file. !!! important The available Python versions are frozen for each uv release. To install new Python versions, you may need upgrade uv. See the [storage documentation](../reference/storage.md#python-versions) for details about where installed Python versions are stored. ### Installing Python executables uv installs Python executables into your `PATH` by default, e.g., on Unix `uv python install 3.12` will install a Python executable into `~/.local/bin`, e.g., as `python3.12`. See the [storage documentation](../reference/storage.md#python-executables) for more details about the target directory. !!! tip If `~/.local/bin` is not in your `PATH`, you can add it with `uv tool update-shell`. To install `python` and `python3` executables, include the experimental `--default` option: ```console $ uv python install 3.12 --default ``` When installing Python executables, uv will only overwrite an existing executable if it is managed by uv — e.g., if `~/.local/bin/python3.12` exists already uv will not overwrite it without the `--force` flag. uv will update executables that it manages. However, it will prefer the latest patch version of each Python minor version by default. For example: ```console $ uv python install 3.12.7 # Adds `python3.12` to `~/.local/bin` $ uv python install 3.12.6 # Does not update `python3.12` $ uv python install 3.12.8 # Updates `python3.12` to point to 3.12.8 ``` ## Upgrading Python versions !!! important Support for upgrading Python versions is in _preview_. This means the behavior is experimental and subject to change. Upgrades are only supported for uv-managed Python versions. Upgrades are not currently supported for PyPy and GraalPy. uv allows transparently upgrading Python versions to the latest patch release, e.g., 3.13.4 to 3.13.5. uv does not allow transparently upgrading across minor Python versions, e.g., 3.12 to 3.13, because changing minor versions can affect dependency resolution. uv-managed Python versions can be upgraded to the latest supported patch release with the `python upgrade` command: To upgrade a Python version to the latest supported patch release: ```console $ uv python upgrade 3.12 ``` To upgrade all installed Python versions: ```console $ uv python upgrade ``` After an upgrade, uv will prefer the new version, but will retain the existing version as it may still be used by virtual environments. If the Python version was installed with the `python-upgrade` [preview feature](./preview.md) enabled, e.g., `uv python install 3.12 --preview-features python-upgrade`, virtual environments using the Python version will be automatically upgraded to the new patch version. !!! note If the virtual environment was created _before_ opting in to the preview mode, it will not be included in the automatic upgrades. If a virtual environment was created with an explicitly requested patch version, e.g., `uv venv -p 3.10.8`, it will not be transparently upgraded to a new version. ### Minor version directories Automatic upgrades for virtual environments are implemented using a directory with the Python minor version, e.g.: ``` ~/.local/share/uv/python/cpython-3.12-macos-aarch64-none ``` which is a symbolic link (on Unix) or junction (on Windows) pointing to a specific patch version: ```console $ readlink ~/.local/share/uv/python/cpython-3.12-macos-aarch64-none ~/.local/share/uv/python/cpython-3.12.11-macos-aarch64-none ``` If this link is resolved by another tool, e.g., by canonicalizing the Python interpreter path, and used to create a virtual environment, it will not be automatically upgraded. ## Project Python versions uv will respect Python requirements defined in `requires-python` in the `pyproject.toml` file during project command invocations. The first Python version that is compatible with the requirement will be used, unless a version is otherwise requested, e.g., via a `.python-version` file or the `--python` flag. ## Viewing available Python versions To list installed and available Python versions: ```console $ uv python list ``` To filter the Python versions, provide a request, e.g., to show all Python 3.13 interpreters: ```console $ uv python list 3.13 ``` Or, to show all PyPy interpreters: ```console $ uv python list pypy ``` By default, downloads for other platforms and old patch versions are hidden. To view all versions: ```console $ uv python list --all-versions ``` To view Python versions for other platforms: ```console $ uv python list --all-platforms ``` To exclude downloads and only show installed Python versions: ```console $ uv python list --only-installed ``` See the [`uv python list`](../reference/cli.md#uv-python-list) reference for more details. ## Finding a Python executable To find a Python executable, use the `uv python find` command: ```console $ uv python find ``` By default, this will display the path to the first available Python executable. See the [discovery rules](#discovery-of-python-versions) for details about how executables are discovered. This interface also supports many [request formats](#requesting-a-version), e.g., to find a Python executable that has a version of 3.11 or newer: ```console $ uv python find '>=3.11' ``` By default, `uv python find` will include Python versions from virtual environments. If a `.venv` directory is found in the working directory or any of the parent directories or the `VIRTUAL_ENV` environment variable is set, it will take precedence over any Python executables on the `PATH`. To ignore virtual environments, use the `--system` flag: ```console $ uv python find --system ``` ## Discovery of Python versions When searching for a Python version, the following locations are checked: - Managed Python installations in the `UV_PYTHON_INSTALL_DIR`. - A Python interpreter on the `PATH` as `python`, `python3`, or `python3.x` on macOS and Linux, or `python.exe` on Windows. - On Windows, the Python interpreters in the Windows registry and Microsoft Store Python interpreters (see `py --list-paths`) that match the requested version. In some cases, uv allows using a Python version from a virtual environment. In this case, the virtual environment's interpreter will be checked for compatibility with the request before searching for an installation as described above. See the [pip-compatible virtual environment discovery](../pip/environments.md#discovery-of-python-environments) documentation for details. When performing discovery, non-executable files will be ignored. Each discovered executable is queried for metadata to ensure it meets the [requested Python version](#requesting-a-version). If the query fails, the executable will be skipped. If the executable satisfies the request, it is used without inspecting additional executables. When searching for a managed Python version, uv will prefer newer versions first. When searching for a system Python version, uv will use the first compatible version — not the newest version. If a Python version cannot be found on the system, uv will check for a compatible managed Python version download. ## Python pre-releases Python pre-releases will not be selected by default. Python pre-releases will be used if there is no other available installation matching the request. For example, if only a pre-release version is available it will be used but otherwise a stable release version will be used. Similarly, if the path to a pre-release Python executable is provided then no other Python version matches the request and the pre-release version will be used. If a pre-release Python version is available and matches the request, uv will not download a stable Python version instead. ## Free-threaded Python uv supports discovering and installing [free-threaded](https://docs.python.org/3.14/glossary.html#term-free-threading) Python variants in CPython 3.13+. For Python 3.13, free-threaded Python versions will not be selected by default. Free-threaded Python versions will only be selected when explicitly requested, e.g., with `3.13t` or `3.13+freethreaded`. For Python 3.14+, uv will allow use of free-threaded Python 3.14+ interpreters without explicit selection. The GIL-enabled build of Python will still be preferred, e.g., when performing an installation with `uv python install 3.14`. However, e.g., if a free-threaded interpreter comes before a GIL-enabled build on the `PATH`, it will be used. If both free-threaded and GIL-enabled Python versions are available on the system, and want to require the use of the GIL-enabled variant in a project, you can use the `+gil` variant specifier. ## Debug Python variants uv supports discovering and installing [debug builds](https://docs.python.org/3.14/using/configure.html#debug-build) of Python, i.e., with debug assertions enabled. !!! important Debug builds of Python are slower and are not appropriate for general use. Debug builds will be used if there is no other available installation matching the request. For example, if only a debug version is available it will be used but otherwise a stable release version will be used. Similarly, if the path to a debug Python executable is provided then no other Python version matches the request and the debug version will be used. Debug builds of Python can be explicitly requested with, e.g., `3.13d` or `3.13+debug`. !!! note CPython versions installed by uv usually have debug symbols stripped to reduce the distribution size. These debug builds do not have debug symbols stripped, which can be useful when debugging Python processes with a C-level debugger. ## Disabling automatic Python downloads By default, uv will automatically download Python versions when needed. The [`python-downloads`](../reference/settings.md#python-downloads) option can be used to disable this behavior. By default, it is set to `automatic`; set to `manual` to only allow Python downloads during `uv python install`. !!! tip The `python-downloads` setting can be set in a [persistent configuration file](./configuration-files.md) to change the default behavior, or the `--no-python-downloads` flag can be passed to any uv command. ## Requiring or disabling managed Python versions By default, uv will attempt to use Python versions found on the system and only download managed Python versions when necessary. To ignore system Python versions, and only use managed Python versions, use the `--managed-python` flag: ```console $ uv python list --managed-python ``` Similarly, to ignore managed Python versions and only use system Python versions, use the `--no-managed-python` flag: ```console $ uv python list --no-managed-python ``` To change uv's default behavior in a configuration file, use the [`python-preference` setting](#adjusting-python-version-preferences). ## Adjusting Python version preferences The [`python-preference`](../reference/settings.md#python-preference) setting determines whether to prefer using Python installations that are already present on the system, or those that are downloaded and installed by uv. By default, the `python-preference` is set to `managed` which prefers managed Python installations over system Python installations. However, system Python installations are still preferred over downloading a managed Python version. The following alternative options are available: - `only-managed`: Only use managed Python installations; never use system Python installations. Equivalent to `--managed-python`. - `system`: Prefer system Python installations over managed Python installations. - `only-system`: Only use system Python installations; never use managed Python installations. Equivalent to `--no-managed-python`. !!! note Automatic Python version downloads can be [disabled](#disabling-automatic-python-downloads) without changing the preference. ## Python implementation support uv supports the CPython, PyPy, Pyodide, and GraalPy Python implementations. If a Python implementation is not supported, uv will fail to discover its interpreter. The implementations may be requested with either the long or short name: - CPython: `cpython`, `cp` - PyPy: `pypy`, `pp` - GraalPy: `graalpy`, `gp` - Pyodide: `pyodide` Implementation name requests are not case-sensitive. See the [Python version request](#requesting-a-version) documentation for more details on the supported formats. ## Managed Python distributions uv supports downloading and installing CPython, PyPy, and Pyodide distributions. ### CPython distributions As Python does not publish official distributable CPython binaries, uv instead uses pre-built distributions from the Astral [`python-build-standalone`](https://github.com/astral-sh/python-build-standalone) project. `python-build-standalone` is also is used in many other Python projects, like [Mise](https://mise.jdx.dev/lang/python.html) and [bazelbuild/rules_python](https://github.com/bazelbuild/rules_python). The uv Python distributions are self-contained, highly-portable, and performant. While Python can be built from source, as in tools like `pyenv`, doing so requires preinstalled system dependencies, and creating optimized, performant builds (e.g., with PGO and LTO enabled) is very slow. These distributions have some behavior quirks, generally as a consequence of portability; see the [`python-build-standalone` quirks](https://gregoryszorc.com/docs/python-build-standalone/main/quirks.html) documentation for details. ### PyPy distributions PyPy distributions are provided by the [PyPy project](https://pypy.org). ### Pyodide distributions Pyodide distributions are provided by the [Pyodide project](https://github.com/pyodide/pyodide). Pyodide is a port of CPython for the WebAssembly / Emscripten platform. ## Transparent x86_64 emulation on aarch64 Both macOS and Windows support running x86_64 binaries on aarch64 through transparent emulation. This is called [Rosetta 2](https://support.apple.com/en-gb/102527) or [Windows on ARM (WoA) emulation](https://learn.microsoft.com/en-us/windows/arm/apps-on-arm-x86-emulation). It's possible to use x86_64 uv on aarch64, and also possible to use an x86_64 Python interpreter on aarch64. Either uv binary can use either Python interpreter, but a Python interpreter needs packages for its architecture, either all x86_64 or all aarch64. ## Registration in the Windows registry On Windows, installation of managed Python versions will register them with the Windows registry as defined by [PEP 514](https://peps.python.org/pep-0514/). After installation, the Python versions can be selected with the `py` launcher, e.g.: ```console $ uv python install 3.13.1 $ py -V:Astral/CPython3.13.1 ``` On uninstall, uv will remove the registry entry for the target version as well as any broken registry entries. uv-0.9.17+ds1/docs/concepts/resolution.md000066400000000000000000001024341520155276700202360ustar00rootroot00000000000000# Resolution Resolution is the process of taking a list of requirements and converting them to a list of package versions that fulfill the requirements. Resolution requires recursively searching for compatible versions of packages, ensuring that the requested requirements are fulfilled and that the requirements of the requested packages are compatible. ## Dependencies Most projects and packages have dependencies. Dependencies are other packages that are necessary in order for the current package to work. A package defines its dependencies as _requirements_, roughly a combination of a package name and acceptable versions. The dependencies defined by the current project are called _direct dependencies_. The dependencies added by each dependency of the current project are called _indirect_ or _transitive dependencies_. !!! note See the [dependency specifiers page](https://packaging.python.org/en/latest/specifications/dependency-specifiers/) in the Python Packaging documentation for details about dependencies. ## Basic examples To help demonstrate the resolution process, consider the following dependencies: - The project depends on `foo` and `bar`. - `foo` has one version, 1.0.0: - `foo 1.0.0` depends on `lib>=1.0.0`. - `bar` has one version, 1.0.0: - `bar 1.0.0` depends on `lib>=2.0.0`. - `lib` has two versions, 1.0.0 and 2.0.0. Both versions have no dependencies. In this example, the resolver must find a set of package versions which satisfies the project requirements. Since there is only one version of both `foo` and `bar`, those will be used. The resolution must also include the transitive dependencies, so a version of `lib` must be chosen. `foo 1.0.0` allows all available versions of `lib`, but `bar 1.0.0` requires `lib>=2.0.0` so `lib 2.0.0` must be used. In some resolutions, there may be more than one valid solution. Consider the following dependencies: - The project depends on `foo` and `bar`. - `foo` has two versions, 1.0.0 and 2.0.0: - `foo 1.0.0` has no dependencies. - `foo 2.0.0` depends on `lib==2.0.0`. - `bar` has two versions, 1.0.0 and 2.0.0: - `bar 1.0.0` has no dependencies. - `bar 2.0.0` depends on `lib==1.0.0` - `lib` has two versions, 1.0.0 and 2.0.0. Both versions have no dependencies. In this example, some version of both `foo` and `bar` must be selected; however, determining which version requires considering the dependencies of each version of `foo` and `bar`. `foo 2.0.0` and `bar 2.0.0` cannot be installed together as they conflict on their required version of `lib`, so the resolver must select either `foo 1.0.0` (along with `bar 2.0.0`) or `bar 1.0.0` (along with `foo 1.0.0`). Both are valid solutions, and different resolution algorithms may yield either result. ## Platform markers Markers allow attaching an expression to requirements that indicate when the dependency should be used. For example `bar ; python_version < "3.9"` indicates that `bar` should only be installed on Python 3.8 and earlier. Markers are used to adjust a package's dependencies based on the current environment or platform. For example, markers can be used to modify dependencies by operating system, CPU architecture, Python version, Python implementation, and more. !!! note See the [environment markers](https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers) section in the Python Packaging documentation for more details about markers. Markers are important for resolution because their values change the required dependencies. Typically, Python package resolvers use the markers of the _current_ platform to determine which dependencies to use since the package is often being _installed_ on the current platform. However, for _locking_ dependencies this is problematic — the lockfile would only work for developers using the same platform the lockfile was created on. To solve this problem, platform-independent, or "universal" resolvers exist. uv supports both [platform-specific](#platform-specific-resolution) and [universal](#universal-resolution) resolution. ## Platform-specific resolution By default, uv's pip interface, i.e., [`uv pip compile`](../pip/compile.md), produces a resolution that is platform-specific, like `pip-tools`. There is no way to use platform-specific resolution in the uv's project interface. uv also supports resolving for specific, alternate platforms and Python versions with the `--python-platform` and `--python-version` options. For example, if using Python 3.12 on macOS, `uv pip compile --python-platform linux --python-version 3.10 requirements.in` can be used to produce a resolution for Python 3.10 on Linux instead. Unlike universal resolution, during platform-specific resolution, the provided `--python-version` is the exact python version to use, not a lower bound. !!! note Python's environment markers expose far more information about the current machine than can be expressed by a simple `--python-platform` argument. For example, the `platform_version` marker on macOS includes the time at which the kernel was built, which can (in theory) be encoded in package requirements. uv's resolver makes a best-effort attempt to generate a resolution that is compatible with any machine running on the target `--python-platform`, which should be sufficient for most use cases, but may lose fidelity for complex package and platform combinations. ## Universal resolution uv's lockfile (`uv.lock`) is created with a universal resolution and is portable across platforms. This ensures that dependencies are locked for everyone working on the project, regardless of operating system, architecture, and Python version. The uv lockfile is created and modified by [project](../concepts/projects/index.md) commands such as `uv lock`, `uv sync`, and `uv add`. Universal resolution is also available in uv's pip interface, i.e., [`uv pip compile`](../pip/compile.md), with the `--universal` flag. The resulting requirements file will contain markers to indicate which platform each dependency is relevant for. During universal resolution, a package may be listed multiple times with different versions or URLs if different versions are needed for different platforms — the markers determine which version will be used. A universal resolution is often more constrained than a platform-specific resolution, since we need to take the requirements for all markers into account. During universal resolution, all required packages must be compatible with the _entire_ range of `requires-python` declared in the `pyproject.toml`. For example, if a project's `requires-python` is `>=3.8`, resolution will fail if all versions of given dependency require Python 3.9 or later, since the dependency lacks a usable version for (e.g.) Python 3.8, the lower bound of the project's supported range. In other words, the project's `requires-python` must be a subset of the `requires-python` of all its dependencies. When selecting the compatible version for a given dependency, uv will ([by default](#multi-version-resolution)) attempt to choose the latest compatible version for each supported Python version. For example, if a project's `requires-python` is `>=3.8`, and the latest version of a dependency requires Python 3.9 or later, while all prior versions supporting Python 3.8, the resolver will select the latest version for users running Python 3.9 or later, and previous versions for users running Python 3.8. When evaluating `requires-python` ranges for dependencies, uv only considers lower bounds and ignores upper bounds entirely. For example, `>=3.8, <4` is treated as `>=3.8`. Respecting upper bounds on `requires-python` often leads to formally correct but practically incorrect resolutions, as, e.g., resolvers will backtrack to the first published version that omits the upper bound (see: [`Requires-Python` upper limits](https://discuss.python.org/t/requires-python-upper-limits/12663)). ## Limited resolution environments By default, the universal resolver attempts to solve for all platforms and Python versions. If your project supports only a limited set of platforms or Python versions, you can constrain the set of solved platforms via the `environments` setting, which accepts a list of [PEP 508 environment markers](https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers). In other words, you can use the `environments` setting to _reduce_ the set of supported platforms. For example, to constrain the lockfile to macOS and Linux, and avoid solving for Windows: ```toml title="pyproject.toml" [tool.uv] environments = [ "sys_platform == 'darwin'", "sys_platform == 'linux'", ] ``` Or, to avoid solving for alternative Python implementations: ```toml title="pyproject.toml" [tool.uv] environments = [ "implementation_name == 'cpython'" ] ``` Entries in the `environments` setting must be disjoint (i.e., they must not overlap). For example, `sys_platform == 'darwin'` and `sys_platform == 'linux'` are disjoint, but `sys_platform == 'darwin'` and `python_version >= '3.9'` are not, since both could be true at the same time. ## Required environments In the Python ecosystem, packages can be published as source distributions, built distributions (wheels), or both; but to install a package, a built distribution is required. If a package lacks a built distribution, or lacks a distribution for the current platform or Python version (built distributions are often platform-specific), uv will attempt to build the package from source, then install the resulting built distribution. Some packages (like PyTorch) publish built distributions, but omit a source distribution. Such packages are _only_ installable on platforms for which a built distribution is available. For example, if a package publishes built distributions for Linux, but not macOS or Windows, then that package will _only_ be installable on Linux. Packages that lack source distributions cause problems for universal resolution, since there will typically be at least one platform or Python version for which the package is not installable. By default, uv requires each such package to include at least one wheel that is compatible with the target Python version. The `required-environments` setting can be used to ensure that the resulting resolution contains wheels for specific platforms, or fails if no such wheels are available. The setting accepts a list of [PEP 508 environment markers](https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers). While the `environments` setting _limits_ the set of environments that uv will consider when resolving dependencies, `required-environments` _expands_ the set of platforms that uv _must_ support when resolving dependencies. For example, `environments = ["sys_platform == 'darwin'"]` would limit uv to solving for macOS (and ignoring Linux and Windows). On the other hand, `required-environments = ["sys_platform == 'darwin'"]` would _require_ that any package without a source distribution include a wheel for macOS in order to be installable (and would fail if no such wheel is available). In practice, `required-environments` can be useful for declaring explicit support for non-latest platforms, since this often requires backtracking past the latest published versions of those packages. For example, to guarantee that any built distribution-only packages includes support for Intel macOS: ```toml title="pyproject.toml" [tool.uv] required-environments = [ "sys_platform == 'darwin' and platform_machine == 'x86_64'" ] ``` ## Dependency preferences If resolution output file exists, i.e., a uv lockfile (`uv.lock`) or a requirements output file (`requirements.txt`), uv will _prefer_ the dependency versions listed there. Similarly, if installing a package into a virtual environment, uv will prefer the already installed version if present. This means that locked or installed versions will not change unless an incompatible version is requested or an upgrade is explicitly requested with `--upgrade`. ## Resolution strategy By default, uv tries to use the latest version of each package. For example, `uv pip install flask>=2.0.0` will install the latest version of Flask, e.g., 3.0.0. If `flask>=2.0.0` is a dependency of the project, only `flask` 3.0.0 will be used. This is important, for example, because running tests will not check that the project is actually compatible with its stated lower bound of `flask` 2.0.0. With `--resolution lowest`, uv will install the lowest possible version for all dependencies, both direct and indirect (transitive). Alternatively, `--resolution lowest-direct` will use the lowest compatible versions for all direct dependencies, while using the latest compatible versions for all other dependencies. uv will always use the latest versions for build dependencies. For example, given the following `requirements.in` file: ```python title="requirements.in" flask>=2.0.0 ``` Running `uv pip compile requirements.in` would produce the following `requirements.txt` file: ```python title="requirements.txt" # This file was autogenerated by uv via the following command: # uv pip compile requirements.in blinker==1.7.0 # via flask click==8.1.7 # via flask flask==3.0.0 itsdangerous==2.1.2 # via flask jinja2==3.1.2 # via flask markupsafe==2.1.3 # via # jinja2 # werkzeug werkzeug==3.0.1 # via flask ``` However, `uv pip compile --resolution lowest requirements.in` would instead produce: ```python title="requirements.in" # This file was autogenerated by uv via the following command: # uv pip compile requirements.in --resolution lowest click==7.1.2 # via flask flask==2.0.0 itsdangerous==2.0.0 # via flask jinja2==3.0.0 # via flask markupsafe==2.0.0 # via jinja2 werkzeug==2.0.0 # via flask ``` When publishing libraries, it is recommended to separately run tests with `--resolution lowest` or `--resolution lowest-direct` in continuous integration to ensure compatibility with the declared lower bounds. ## Pre-release handling By default, uv will accept pre-release versions during dependency resolution in two cases: 1. If the package is a direct dependency, and its version specifiers include a pre-release specifier (e.g., `flask>=2.0.0rc1`). 1. If _all_ published versions of a package are pre-releases. If dependency resolution fails due to a transitive pre-release, uv will prompt use of `--prerelease allow` to allow pre-releases for all dependencies. Alternatively, the transitive dependency can be added as a [constraint](#dependency-constraints) or direct dependency (i.e. in `requirements.in` or `pyproject.toml`) with a pre-release version specifier (e.g., `flask>=2.0.0rc1`) to opt in to pre-release support for that specific dependency. Pre-releases are [notoriously difficult](https://pubgrub-rs-guide.netlify.app/limitations/prerelease_versions) to model, and are a frequent source of bugs in other packaging tools. uv's pre-release handling is _intentionally_ limited and requires user opt-in for pre-releases to ensure correctness. For more details, see [Pre-release compatibility](../pip/compatibility.md#pre-release-compatibility). ## Multi-version resolution During universal resolution, a package may be listed multiple times with different versions or URLs within the same lockfile, since different versions may be needed for different platforms or Python versions. The `--fork-strategy` setting can be used to control how uv trades off between (1) minimizing the number of selected versions and (2) selecting the latest-possible version for each platform. The former leads to greater consistency across platforms, while the latter leads to use of newer package versions where possible. By default (`--fork-strategy requires-python`), uv will optimize for selecting the latest version of each package for each supported Python version, while minimizing the number of selected versions across platforms. For example, when resolving `numpy` with a Python requirement of `>=3.8`, uv would select the following versions: ```txt numpy==1.24.4 ; python_version == "3.8" numpy==2.0.2 ; python_version == "3.9" numpy==2.2.0 ; python_version >= "3.10" ``` This resolution reflects the fact that NumPy 2.2.0 and later require at least Python 3.10, while earlier versions are compatible with Python 3.8 and 3.9. Under `--fork-strategy fewest`, uv will instead minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms. For example, when in the scenario above, uv would select `numpy==1.24.4` for all Python versions, rather than upgrading to `numpy==2.0.2` for Python 3.9 and `numpy==2.2.0` for Python 3.10 and later. ## Dependency constraints Like pip, uv supports constraint files (`--constraint constraints.txt`) which narrow the set of acceptable versions for the given packages. Constraint files are similar to requirements files, but being listed as a constraint alone will not cause a package to be included to the resolution. Instead, constraints only take effect if a requested package is already pulled in as a direct or transitive dependency. Constraints are useful for reducing the range of available versions for a transitive dependency. They can also be used to keep a resolution in sync with some other set of resolved versions, regardless of which packages are overlapping between the two. ## Dependency overrides Dependency overrides allow bypassing unsuccessful or undesirable resolutions by overriding a package's declared dependencies. Overrides are a useful last resort for cases in which you _know_ that a dependency is compatible with a certain version of a package, despite the metadata indicating otherwise. For example, if a transitive dependency declares the requirement `pydantic>=1.0,<2.0`, but _does_ work with `pydantic>=2.0`, the user can override the declared dependency by including `pydantic>=1.0,<3` in the overrides, thereby allowing the resolver to choose a newer version of `pydantic`. Concretely, if `pydantic>=1.0,<3` is included as an override, uv will ignore all declared requirements on `pydantic`, replacing them with the override. In the above example, the `pydantic>=1.0,<2.0` requirement would be ignored completely, and would instead be replaced with `pydantic>=1.0,<3`. While constraints can only _reduce_ the set of acceptable versions for a package, overrides can _expand_ the set of acceptable versions, providing an escape hatch for erroneous upper version bounds. As with constraints, overrides do not add a dependency on the package and only take effect if the package is requested in a direct or transitive dependency. In a `pyproject.toml`, use `tool.uv.override-dependencies` to define a list of overrides. In the pip-compatible interface, the `--override` option can be used to pass files with the same format as constraints files. If multiple overrides are provided for the same package, they must be differentiated with [markers](#platform-markers). If a package has a dependency with a marker, it is replaced unconditionally when using overrides — it does not matter if the marker evaluates to true or false. ## Dependency metadata During resolution, uv needs to resolve the metadata for each package it encounters, in order to determine its dependencies. This metadata is often available as a static file in the package index; however, for packages that only provide source distributions, the metadata may not be available upfront. In such cases, uv has to build the package to determine its metadata (e.g., by invoking `setup.py`). This can introduce a performance penalty during resolution. Further, it imposes the requirement that the package can be built on all platforms, which may not be true. For example, you may have a package that should only be built and installed on Linux, but doesn't build successfully on macOS or Windows. While uv can construct a perfectly valid lockfile for this scenario, doing so would require building the package, which would fail on non-Linux platforms. The `tool.uv.dependency-metadata` table can be used to provide static metadata for such dependencies upfront, thereby allowing uv to skip the build step and use the provided metadata instead. For example, to provide metadata for `chumpy` upfront, include its `dependency-metadata` in the `pyproject.toml`: ```toml [[tool.uv.dependency-metadata]] name = "chumpy" version = "0.70" requires-dist = ["numpy>=1.8.1", "scipy>=0.13.0", "six>=1.11.0"] ``` These declarations are intended for cases in which a package does _not_ declare static metadata upfront, though they are also useful for packages that require [disabling build isolation](./projects/config.md#build-isolation) In such cases, it may be easier to declare the package metadata upfront, rather than creating a custom build environment prior to resolving the package. For example, past versions of `flash-attn` did not declare static metadata. By declaring metadata for `flash-attn` upfront, uv can resolve `flash-attn` without building the package from source (which itself requires installing `torch`): ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.12" dependencies = ["flash-attn"] [tool.uv.sources] flash-attn = { git = "https://github.com/Dao-AILab/flash-attention", tag = "v2.6.3" } [[tool.uv.dependency-metadata]] name = "flash-attn" version = "2.6.3" requires-dist = ["torch", "einops"] ``` Like dependency overrides, `tool.uv.dependency-metadata` can also be used for cases in which a package's metadata is incorrect or incomplete, or when a package is not available in the package index. While dependency overrides allow overriding the allowed versions of a package globally, metadata overrides allow overriding the declared metadata of a _specific package_. !!! note The `version` field in `tool.uv.dependency-metadata` is optional for registry-based dependencies (when omitted, uv will assume the metadata applies to all versions of the package), but _required_ for direct URL dependencies (like Git dependencies). Entries in the `tool.uv.dependency-metadata` table follow the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) specification, though only `name`, `version`, `requires-dist`, `requires-python`, and `provides-extra` are read by uv. The `version` field is also considered optional. If omitted, the metadata will be used for all versions of the specified package. ## Conflicting dependencies uv requires that all dependencies declared by a project are compatible with each other and resolves all dependencies together when creating the lockfile. This includes project dependencies, optional dependencies ("extras"), and dependency groups (development dependencies). If dependencies declared in one extra are not compatible with those in another extra, uv will fail to resolve the requirements of the project with an error. For example, consider two sets of optional dependencies that conflict with one another: ```toml title="pyproject.toml" [project.optional-dependencies] extra1 = ["numpy==2.1.2"] extra2 = ["numpy==2.0.0"] ``` If you run `uv lock` with the above dependencies, resolution will fail: ```console $ uv lock x No solution found when resolving dependencies: `-> Because myproject[extra2] depends on numpy==2.0.0 and myproject[extra1] depends on numpy==2.1.2, we can conclude that myproject[extra1] and myproject[extra2] are incompatible. And because your project requires myproject[extra1] and myproject[extra2], we can conclude that your projects's requirements are unsatisfiable. ``` To work around this, uv supports explicit declaration of conflicts. If you specify that `extra1` and `extra2` are conflicting, uv will resolve them separately. Specify conflicts in the `tool.uv` section: ```toml title="pyproject.toml" [tool.uv] conflicts = [ [ { extra = "extra1" }, { extra = "extra2" }, ], ] ``` Now, running `uv lock` will succeed. However, now you cannot install both `extra1` and `extra2` at the same time: ```console $ uv sync --extra extra1 --extra extra2 Resolved 3 packages in 14ms error: extra `extra1`, extra `extra2` are incompatible with the declared conflicts: {`myproject[extra1]`, `myproject[extra2]`} ``` This error occurs because installing both `extra1` and `extra2` would result in installing two different versions of a package into the same environment. The above strategy for dealing with conflicting optional dependencies also works with dependency groups: ```toml title="pyproject.toml" [dependency-groups] group1 = ["numpy==2.1.2"] group2 = ["numpy==2.0.0"] [tool.uv] conflicts = [ [ { group = "group1" }, { group = "group2" }, ], ] ``` The only difference from conflicting extras is that you need to use the `group` key instead of `extra`. When using a workspace with multiple projects, the same restrictions apply — uv requires all workspace members to be compatible with each other. Similarly, conflicts can be declared across workspace members. For example, consider the following workspace: ```toml title="member1/pyproject.toml" [project] name = "member1" [project.optional-dependencies] extra1 = ["numpy==2.1.2"] ``` ```toml title="member2/pyproject.toml" [project] name = "member2" [project.optional-dependencies] extra2 = ["numpy==2.0.0"] ``` To declare a conflict between extras in these different workspace members, use the `package` key: ```toml title="pyproject.toml" [tool.uv] conflicts = [ [ { package = "member1", extra = "extra1" }, { package = "member2", extra = "extra2" }, ], ] ``` It's also possible for the project dependencies (i.e., `project.dependencies`) of one workspace member to conflict with the extra of another member, for example: ```toml title="member1/pyproject.toml" [project] name = "member1" dependencies = ["numpy==2.1.2"] ``` ```toml title="member2/pyproject.toml" [project] name = "member2" [project.optional-dependencies] extra2 = ["numpy==2.0.0"] ``` This conflict can also be declared using the `package` key: ```toml title="pyproject.toml" [tool.uv] conflicts = [ [ { package = "member1" }, { package = "member2", extra = "extra2" }, ], ] ``` Similarly, it's possible for some workspace members to have conflicting project dependencies: ```toml title="member1/pyproject.toml" [project] name = "member1" dependencies = ["numpy==2.1.2"] ``` ```toml title="member2/pyproject.toml" [project] name = "member2" dependencies = ["numpy==2.0.0"] ``` This conflict can also be declared using the `package` key: ```toml title="pyproject.toml" [tool.uv] conflicts = [ [ { package = "member1" }, { package = "member2" }, ], ] ``` These workspace members will not be installable together, e.g., the workspace root cannot define: ```toml title="pyproject.toml" [project] name = "root" dependencies = ["member1", "member2"] ``` ## Lower bounds By default, `uv add` adds lower bounds to dependencies and, when using uv to manage projects, uv will warn if direct dependencies don't have lower bound. Lower bounds are not critical in the "happy path", but they are important for cases where there are dependency conflicts. For example, consider a project that requires two packages and those packages have conflicting dependencies. The resolver needs to check all combinations of all versions within the constraints for the two packages — if all of them conflict, an error is reported because the dependencies are not satisfiable. If there are no lower bounds, the resolver can (and often will) backtrack down to the oldest version of a package. This isn't only problematic because it's slow, the old version of the package often fails to build, or the resolver can end up picking a version that's old enough that it doesn't depend on the conflicting package, but also doesn't work with your code. Lower bounds are particularly critical when writing a library. It's important to declare the lowest version for each dependency that your library works with, and to validate that the bounds are correct — testing with [`--resolution lowest` or `--resolution lowest-direct`](#resolution-strategy). Otherwise, a user may receive an old, incompatible version of one of your library's dependencies and the library will fail with an unexpected error. ## Reproducible resolutions uv supports an `--exclude-newer` option to limit resolution to distributions published before a specific date, allowing reproduction of installations regardless of new package releases. The date may be specified as an [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) timestamp (e.g., `2006-12-02T02:07:43Z`) or a local date in the same format (e.g., `2006-12-02`) in your system's configured time zone. !!! important The package index must support the `upload-time` field as specified in [`PEP 700`](https://peps.python.org/pep-0700/). If the field is not present for a given distribution, the distribution will be treated as unavailable. PyPI provides `upload-time` for all packages. To ensure reproducibility, messages for unsatisfiable resolutions will not mention that distributions were excluded due to the `--exclude-newer` flag — newer distributions will be treated as if they do not exist. !!! note The `--exclude-newer` option is only applied to packages that are read from a registry (as opposed to, e.g., Git dependencies). Further, when using the `uv pip` interface, uv will not downgrade previously installed packages unless the `--reinstall` flag is provided, in which case uv will perform a new resolution. This option is also supported in the `pyproject.toml`, e.g.: ```pyproject.toml [tool.uv] exclude-newer = "2006-12-02T02:07:43Z" ``` When specified in persistent configuration, local date times are not allowed. Values may also be specified for specific packages, e.g., `--exclude-newer-package setuptools=2006-12-02`, or: ```pyproject.toml [tool.uv] exclude-newer-package = { setuptools = "2006-12-02T02:07:43Z" } ``` Package-specific values will take precedence over global values. ## Dependency cooldowns uv also supports dependency "cooldowns" in which resolution will ignore packages newer than a duration. This is a good way to improve security posture by delaying package updates until the community has had the opportunity to vet new versions of packages. This feature is available via the [`exclude-newer` option](#reproducible-resolutions) and shares the same semantics. Define a dependency cooldown by specifying a duration instead of an absolute value. Either a "friendly" duration (e.g., `24 hours`, `1 week`, `30 days`) or an ISO 8601 duration (e.g., `PT24H`, `P7D`, `P30D`) can be used. !!! note Durations do not respect semantics of the local time zone and are always resolved to a fixed number of seconds assuming that a day is 24 hours (e.g., DST transitions are ignored). Calendar units such as months and years are not allowed since they are inherently inconsistent lengths. When a duration is used for resolution, a timestamp is calculated relative to the current time. When using a `uv.lock` file, the timestamp is included in the lockfile. uv will not update the lockfile when the current time changes, instead, uv will update the timestamp when a new resolution is performed, e.g., when `--upgrade` or `--refresh` is used. This option is also supported in the `pyproject.toml`, e.g.: ```pyproject.toml [tool.uv] exclude-newer = "1 week" ``` Values may also be specified for specific packages, e.g., `--exclude-newer-package "setuptools=30 days"`, or: ```pyproject.toml [tool.uv] exclude-newer = "1 week" exclude-newer-package = { setuptools = "30 days" } ``` ## Source distribution [PEP 625](https://peps.python.org/pep-0625/) specifies that packages must distribute source distributions as gzip tarball (`.tar.gz`) archives. Prior to this specification, other archive formats, which need to be supported for backward compatibility, were also allowed. uv supports reading and extracting archives in the following formats: - gzip tarball (`.tar.gz`, `.tgz`) - bzip2 tarball (`.tar.bz2`, `.tbz`) - xz tarball (`.tar.xz`, `.txz`) - zstd tarball (`.tar.zst`) - lzip tarball (`.tar.lz`) - lzma tarball (`.tar.lzma`) - zip (`.zip`) ## Lockfile versioning The `uv.lock` file uses a versioned schema. The schema version is included in the `version` field of the lockfile. Any given version of uv can read and write lockfiles with the same schema version, but will reject lockfiles with a greater schema version. For example, if your uv version supports schema v1, `uv lock` will error if it encounters an existing lockfile with schema v2. uv versions that support schema v2 _may_ be able to read lockfiles with schema v1 if the schema update was backwards-compatible. However, this is not guaranteed, and uv may exit with an error if it encounters a lockfile with an outdated schema version. The schema version is considered part of the public API, and so is only bumped in minor releases, as a breaking change (see [Versioning](../reference/policies/versioning.md)). As such, all uv patch versions within a given minor uv release are guaranteed to have full lockfile compatibility. In other words, lockfiles may only be rejected across minor releases. The `revision` field of the lockfile is used to track backwards compatible changes to the lockfile. For example, adding a new field to distributions. Changes to the revision will not cause older versions of uv to error. ## Learn more For more details about the internals of the resolver, see the [resolver reference](../reference/internals/resolver.md) documentation. uv-0.9.17+ds1/docs/concepts/tools.md000066400000000000000000000227511520155276700171760ustar00rootroot00000000000000# Tools Tools are Python packages that provide command-line interfaces. !!! note See the [tools guide](../guides/tools.md) for an introduction to working with the tools interface — this document discusses details of tool management. ## The `uv tool` interface uv includes a dedicated interface for interacting with tools. Tools can be invoked without installation using `uv tool run`, in which case their dependencies are installed in a temporary virtual environment isolated from the current project. Because it is very common to run tools without installing them, a `uvx` alias is provided for `uv tool run` — the two commands are exactly equivalent. For brevity, the documentation will mostly refer to `uvx` instead of `uv tool run`. Tools can also be installed with `uv tool install`, in which case their executables are [available on the `PATH`](#tool-executables) — an isolated virtual environment is still used, but it is not removed when the command completes. ## Execution vs installation In most cases, executing a tool with `uvx` is more appropriate than installing the tool. Installing the tool is useful if you need the tool to be available to other programs on your system, e.g., if some script you do not control requires the tool, or if you are in a Docker image and want to make the tool available to users. ## Tool environments When running a tool with `uvx`, a virtual environment is stored in the uv cache directory and is treated as disposable, i.e., if you run `uv cache clean` the environment will be deleted. The environment is only cached to reduce the overhead of repeated invocations. If the environment is removed, a new one will be created automatically. When installing a tool with `uv tool install`, a virtual environment is created in the [uv tools directory](../reference/storage.md#tools). The environment will not be removed unless the tool is uninstalled. If the environment is manually deleted, the tool will fail to run. !!! important Tool environments are _not_ intended to be mutated directly. It is strongly recommended never to mutate a tool environment manually, e.g., with a `pip` operation. ## Tool versions Unless a specific version is requested, `uv tool install` will install the latest available of the requested tool. `uvx` will use the latest available version of the requested tool _on the first invocation_. After that, `uvx` will use the cached version of the tool unless a different version is requested, the cache is pruned, or the cache is refreshed. For example, to run a specific version of Ruff: ```console $ uvx ruff@0.6.0 --version ruff 0.6.0 ``` A subsequent invocation of `uvx` will use the latest, not the cached, version. ```console $ uvx ruff --version ruff 0.6.2 ``` But, if a new version of Ruff was released, it would not be used unless the cache was refreshed. To request the latest version of Ruff and refresh the cache, use the `@latest` suffix: ```console $ uvx ruff@latest --version 0.6.2 ``` Once a tool is installed with `uv tool install`, `uvx` will use the installed version by default. For example, after installing an older version of Ruff: ```console $ uv tool install ruff==0.5.0 ``` The version of `ruff` and `uvx ruff` is the same: ```console $ ruff --version ruff 0.5.0 $ uvx ruff --version ruff 0.5.0 ``` However, you can ignore the installed version by requesting the latest version explicitly, e.g.: ```console $ uvx ruff@latest --version 0.6.2 ``` Or, by using the `--isolated` flag, which will avoid refreshing the cache but ignore the installed version: ```console $ uvx --isolated ruff --version 0.6.2 ``` `uv tool install` will also respect the `{package}@{version}` and `{package}@latest` specifiers, as in: ```console $ uv tool install ruff@latest $ uv tool install ruff@0.6.0 ``` ## Upgrading tools Tool environments may be upgraded via `uv tool upgrade`, or re-created entirely via subsequent `uv tool install` operations. To upgrade all packages in a tool environment ```console $ uv tool upgrade black ``` To upgrade a single package in a tool environment: ```console $ uv tool upgrade black --upgrade-package click ``` Tool upgrades will respect the version constraints provided when installing the tool. For example, `uv tool install black >=23,<24` followed by `uv tool upgrade black` will upgrade Black to the latest version in the range `>=23,<24`. To instead replace the version constraints, reinstall the tool with `uv tool install`: ```console $ uv tool install black>=24 ``` Similarly, tool upgrades will retain the settings provided when installing the tool. For example, `uv tool install black --prerelease allow` followed by `uv tool upgrade black` will retain the `--prerelease allow` setting. !!! note Tool upgrades will reinstall the tool executables, even if they have not changed. To reinstall packages during upgrade, use the `--reinstall` and `--reinstall-package` options. To reinstall all packages in a tool environment ```console $ uv tool upgrade black --reinstall ``` To reinstall a single package in a tool environment: ```console $ uv tool upgrade black --reinstall-package click ``` ## Including additional dependencies Additional packages can be included during tool execution: ```console $ uvx --with ``` And, during tool installation: ```console $ uv tool install --with ``` The `--with` option can be provided multiple times to include additional packages. The `--with` option supports package specifications, so a specific version can be requested: ```console $ uvx --with == ``` The `-w` shorthand can be used in place of the `--with` option: ```console $ uvx -w ``` If the requested version conflicts with the requirements of the tool package, package resolution will fail and the command will error. ## Installing executables from additional packages When installing a tool, you may want to include executables from additional packages in the same tool environment. This is useful when you have related tools that work together or when you want to install multiple executables that share dependencies. The `--with-executables-from` option allows you to specify additional packages whose executables should be installed alongside the main tool: ```console $ uv tool install --with-executables-from , ``` For example, to install Ansible along with executables from `ansible-core` and `ansible-lint`: ```console $ uv tool install --with-executables-from ansible-core,ansible-lint ansible ``` This will install all executables from the `ansible`, `ansible-core`, and `ansible-lint` packages into the same tool environment, making them all available on the `PATH`. The `--with-executables-from` option can be combined with other installation options: ```console $ uv tool install --with-executables-from ansible-core --with mkdocs-material ansible ``` Note that `--with-executables-from` differs from `--with` in that: - `--with` includes additional packages as dependencies but does not install their executables - `--with-executables-from` includes both the packages as dependencies and installs their executables ## Python versions Each tool environment is linked to a specific Python version. This uses the same Python version [discovery logic](./python-versions.md#discovery-of-python-versions) as other virtual environments created by uv, but will ignore non-global Python version requests like `.python-version` files and the `requires-python` value from a `pyproject.toml`. The `--python` option can be used to request a specific version. See the [Python version](./python-versions.md) documentation for more details. If the Python version used by a tool is _uninstalled_, the tool environment will be broken and the tool may be unusable. ## Tool executables Tool executables include all console entry points, script entry points, and binary scripts provided by a Python package. Tool executables are symlinked into the [executable directory](../reference/storage.md#tool-executables) on Unix and copied on Windows. !!! note Executables provided by dependencies of tool packages are not installed. The [executable directory](../reference/storage.md#executable-directory) must be in the `PATH` variable for tool executables to be available from the shell. If it is not in the `PATH`, a warning will be displayed. The `uv tool update-shell` command can be used to add the executable directory to the `PATH` in common shell configuration files. ### Overwriting executables Installation of tools will not overwrite executables in the executable directory that were not previously installed by uv. For example, if `pipx` has been used to install a tool, `uv tool install` will fail. The `--force` flag can be used to override this behavior. ## Relationship to `uv run` The invocation `uv tool run ` (or `uvx `) is nearly equivalent to: ```console $ uv run --no-project --with -- ``` However, there are a couple notable differences when using uv's tool interface: - The `--with` option is not needed — the required package is inferred from the command name. - The temporary environment is cached in a dedicated location. - The `--no-project` flag is not needed — tools are always run isolated from the project. - If a tool is already installed, `uv tool run` will use the installed version but `uv run` will not. If the tool should not be isolated from the project, e.g., when running `pytest` or `mypy`, then `uv run` should be used instead of `uv tool run`. uv-0.9.17+ds1/docs/getting-started/000077500000000000000000000000001520155276700167745ustar00rootroot00000000000000uv-0.9.17+ds1/docs/getting-started/features.md000066400000000000000000000104301520155276700211320ustar00rootroot00000000000000# Features uv provides essential features for Python development — from installing Python and hacking on simple scripts to working on large projects that support multiple Python versions and platforms. uv's interface can be broken down into sections, which are usable independently or together. ## Python versions Installing and managing Python itself. - `uv python install`: Install Python versions. - `uv python list`: View available Python versions. - `uv python find`: Find an installed Python version. - `uv python pin`: Pin the current project to use a specific Python version. - `uv python uninstall`: Uninstall a Python version. See the [guide on installing Python](../guides/install-python.md) to get started. ## Scripts Executing standalone Python scripts, e.g., `example.py`. - `uv run`: Run a script. - `uv add --script`: Add a dependency to a script. - `uv remove --script`: Remove a dependency from a script. See the [guide on running scripts](../guides/scripts.md) to get started. ## Projects Creating and working on Python projects, i.e., with a `pyproject.toml`. - `uv init`: Create a new Python project. - `uv add`: Add a dependency to the project. - `uv remove`: Remove a dependency from the project. - `uv sync`: Sync the project's dependencies with the environment. - `uv lock`: Create a lockfile for the project's dependencies. - `uv run`: Run a command in the project environment. - `uv tree`: View the dependency tree for the project. - `uv build`: Build the project into distribution archives. - `uv publish`: Publish the project to a package index. See the [guide on projects](../guides/projects.md) to get started. ## Tools Running and installing tools published to Python package indexes, e.g., `ruff` or `black`. - `uvx` / `uv tool run`: Run a tool in a temporary environment. - `uv tool install`: Install a tool user-wide. - `uv tool uninstall`: Uninstall a tool. - `uv tool list`: List installed tools. - `uv tool update-shell`: Update the shell to include tool executables. See the [guide on tools](../guides/tools.md) to get started. ## The pip interface Manually managing environments and packages — intended to be used in legacy workflows or cases where the high-level commands do not provide enough control. Creating virtual environments (replacing `venv` and `virtualenv`): - `uv venv`: Create a new virtual environment. See the documentation on [using environments](../pip/environments.md) for details. Managing packages in an environment (replacing [`pip`](https://github.com/pypa/pip) and [`pipdeptree`](https://github.com/tox-dev/pipdeptree)): - `uv pip install`: Install packages into the current environment. - `uv pip show`: Show details about an installed package. - `uv pip freeze`: List installed packages and their versions. - `uv pip check`: Check that the current environment has compatible packages. - `uv pip list`: List installed packages. - `uv pip uninstall`: Uninstall packages. - `uv pip tree`: View the dependency tree for the environment. See the documentation on [managing packages](../pip/packages.md) for details. Locking packages in an environment (replacing [`pip-tools`](https://github.com/jazzband/pip-tools)): - `uv pip compile`: Compile requirements into a lockfile. - `uv pip sync`: Sync an environment with a lockfile. See the documentation on [locking environments](../pip/compile.md) for details. !!! important These commands do not exactly implement the interfaces and behavior of the tools they are based on. The further you stray from common workflows, the more likely you are to encounter differences. Consult the [pip-compatibility guide](../pip/compatibility.md) for details. ## Utility Managing and inspecting uv's state, such as the cache, storage directories, or performing a self-update: - `uv cache clean`: Remove cache entries. - `uv cache prune`: Remove outdated cache entries. - `uv cache dir`: Show the uv cache directory path. - `uv tool dir`: Show the uv tool directory path. - `uv python dir`: Show the uv installed Python versions path. - `uv self update`: Update uv to the latest version. ## Next steps Read the [guides](../guides/index.md) for an introduction to each feature, check out the [concept](../concepts/index.md) pages for in-depth details about uv's features, or learn how to [get help](./help.md) if you run into any problems. uv-0.9.17+ds1/docs/getting-started/first-steps.md000066400000000000000000000010171520155276700216000ustar00rootroot00000000000000# First steps with uv After [installing uv](./installation.md), you can check that uv is available by running the `uv` command: ```console $ uv An extremely fast Python package manager. Usage: uv [OPTIONS] ... ``` You should see a help menu listing the available commands. ## Next steps Now that you've confirmed uv is installed, check out an [overview of features](./features.md), learn how to [get help](./help.md) if you run into any problems, or jump to the [guides](../guides/index.md) to start using uv. uv-0.9.17+ds1/docs/getting-started/help.md000066400000000000000000000040551520155276700202520ustar00rootroot00000000000000# Getting help ## Help menus The `--help` flag can be used to view the help menu for a command, e.g., for `uv`: ```console $ uv --help ``` To view the help menu for a specific command, e.g., for `uv init`: ```console $ uv init --help ``` When using the `--help` flag, uv displays a condensed help menu. To view a longer help menu for a command, use `uv help`: ```console $ uv help ``` To view the long help menu for a specific command, e.g., for `uv init`: ```console $ uv help init ``` When using the long help menu, uv will attempt to use `less` or `more` to "page" the output so it is not all displayed at once. To exit the pager, press `q`. ## Displaying verbose output The `-v` flag can be used to display verbose output for a command, e.g., for `uv sync`: ```console $ uv sync -v ``` The `-v` flag can be repeated to increase verbosity, e.g.: ```console $ uv sync -vv ``` Often, the verbose output will include additional information about why uv is behaving in a certain way. ## Viewing the version When seeking help, it's important to determine the version of uv that you're using — sometimes the problem is already solved in a newer version. To check the installed version: ```console $ uv self version ``` The following are also valid: ```console $ uv --version # Same output as `uv self version` $ uv -V # Will not include the build commit and date ``` !!! note Before uv 0.7.0, `uv version` was used instead of `uv self version`. ## Troubleshooting issues The reference documentation contains a [troubleshooting guide](../reference/troubleshooting/index.md) for common issues. ## Open an issue on GitHub The [issue tracker](https://github.com/astral-sh/uv/issues) on GitHub is a good place to report bugs and request features. Make sure to search for similar issues first, as it is common for someone else to encounter the same problem. ## Chat on Discord Astral has a [Discord server](https://discord.com/invite/astral-sh), which is a great place to ask questions, learn more about uv, and engage with other community members. uv-0.9.17+ds1/docs/getting-started/index.md000066400000000000000000000010441520155276700204240ustar00rootroot00000000000000# Getting started To help you get started with uv, we'll cover a few important topics: - [Installing uv](./installation.md) - [First steps after installation](./first-steps.md) - [An overview of uv's features](./features.md) - [How to get help](./help.md) Read on, or jump ahead to another section: - Get going quickly with [guides](../guides/index.md) for common workflows. - Learn more about the core [concepts](../concepts/index.md) in uv. - Use the [reference](../reference/index.md) documentation to find details about something specific. uv-0.9.17+ds1/docs/getting-started/installation.md000066400000000000000000000154621520155276700220270ustar00rootroot00000000000000# Installing uv ## Installation methods Install uv with our standalone installers or your package manager of choice. ### Standalone installer uv provides a standalone installer to download and install uv: === "macOS and Linux" Use `curl` to download the script and execute it with `sh`: ```console $ curl -LsSf https://astral.sh/uv/install.sh | sh ``` If your system doesn't have `curl`, you can use `wget`: ```console $ wget -qO- https://astral.sh/uv/install.sh | sh ``` Request a specific version by including it in the URL: ```console $ curl -LsSf https://astral.sh/uv/0.9.17/install.sh | sh ``` === "Windows" Use `irm` to download the script and execute it with `iex`: ```pwsh-session PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` Changing the [execution policy](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.4#powershell-execution-policies) allows running a script from the internet. Request a specific version by including it in the URL: ```pwsh-session PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.9.17/install.ps1 | iex" ``` !!! tip The installation script may be inspected before use: === "macOS and Linux" ```console $ curl -LsSf https://astral.sh/uv/install.sh | less ``` === "Windows" ```pwsh-session PS> powershell -c "irm https://astral.sh/uv/install.ps1 | more" ``` Alternatively, the installer or binaries can be downloaded directly from [GitHub](#github-releases). See the reference documentation on the [installer](../reference/installer.md) for details on customizing your uv installation. ### PyPI For convenience, uv is published to [PyPI](https://pypi.org/project/uv/). If installing from PyPI, we recommend installing uv into an isolated environment, e.g., with `pipx`: ```console $ pipx install uv ``` However, `pip` can also be used: ```console $ pip install uv ``` !!! note uv ships with prebuilt distributions (wheels) for many platforms; if a wheel is not available for a given platform, uv will be built from source, which requires a Rust toolchain. See the [contributing setup guide](https://github.com/astral-sh/uv/blob/main/CONTRIBUTING.md#setup) for details on building uv from source. ### Homebrew uv is available in the core Homebrew packages. ```console $ brew install uv ``` ### MacPorts uv is available via [MacPorts](https://ports.macports.org/port/uv/). ```console $ sudo port install uv ``` ### WinGet uv is available via [WinGet](https://winstall.app/apps/astral-sh.uv). ```console $ winget install --id=astral-sh.uv -e ``` ### Scoop uv is available via [Scoop](https://scoop.sh/#/apps?q=uv). ```console $ scoop install main/uv ``` ### Docker uv provides a Docker image at [`ghcr.io/astral-sh/uv`](https://github.com/astral-sh/uv/pkgs/container/uv). See our guide on [using uv in Docker](../guides/integration/docker.md) for more details. ### GitHub Releases uv release artifacts can be downloaded directly from [GitHub Releases](https://github.com/astral-sh/uv/releases). Each release page includes binaries for all supported platforms as well as instructions for using the standalone installer via `github.com` instead of `astral.sh`. ### Cargo uv is available via [crates.io](https://crates.io). ```console $ cargo install --locked uv ``` !!! note This method builds uv from source, which requires a compatible Rust toolchain. ## Upgrading uv When uv is installed via the standalone installer, it can update itself on-demand: ```console $ uv self update ``` !!! tip Updating uv will re-run the installer and can modify your shell profiles. To disable this behavior, set `UV_NO_MODIFY_PATH=1`. When another installation method is used, self-updates are disabled. Use the package manager's upgrade method instead. For example, with `pip`: ```console $ pip install --upgrade uv ``` ## Shell autocompletion !!! tip You can run `echo $SHELL` to help you determine your shell. To enable shell autocompletion for uv commands, run one of the following: === "Bash" ```bash echo 'eval "$(uv generate-shell-completion bash)"' >> ~/.bashrc ``` === "Zsh" ```bash echo 'eval "$(uv generate-shell-completion zsh)"' >> ~/.zshrc ``` === "fish" ```bash echo 'uv generate-shell-completion fish | source' > ~/.config/fish/completions/uv.fish ``` === "Elvish" ```bash echo 'eval (uv generate-shell-completion elvish | slurp)' >> ~/.elvish/rc.elv ``` === "PowerShell / pwsh" ```powershell if (!(Test-Path -Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force } Add-Content -Path $PROFILE -Value '(& uv generate-shell-completion powershell) | Out-String | Invoke-Expression' ``` To enable shell autocompletion for uvx, run one of the following: === "Bash" ```bash echo 'eval "$(uvx --generate-shell-completion bash)"' >> ~/.bashrc ``` === "Zsh" ```bash echo 'eval "$(uvx --generate-shell-completion zsh)"' >> ~/.zshrc ``` === "fish" ```bash echo 'uvx --generate-shell-completion fish | source' > ~/.config/fish/completions/uvx.fish ``` === "Elvish" ```bash echo 'eval (uvx --generate-shell-completion elvish | slurp)' >> ~/.elvish/rc.elv ``` === "PowerShell / pwsh" ```powershell if (!(Test-Path -Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force } Add-Content -Path $PROFILE -Value '(& uvx --generate-shell-completion powershell) | Out-String | Invoke-Expression' ``` Then restart the shell or source the shell config file. ## Uninstallation If you need to remove uv from your system, follow these steps: 1. Clean up stored data (optional): ```console $ uv cache clean $ rm -r "$(uv python dir)" $ rm -r "$(uv tool dir)" ``` !!! tip Before removing the binaries, you may want to remove any data that uv has stored. See the [storage reference](../reference/storage.md) for details on where uv stores data. 2. Remove the uv, uvx, and uvw binaries: === "macOS and Linux" ```console $ rm ~/.local/bin/uv ~/.local/bin/uvx ``` === "Windows" ```pwsh-session PS> rm $HOME\.local\bin\uv.exe PS> rm $HOME\.local\bin\uvx.exe PS> rm $HOME\.local\bin\uvw.exe ``` !!! note Prior to 0.5.0, uv was installed into `~/.cargo/bin`. The binaries can be removed from there to uninstall. Upgrading from an older version will not automatically remove the binaries from `~/.cargo/bin`. ## Next steps See the [first steps](./first-steps.md) or jump straight to the [guides](../guides/index.md) to start using uv. uv-0.9.17+ds1/docs/guides/000077500000000000000000000000001520155276700151475ustar00rootroot00000000000000uv-0.9.17+ds1/docs/guides/index.md000066400000000000000000000010541520155276700166000ustar00rootroot00000000000000# Guides overview Check out one of the core guides to get started: - [Installing Python versions](./install-python.md) - [Running scripts and declaring dependencies](./scripts.md) - [Running and installing applications as tools](./tools.md) - [Creating and working on projects](./projects.md) - [Building and publishing packages](./package.md) - [Integrate uv with other software, e.g., Docker, GitHub, PyTorch, and more](./integration/index.md) Or, explore the [concept documentation](../concepts/index.md) for comprehensive breakdown of each feature. uv-0.9.17+ds1/docs/guides/install-python.md000066400000000000000000000113741520155276700204640ustar00rootroot00000000000000--- title: Installing and managing Python description: A guide to using uv to install Python, including requesting specific versions, automatic installation, viewing installed versions, and more. --- # Installing Python If Python is already installed on your system, uv will [detect and use](#using-existing-python-versions) it without configuration. However, uv can also install and manage Python versions. uv [automatically installs](#automatic-python-downloads) missing Python versions as needed — you don't need to install Python to get started. ## Getting started To install the latest Python version: ```console $ uv python install ``` !!! note Python does not publish official distributable binaries. As such, uv uses distributions from the Astral [`python-build-standalone`](https://github.com/astral-sh/python-build-standalone) project. See the [Python distributions](../concepts/python-versions.md#managed-python-distributions) documentation for more details. Once Python is installed, it will be used by `uv` commands automatically. uv also adds the installed version to your `PATH`: ```console $ python3.13 ``` uv only installs a _versioned_ executable by default. To install `python` and `python3` executables, include the experimental `--default` option: ```console $ uv python install --default ``` !!! tip See the documentation on [installing Python executables](../concepts/python-versions.md#installing-python-executables) for more details. ## Installing a specific version To install a specific Python version: ```console $ uv python install 3.12 ``` To install multiple Python versions: ```console $ uv python install 3.11 3.12 ``` To install an alternative Python implementation, e.g., PyPy: ```console $ uv python install pypy@3.10 ``` See the [`python install`](../concepts/python-versions.md#installing-a-python-version) documentation for more details. ## Reinstalling Python To reinstall uv-managed Python versions, use `--reinstall`, e.g.: ```console $ uv python install --reinstall ``` This will reinstall all previously installed Python versions. Improvements are constantly being added to the Python distributions, so reinstalling may resolve bugs even if the Python version does not change. ## Viewing Python installations To view available and installed Python versions: ```console $ uv python list ``` See the [`python list`](../concepts/python-versions.md#viewing-available-python-versions) documentation for more details. ## Automatic Python downloads Python does not need to be explicitly installed to use uv. By default, uv will automatically download Python versions when they are required. For example, the following would download Python 3.12 if it was not installed: ```console $ uvx python@3.12 -c "print('hello world')" ``` Even if a specific Python version is not requested, uv will download the latest version on demand. For example, if there are no Python versions on your system, the following will install Python before creating a new virtual environment: ```console $ uv venv ``` !!! tip Automatic Python downloads can be [easily disabled](../concepts/python-versions.md#disabling-automatic-python-downloads) if you want more control over when Python is downloaded. ## Using existing Python versions uv will use existing Python installations if present on your system. There is no configuration necessary for this behavior: uv will use the system Python if it satisfies the requirements of the command invocation. See the [Python discovery](../concepts/python-versions.md#discovery-of-python-versions) documentation for details. To force uv to use the system Python, provide the `--no-managed-python` flag. See the [Python version preference](../concepts/python-versions.md#requiring-or-disabling-managed-python-versions) documentation for more details. ## Upgrading Python versions !!! important Support for upgrading Python patch versions is in _preview_. This means the behavior is experimental and subject to change. To upgrade a Python version to the latest supported patch release: ```console $ uv python upgrade 3.12 ``` To upgrade all uv-managed Python versions: ```console $ uv python upgrade ``` See the [`python upgrade`](../concepts/python-versions.md#upgrading-python-versions) documentation for more details. ## Next steps To learn more about `uv python`, see the [Python version concept](../concepts/python-versions.md) page and the [command reference](../reference/cli.md#uv-python). Or, read on to learn how to [run scripts](./scripts.md) and invoke Python with uv. uv-0.9.17+ds1/docs/guides/integration/000077500000000000000000000000001520155276700174725ustar00rootroot00000000000000uv-0.9.17+ds1/docs/guides/integration/alternative-indexes.md000066400000000000000000000354321520155276700237760ustar00rootroot00000000000000--- title: Using alternative package indexes description: A guide to using alternative package indexes with uv, including Azure Artifacts, Google Artifact Registry, AWS CodeArtifact, and more. --- # Using alternative package indexes While uv uses the official Python Package Index (PyPI) by default, it also supports [alternative package indexes](../../concepts/indexes.md). Most alternative indexes require various forms of authentication, which require some initial setup. !!! important If using the pip interface, please read the documentation on [using multiple indexes](../../pip/compatibility.md#packages-that-exist-on-multiple-indexes) in uv — the default behavior is different from pip to prevent dependency confusion attacks, but this means that uv may not find the versions of a package as you'd expect. ## Azure Artifacts uv can install packages from [Azure Artifacts](https://learn.microsoft.com/en-us/azure/devops/artifacts/start-using-azure-artifacts?view=azure-devops&tabs=nuget%2Cnugetserver), either by using a [Personal Access Token](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows) (PAT), or using the [`keyring`](https://github.com/jaraco/keyring) package. To use Azure Artifacts, add the index to your project: ```toml title="pyproject.toml" [[tool.uv.index]] name = "private-registry" url = "https://pkgs.dev.azure.com///_packaging//pypi/simple/" ``` ### Authenticate with an Azure access token If there is a personal access token (PAT) available (e.g., [`$(System.AccessToken)` in an Azure pipeline](https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#systemaccesstoken)), credentials can be provided via "Basic" HTTP authentication scheme. Include the PAT in the password field of the URL. A username must be included as well, but can be any string. For example, with the token stored in the `$AZURE_ARTIFACTS_TOKEN` environment variable, set credentials for the index with: ```bash export UV_INDEX_PRIVATE_REGISTRY_USERNAME=dummy export UV_INDEX_PRIVATE_REGISTRY_PASSWORD="$AZURE_ARTIFACTS_TOKEN" ``` !!! note `PRIVATE_REGISTRY` should match the name of the index defined in your `pyproject.toml`. ### Authenticate with `keyring` and `artifacts-keyring` You can also authenticate to Artifacts using [`keyring`](https://github.com/jaraco/keyring) package with the [`artifacts-keyring` plugin](https://github.com/Microsoft/artifacts-keyring). Because these two packages are required to authenticate to Azure Artifacts, they must be pre-installed from a source other than Artifacts. The `artifacts-keyring` plugin wraps the [Azure Artifacts Credential Provider tool](https://github.com/microsoft/artifacts-credprovider). The credential provider supports a few different authentication modes including interactive login — see the [tool's documentation](https://github.com/microsoft/artifacts-credprovider) for information on configuration. uv only supports using the `keyring` package in [subprocess mode](../../reference/settings.md#keyring-provider). The `keyring` executable must be in the `PATH`, i.e., installed globally or in the active environment. The `keyring` CLI requires a username in the URL, and it must be `VssSessionToken`. ```bash # Pre-install keyring and the Artifacts plugin from the public PyPI uv tool install keyring --with artifacts-keyring # Enable keyring authentication export UV_KEYRING_PROVIDER=subprocess # Set the username for the index export UV_INDEX_PRIVATE_REGISTRY_USERNAME=VssSessionToken ``` !!! note The [`tool.uv.keyring-provider`](../../reference/settings.md#keyring-provider) setting can be used to enable keyring in your `uv.toml` or `pyproject.toml`. Similarly, the username for the index can be added directly to the index URL. ### Publishing packages to Azure Artifacts If you also want to publish your own packages to Azure Artifacts, you can use `uv publish` as described in the [Building and publishing guide](../package.md). First, add a `publish-url` to the index you want to publish packages to. For example: ```toml title="pyproject.toml" hl_lines="4" [[tool.uv.index]] name = "private-registry" url = "https://pkgs.dev.azure.com///_packaging//pypi/simple/" publish-url = "https://pkgs.dev.azure.com///_packaging//pypi/upload/" ``` Then, configure credentials (if not using keyring): ```console $ export UV_PUBLISH_USERNAME=dummy $ export UV_PUBLISH_PASSWORD="$AZURE_ARTIFACTS_TOKEN" ``` And publish the package: ```console $ uv publish --index private-registry ``` To use `uv publish` without adding the `publish-url` to the project, you can set `UV_PUBLISH_URL`: ```console $ export UV_PUBLISH_URL=https://pkgs.dev.azure.com///_packaging//pypi/upload/ $ uv publish ``` Note this method is not preferable because uv cannot check if the package is already published before uploading artifacts. ## Google Artifact Registry uv can install packages from [Google Artifact Registry](https://cloud.google.com/artifact-registry/docs), either by using an access token, or using the [`keyring`](https://github.com/jaraco/keyring) package. !!! note This guide assumes that [`gcloud`](https://cloud.google.com/sdk/gcloud) CLI is installed and authenticated. To use Google Artifact Registry, add the index to your project: ```toml title="pyproject.toml" [[tool.uv.index]] name = "private-registry" url = "https://-python.pkg.dev///simple/" ``` ### Authenticate with a Google access token Credentials can be provided via "Basic" HTTP authentication scheme. Include access token in the password field of the URL. Username must be `oauth2accesstoken`, otherwise authentication will fail. Generate a token with `gcloud`: ```bash export ARTIFACT_REGISTRY_TOKEN=$( gcloud auth application-default print-access-token ) ``` !!! note You might need to pass extra parameters to properly generate the token (like `--project`), this is a basic example. Then set credentials for the index with: ```bash export UV_INDEX_PRIVATE_REGISTRY_USERNAME=oauth2accesstoken export UV_INDEX_PRIVATE_REGISTRY_PASSWORD="$ARTIFACT_REGISTRY_TOKEN" ``` !!! note `PRIVATE_REGISTRY` should match the name of the index defined in your `pyproject.toml`. ### Authenticate with `keyring` and `keyrings.google-artifactregistry-auth` You can also authenticate to Artifact Registry using [`keyring`](https://github.com/jaraco/keyring) package with the [`keyrings.google-artifactregistry-auth` plugin](https://github.com/GoogleCloudPlatform/artifact-registry-python-tools). Because these two packages are required to authenticate to Artifact Registry, they must be pre-installed from a source other than Artifact Registry. The `keyrings.google-artifactregistry-auth` plugin wraps [gcloud CLI](https://cloud.google.com/sdk/gcloud) to generate short-lived access tokens, securely store them in system keyring, and refresh them when they are expired. uv only supports using the `keyring` package in [subprocess mode](../../reference/settings.md#keyring-provider). The `keyring` executable must be in the `PATH`, i.e., installed globally or in the active environment. The `keyring` CLI requires a username in the URL and it must be `oauth2accesstoken`. ```bash # Pre-install keyring and Artifact Registry plugin from the public PyPI uv tool install keyring --with keyrings.google-artifactregistry-auth # Enable keyring authentication export UV_KEYRING_PROVIDER=subprocess # Set the username for the index export UV_INDEX_PRIVATE_REGISTRY_USERNAME=oauth2accesstoken ``` !!! note The [`tool.uv.keyring-provider`](../../reference/settings.md#keyring-provider) setting can be used to enable keyring in your `uv.toml` or `pyproject.toml`. Similarly, the username for the index can be added directly to the index URL. ### Publishing packages to Google Artifact Registry If you also want to publish your own packages to Google Artifact Registry, you can use `uv publish` as described in the [Building and publishing guide](../package.md). First, add a `publish-url` to the index you want to publish packages to. For example: ```toml title="pyproject.toml" hl_lines="4" [[tool.uv.index]] name = "private-registry" url = "https://-python.pkg.dev///simple/" publish-url = "https://-python.pkg.dev///" ``` Then, configure credentials (if not using keyring): ```console $ export UV_PUBLISH_USERNAME=oauth2accesstoken $ export UV_PUBLISH_PASSWORD="$ARTIFACT_REGISTRY_TOKEN" ``` And publish the package: ```console $ uv publish --index private-registry ``` To use `uv publish` without adding the `publish-url` to the project, you can set `UV_PUBLISH_URL`: ```console $ export UV_PUBLISH_URL=https://-python.pkg.dev/// $ uv publish ``` Note this method is not preferable because uv cannot check if the package is already published before uploading artifacts. ## AWS CodeArtifact uv can install packages from [AWS CodeArtifact](https://docs.aws.amazon.com/codeartifact/latest/ug/using-python.html), either by using an access token, or using the [`keyring`](https://github.com/jaraco/keyring) package. !!! note This guide assumes that [`awscli`](https://aws.amazon.com/cli/) is installed and authenticated. The index can be declared like so: ```toml title="pyproject.toml" [[tool.uv.index]] name = "private-registry" url = "https://-.d.codeartifact..amazonaws.com/pypi//simple/" ``` ### Authenticate with an AWS access token Credentials can be provided via "Basic" HTTP authentication scheme. Include access token in the password field of the URL. Username must be `aws`, otherwise authentication will fail. Generate a token with `awscli`: ```bash export AWS_CODEARTIFACT_TOKEN="$( aws codeartifact get-authorization-token \ --domain \ --domain-owner \ --query authorizationToken \ --output text )" ``` !!! note You might need to pass extra parameters to properly generate the token (like `--region`), this is a basic example. Then set credentials for the index with: ```bash export UV_INDEX_PRIVATE_REGISTRY_USERNAME=aws export UV_INDEX_PRIVATE_REGISTRY_PASSWORD="$AWS_CODEARTIFACT_TOKEN" ``` !!! note `PRIVATE_REGISTRY` should match the name of the index defined in your `pyproject.toml`. ### Authenticate with `keyring` and `keyrings.codeartifact` You can also authenticate to Artifact Registry using [`keyring`](https://github.com/jaraco/keyring) package with the [`keyrings.codeartifact` plugin](https://github.com/jmkeyes/keyrings.codeartifact). Because these two packages are required to authenticate to Artifact Registry, they must be pre-installed from a source other than Artifact Registry. The `keyrings.codeartifact` plugin wraps [boto3](https://pypi.org/project/boto3/) to generate short-lived access tokens, securely store them in system keyring, and refresh them when they are expired. uv only supports using the `keyring` package in [subprocess mode](../../reference/settings.md#keyring-provider). The `keyring` executable must be in the `PATH`, i.e., installed globally or in the active environment. The `keyring` CLI requires a username in the URL and it must be `aws`. ```bash # Pre-install keyring and AWS CodeArtifact plugin from the public PyPI uv tool install keyring --with keyrings.codeartifact # Enable keyring authentication export UV_KEYRING_PROVIDER=subprocess # Set the username for the index export UV_INDEX_PRIVATE_REGISTRY_USERNAME=aws ``` !!! note The [`tool.uv.keyring-provider`](../../reference/settings.md#keyring-provider) setting can be used to enable keyring in your `uv.toml` or `pyproject.toml`. Similarly, the username for the index can be added directly to the index URL. ### Publishing packages to AWS CodeArtifact If you also want to publish your own packages to AWS CodeArtifact, you can use `uv publish` as described in the [Building and publishing guide](../package.md). First, add a `publish-url` to the index you want to publish packages to. For example: ```toml title="pyproject.toml" hl_lines="4" [[tool.uv.index]] name = "private-registry" url = "https://-.d.codeartifact..amazonaws.com/pypi//simple/" publish-url = "https://-.d.codeartifact..amazonaws.com/pypi//" ``` Then, configure credentials (if not using keyring): ```console $ export UV_PUBLISH_USERNAME=aws $ export UV_PUBLISH_PASSWORD="$AWS_CODEARTIFACT_TOKEN" ``` And publish the package: ```console $ uv publish --index private-registry ``` To use `uv publish` without adding the `publish-url` to the project, you can set `UV_PUBLISH_URL`: ```console $ export UV_PUBLISH_URL=https://-.d.codeartifact..amazonaws.com/pypi// $ uv publish ``` Note this method is not preferable because uv cannot check if the package is already published before uploading artifacts. ## JFrog Artifactory uv can install packages from JFrog Artifactory, either by using a username and password or a JWT token. To use it, add the index to your project: ```toml title="pyproject.toml" [[tool.uv.index]] name = "private-registry" url = "https://.jfrog.io/artifactory/api/pypi//simple" ``` ### Authenticate with username and password ```console $ export UV_INDEX_PRIVATE_REGISTRY_USERNAME="" $ export UV_INDEX_PRIVATE_REGISTRY_PASSWORD="" ``` ### Authenticate with JWT token ```console $ export UV_INDEX_PRIVATE_REGISTRY_USERNAME="" $ export UV_INDEX_PRIVATE_REGISTRY_PASSWORD="$JFROG_JWT_TOKEN" ``` !!! note Replace `PRIVATE_REGISTRY` in the environment variable names with the actual index name defined in your `pyproject.toml`. ### Publishing packages to JFrog Artifactory Add a `publish-url` to your index definition: ```toml title="pyproject.toml" [[tool.uv.index]] name = "private-registry" url = "https://.jfrog.io/artifactory/api/pypi//simple" publish-url = "https://.jfrog.io/artifactory/api/pypi/" ``` !!! important If you use `--token "$JFROG_TOKEN"` or `UV_PUBLISH_TOKEN` with JFrog, you will receive a 401 Unauthorized error as JFrog requires an empty username but uv passes `__token__` for as the username when `--token` is used. To authenticate, pass your token as the password and set the username to an empty string: ```console $ uv publish --index -u "" -p "$JFROG_TOKEN" ``` Alternatively, you can set environment variables: ```console $ export UV_PUBLISH_USERNAME="" $ export UV_PUBLISH_PASSWORD="$JFROG_TOKEN" $ uv publish --index private-registry ``` !!! note The publish environment variables (`UV_PUBLISH_USERNAME` and `UV_PUBLISH_PASSWORD`) do not include the index name. uv-0.9.17+ds1/docs/guides/integration/aws-lambda.md000066400000000000000000000464361520155276700220410ustar00rootroot00000000000000--- title: Using uv with AWS Lambda description: A complete guide to using uv with AWS Lambda to manage Python dependencies and deploy serverless functions via Docker containers or zip archives. --- # Using uv with AWS Lambda [AWS Lambda](https://aws.amazon.com/lambda/) is a serverless computing service that lets you run code without provisioning or managing servers. You can use uv with AWS Lambda to manage your Python dependencies, build your deployment package, and deploy your Lambda functions. !!! tip Check out the [`uv-aws-lambda-example`](https://github.com/astral-sh/uv-aws-lambda-example) project for an example of best practices when using uv to deploy an application to AWS Lambda. ## Getting started To start, assume we have a minimal FastAPI application with the following structure: ```plaintext project ├── pyproject.toml └── app ├── __init__.py └── main.py ``` Where the `pyproject.toml` contains: ```toml title="pyproject.toml" [project] name = "uv-aws-lambda-example" version = "0.1.0" requires-python = ">=3.13" dependencies = [ # FastAPI is a modern web framework for building APIs with Python. "fastapi", # Mangum is a library that adapts ASGI applications to AWS Lambda and API Gateway. "mangum", ] [dependency-groups] dev = [ # In development mode, include the FastAPI development server. "fastapi[standard]>=0.115", ] ``` And the `main.py` file contains: ```python title="app/main.py" import logging from fastapi import FastAPI from mangum import Mangum logger = logging.getLogger() logger.setLevel(logging.INFO) app = FastAPI() handler = Mangum(app) @app.get("/") async def root() -> str: return "Hello, world!" ``` We can run this application locally with: ```console $ uv run fastapi dev ``` From there, opening http://127.0.0.1:8000/ in a web browser will display "Hello, world!" ## Deploying a Docker image To deploy to AWS Lambda, we need to build a container image that includes the application code and dependencies in a single output directory. We'll follow the principles outlined in the [Docker guide](./docker.md) (in particular, a multi-stage build) to ensure that the final image is as small and cache-friendly as possible. In the first stage, we'll populate a single directory with all application code and dependencies. In the second stage, we'll copy this directory over to the final image, omitting the build tools and other unnecessary files. ```dockerfile title="Dockerfile" FROM ghcr.io/astral-sh/uv:0.9.17 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder # Enable bytecode compilation, to improve cold-start performance. ENV UV_COMPILE_BYTECODE=1 # Disable installer metadata, to create a deterministic layer. ENV UV_NO_INSTALLER_METADATA=1 # Enable copy mode to support bind mount caching. ENV UV_LINK_MODE=copy # Bundle the dependencies into the Lambda task root via `uv pip install --target`. # # Omit any local packages (`--no-emit-workspace`) and development dependencies (`--no-dev`). # This ensures that the Docker layer cache is only invalidated when the `pyproject.toml` or `uv.lock` # files change, but remains robust to changes in the application code. RUN --mount=from=uv,source=/uv,target=/bin/uv \ --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv export --frozen --no-emit-workspace --no-dev --no-editable -o requirements.txt && \ uv pip install -r requirements.txt --target "${LAMBDA_TASK_ROOT}" FROM public.ecr.aws/lambda/python:3.13 # Copy the runtime dependencies from the builder stage. COPY --from=builder ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT} # Copy the application code. COPY ./app ${LAMBDA_TASK_ROOT}/app # Set the AWS Lambda handler. CMD ["app.main.handler"] ``` !!! tip To deploy to ARM-based AWS Lambda runtimes, replace `public.ecr.aws/lambda/python:3.13` with `public.ecr.aws/lambda/python:3.13-arm64`. We can build the image with, e.g.: ```console $ uv lock $ docker build -t fastapi-app . ``` The core benefits of this Dockerfile structure are as follows: 1. **Minimal image size.** By using a multi-stage build, we can ensure that the final image only includes the application code and dependencies. For example, the uv binary itself is not included in the final image. 2. **Maximal cache reuse.** By installing application dependencies separately from the application code, we can ensure that the Docker layer cache is only invalidated when the dependencies change. Concretely, rebuilding the image after modifying the application source code can reuse the cached layers, resulting in millisecond builds: ```console => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 1.31kB 0.0s => [internal] load metadata for public.ecr.aws/lambda/python:3.13 0.3s => [internal] load metadata for ghcr.io/astral-sh/uv:latest 0.3s => [internal] load .dockerignore 0.0s => => transferring context: 106B 0.0s => [uv 1/1] FROM ghcr.io/astral-sh/uv:latest@sha256:ea61e006cfec0e8d81fae901ad703e09d2c6cf1aa58abcb6507d124b50286f 0.0s => [builder 1/2] FROM public.ecr.aws/lambda/python:3.13@sha256:f5b51b377b80bd303fe8055084e2763336ea8920d12955b23ef 0.0s => [internal] load build context 0.0s => => transferring context: 185B 0.0s => CACHED [builder 2/2] RUN --mount=from=uv,source=/uv,target=/bin/uv --mount=type=cache,target=/root/.cache/u 0.0s => CACHED [stage-2 2/3] COPY --from=builder /var/task /var/task 0.0s => CACHED [stage-2 3/3] COPY ./app /var/task 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:6f8f9ef715a7cda466b677a9df4046ebbb90c8e88595242ade3b4771f547652d 0.0 ``` After building, we can push the image to [Elastic Container Registry (ECR)](https://aws.amazon.com/ecr/) with, e.g.: ```console $ aws ecr get-login-password --region region | docker login --username AWS --password-stdin aws_account_id.dkr.ecr.region.amazonaws.com $ docker tag fastapi-app:latest aws_account_id.dkr.ecr.region.amazonaws.com/fastapi-app:latest $ docker push aws_account_id.dkr.ecr.region.amazonaws.com/fastapi-app:latest ``` Finally, we can deploy the image to AWS Lambda using the AWS Management Console or the AWS CLI, e.g.: ```console $ aws lambda create-function \ --function-name myFunction \ --package-type Image \ --code ImageUri=aws_account_id.dkr.ecr.region.amazonaws.com/fastapi-app:latest \ --role arn:aws:iam::111122223333:role/my-lambda-role ``` Where the [execution role](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html#permissions-executionrole-api) is created via: ```console $ aws iam create-role \ --role-name my-lambda-role \ --assume-role-policy-document '{"Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}' ``` Or, update an existing function with: ```console $ aws lambda update-function-code \ --function-name myFunction \ --image-uri aws_account_id.dkr.ecr.region.amazonaws.com/fastapi-app:latest \ --publish ``` To test the Lambda, we can invoke it via the AWS Management Console or the AWS CLI, e.g.: ```console $ aws lambda invoke \ --function-name myFunction \ --payload file://event.json \ --cli-binary-format raw-in-base64-out \ response.json { "StatusCode": 200, "ExecutedVersion": "$LATEST" } ``` Where `event.json` contains the event payload to pass to the Lambda function: ```json title="event.json" { "httpMethod": "GET", "path": "/", "requestContext": {}, "version": "1.0" } ``` And `response.json` contains the response from the Lambda function: ```json title="response.json" { "statusCode": 200, "headers": { "content-length": "14", "content-type": "application/json" }, "multiValueHeaders": {}, "body": "\"Hello, world!\"", "isBase64Encoded": false } ``` For details, see the [AWS Lambda documentation](https://docs.aws.amazon.com/lambda/latest/dg/python-image.html). ### Workspace support If a project includes local dependencies (e.g., via [Workspaces](../../concepts/projects/workspaces.md)), those too must be included in the deployment package. We'll start by extending the above example to include a dependency on a locally-developed library named `library`. First, we'll create the library itself: ```console $ uv init --lib library $ uv add ./library ``` Running `uv init` within the `project` directory will automatically convert `project` to a workspace and add `library` as a workspace member: ```toml title="pyproject.toml" [project] name = "uv-aws-lambda-example" version = "0.1.0" requires-python = ">=3.13" dependencies = [ # FastAPI is a modern web framework for building APIs with Python. "fastapi", # A local library. "library", # Mangum is a library that adapts ASGI applications to AWS Lambda and API Gateway. "mangum", ] [dependency-groups] dev = [ # In development mode, include the FastAPI development server. "fastapi[standard]", ] [tool.uv.workspace] members = ["library"] [tool.uv.sources] lib = { workspace = true } ``` By default, `uv init --lib` will create a package that exports a `hello` function. We'll modify the application source code to call that function: ```python title="app/main.py" import logging from fastapi import FastAPI from mangum import Mangum from library import hello logger = logging.getLogger() logger.setLevel(logging.INFO) app = FastAPI() handler = Mangum(app) @app.get("/") async def root() -> str: return hello() ``` We can run the modified application locally with: ```console $ uv run fastapi dev ``` And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hello from library!" (instead of "Hello, World!") Finally, we'll update the Dockerfile to include the local library in the deployment package: ```dockerfile title="Dockerfile" FROM ghcr.io/astral-sh/uv:0.9.17 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder # Enable bytecode compilation, to improve cold-start performance. ENV UV_COMPILE_BYTECODE=1 # Disable installer metadata, to create a deterministic layer. ENV UV_NO_INSTALLER_METADATA=1 # Enable copy mode to support bind mount caching. ENV UV_LINK_MODE=copy # Bundle the dependencies into the Lambda task root via `uv pip install --target`. # # Omit any local packages (`--no-emit-workspace`) and development dependencies (`--no-dev`). # This ensures that the Docker layer cache is only invalidated when the `pyproject.toml` or `uv.lock` # files change, but remains robust to changes in the application code. RUN --mount=from=uv,source=/uv,target=/bin/uv \ --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv export --frozen --no-emit-workspace --no-dev --no-editable -o requirements.txt && \ uv pip install -r requirements.txt --target "${LAMBDA_TASK_ROOT}" # If you have a workspace, copy it over and install it too. # # By omitting `--no-emit-workspace`, `library` will be copied into the task root. Using a separate # `RUN` command ensures that all third-party dependencies are cached separately and remain # robust to changes in the workspace. RUN --mount=from=uv,source=/uv,target=/bin/uv \ --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ --mount=type=bind,source=library,target=library \ uv export --frozen --no-dev --no-editable -o requirements.txt && \ uv pip install -r requirements.txt --target "${LAMBDA_TASK_ROOT}" FROM public.ecr.aws/lambda/python:3.13 # Copy the runtime dependencies from the builder stage. COPY --from=builder ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT} # Copy the application code. COPY ./app ${LAMBDA_TASK_ROOT}/app # Set the AWS Lambda handler. CMD ["app.main.handler"] ``` !!! tip To deploy to ARM-based AWS Lambda runtimes, replace `public.ecr.aws/lambda/python:3.13` with `public.ecr.aws/lambda/python:3.13-arm64`. From there, we can build and deploy the updated image as before. ## Deploying a zip archive AWS Lambda also supports deployment via zip archives. For simple applications, zip archives can be a more straightforward and efficient deployment method than Docker images; however, zip archives are limited to [250 MB](https://docs.aws.amazon.com/lambda/latest/dg/python-package.html#python-package-create-update) in size. Returning to the FastAPI example, we can bundle the application dependencies into a local directory for AWS Lambda via: ```console $ uv export --frozen --no-dev --no-editable -o requirements.txt $ uv pip install \ --no-installer-metadata \ --no-compile-bytecode \ --python-platform x86_64-manylinux2014 \ --python 3.13 \ --target packages \ -r requirements.txt ``` !!! tip To deploy to ARM-based AWS Lambda runtimes, replace `x86_64-manylinux2014` with `aarch64-manylinux2014`. Following the [AWS Lambda documentation](https://docs.aws.amazon.com/lambda/latest/dg/python-package.html), we can then bundle these dependencies into a zip as follows: ```console $ cd packages $ zip -r ../package.zip . $ cd .. ``` Finally, we can add the application code to the zip archive: ```console $ zip -r package.zip app ``` We can then deploy the zip archive to AWS Lambda via the AWS Management Console or the AWS CLI, e.g.: ```console $ aws lambda create-function \ --function-name myFunction \ --runtime python3.13 \ --zip-file fileb://package.zip \ --handler app.main.handler \ --role arn:aws:iam::111122223333:role/service-role/my-lambda-role ``` Where the [execution role](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html#permissions-executionrole-api) is created via: ```console $ aws iam create-role \ --role-name my-lambda-role \ --assume-role-policy-document '{"Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}' ``` Or, update an existing function with: ```console $ aws lambda update-function-code \ --function-name myFunction \ --zip-file fileb://package.zip ``` !!! note By default, the AWS Management Console assumes a Lambda entrypoint of `lambda_function.lambda_handler`. If your application uses a different entrypoint, you'll need to modify it in the AWS Management Console. For example, the above FastAPI application uses `app.main.handler`. To test the Lambda, we can invoke it via the AWS Management Console or the AWS CLI, e.g.: ```console $ aws lambda invoke \ --function-name myFunction \ --payload file://event.json \ --cli-binary-format raw-in-base64-out \ response.json { "StatusCode": 200, "ExecutedVersion": "$LATEST" } ``` Where `event.json` contains the event payload to pass to the Lambda function: ```json title="event.json" { "httpMethod": "GET", "path": "/", "requestContext": {}, "version": "1.0" } ``` And `response.json` contains the response from the Lambda function: ```json title="response.json" { "statusCode": 200, "headers": { "content-length": "14", "content-type": "application/json" }, "multiValueHeaders": {}, "body": "\"Hello, world!\"", "isBase64Encoded": false } ``` ### Using a Lambda layer AWS Lambda also supports the deployment of multiple composed [Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/python-layers.html) when working with zip archives. These layers are conceptually similar to layers in a Docker image, allowing you to separate application code from dependencies. In particular, we can create a lambda layer for application dependencies and attach it to the Lambda function, separate from the application code itself. This setup can improve cold-start performance for application updates, as the dependencies layer can be reused across deployments. To create a Lambda layer, we'll follow similar steps, but create two separate zip archives: one for the application code and one for the application dependencies. First, we'll create the dependency layer. Lambda layers are expected to follow a slightly different structure, so we'll use `--prefix` rather than `--target`: ```console $ uv export --frozen --no-dev --no-editable -o requirements.txt $ uv pip install \ --no-installer-metadata \ --no-compile-bytecode \ --python-platform x86_64-manylinux2014 \ --python 3.13 \ --prefix packages \ -r requirements.txt ``` We'll then zip the dependencies in adherence with the expected layout for Lambda layers: ```console $ mkdir python $ cp -r packages/lib python/ $ zip -r layer_content.zip python ``` !!! tip To generate deterministic zip archives, consider passing the `-X` flag to `zip` to exclude extended attributes and file system metadata. And publish the Lambda layer: ```console $ aws lambda publish-layer-version --layer-name dependencies-layer \ --zip-file fileb://layer_content.zip \ --compatible-runtimes python3.13 \ --compatible-architectures "x86_64" ``` We can then create the Lambda function as in the previous example, omitting the dependencies: ```console $ # Zip the application code. $ zip -r app.zip app $ # Create the Lambda function. $ aws lambda create-function \ --function-name myFunction \ --runtime python3.13 \ --zip-file fileb://app.zip \ --handler app.main.handler \ --role arn:aws:iam::111122223333:role/service-role/my-lambda-role ``` Finally, we can attach the dependencies layer to the Lambda function, using the ARN returned by the `publish-layer-version` step: ```console $ aws lambda update-function-configuration --function-name myFunction \ --cli-binary-format raw-in-base64-out \ --layers "arn:aws:lambda:region:111122223333:layer:dependencies-layer:1" ``` When the application dependencies change, the layer can be updated independently of the application by republishing the layer and updating the Lambda function configuration: ```console $ # Update the dependencies in the layer. $ aws lambda publish-layer-version --layer-name dependencies-layer \ --zip-file fileb://layer_content.zip \ --compatible-runtimes python3.13 \ --compatible-architectures "x86_64" $ # Update the Lambda function configuration. $ aws lambda update-function-configuration --function-name myFunction \ --cli-binary-format raw-in-base64-out \ --layers "arn:aws:lambda:region:111122223333:layer:dependencies-layer:2" ``` uv-0.9.17+ds1/docs/guides/integration/coiled.md000066400000000000000000000110211520155276700212460ustar00rootroot00000000000000--- title: Using uv with Coiled description: A complete guide to using uv with Coiled to manage Python dependencies and deploy serverless scripts. --- # Using uv with Coiled [Coiled](https://coiled.io?utm_source=uv-docs) is a serverless, UX-focused cloud computing platform that makes it easy to run code on cloud hardware (AWS, GCP, and Azure). This guide shows how to run Python scripts on the cloud using uv for dependency management and Coiled for cloud deployment. ## Managing script dependencies with uv !!! note We'll use this concrete example throughout this guide, but any Python script can be used with uv and Coiled. We'll use the following script as an example: ```python title="process.py" hl_lines="1-8" # /// script # requires-python = ">=3.12" # dependencies = [ # "pandas", # "pyarrow", # "s3fs", # ] # /// import pandas as pd df = pd.read_parquet( "s3://coiled-data/uber/part.0.parquet", storage_options={"anon": True}, ) print(df.head()) ``` The script uses [`pandas`](https://pandas.pydata.org/docs/) to load a Parquet file hosted in a public bucket on S3, then prints the first few rows. It uses [inline script metadata](https://peps.python.org/pep-0723/) to enumerate its dependencies. When running this script locally, e.g., with: ```bash $ uv run process.py ``` uv will automatically create a virtual environment and installs its dependencies. To learn more about using inline script metadata with uv, see the [script guide](../scripts.md#declaring-script-dependencies). ## Running scripts on the cloud with Coiled Using inline script metadata makes the script fully self-contained: it includes the information that is needed to run it. This makes it easier to run on other machines, like a machine in the cloud. There are many use cases where resources beyond what's available on a local workstation are needed, e.g.: - Processing large amounts of cloud-hosted data - Needing accelerated hardware like GPUs or a big machine with more memory - Running the same script with hundreds or thousands of different inputs, in parallel Coiled makes it simple to run code on cloud hardware. First, authenticate with Coiled using [`coiled login`](https://docs.coiled.io/user_guide/api.html?utm_source=uv-docs#coiled-login) : ```bash $ uvx coiled login ``` You'll be prompted to create a Coiled account if you don't already have one — it's free to start using Coiled. To instruct Coiled to run the script on a virtual machine on AWS, add two comments to the top: ```python title="process.py" hl_lines="1-2" # COILED container ghcr.io/astral-sh/uv:debian-slim # COILED region us-east-2 # /// script # requires-python = ">=3.12" # dependencies = [ # "pandas", # "pyarrow", # "s3fs", # ] # /// import pandas as pd df = pd.read_parquet( "s3://coiled-data/uber/part.0.parquet", storage_options={"anon": True}, ) print(df.head()) ``` !!! tip While Coiled supports AWS, GCP, and Azure, this example assumes AWS is being used (see the `region` option above). If you're new to Coiled, you'll automatically have access to a free account running on AWS. If you're not running on AWS, you can either use a valid `region` for your cloud provider or remove the `region` line above. The comments tell Coiled to use the official [uv Docker image](../integration/docker.md) when running the script (ensuring uv is available) and to run in the `us-east-2` region on AWS (where this example data file happens to live) to avoid any data egress. To submit a batch job for Coiled to run, use [`coiled batch run`](https://docs.coiled.io/user_guide/api.html?utm_source=uv-docs#coiled-batch-run) to execute the `uv run` command in the cloud: ```bash hl_lines="1" $ uvx coiled batch run \ uv run process.py ``` The same process that previously ran locally is now running on a remote cloud VM on AWS. You can monitor the progress of the batch job in the UI at [cloud.coiled.io](https://cloud.coiled.io) or from the terminal using the `coiled batch status`, `coiled batch wait`, and `coiled batch logs` commands. ![Coiled UI](https://docs.coiled.io/_images/uv-coiled.png) Note there's additional configuration we could have specified, e.g., the instance type (the default is a 4-core virtual machine with 16 GiB of memory), disk size, whether to use spot instance, and more. See the [Coiled Batch documentation](https://docs.coiled.io/user_guide/batch.html?utm_source=uv-docs) for more details. For more details on Coiled, and how it can help with other use cases, see the [Coiled documentation](https://docs.coiled.io?utm_source=uv-docs). uv-0.9.17+ds1/docs/guides/integration/dependency-bots.md000066400000000000000000000047601520155276700231060ustar00rootroot00000000000000--- title: Using uv with dependency bots description: A guide to using uv with dependency bots like Renovate and Dependabot. --- # Dependency bots It is considered best practice to regularly update dependencies, to avoid being exposed to vulnerabilities, limit incompatibilities between dependencies, and avoid complex upgrades when upgrading from a too old version. A variety of tools can help staying up-to-date by creating automated pull requests. Several of them support uv, or have work underway to support it. ## Renovate uv is supported by [Renovate](https://github.com/renovatebot/renovate). ### `uv.lock` output Renovate uses the presence of a `uv.lock` file to determine that uv is used for managing dependencies, and will suggest upgrades to [project dependencies](../../concepts/projects/dependencies.md#project-dependencies), [optional dependencies](../../concepts/projects/dependencies.md#optional-dependencies) and [development dependencies](../../concepts/projects/dependencies.md#development-dependencies). Renovate will update both the `pyproject.toml` and `uv.lock` files. The lockfile can also be refreshed on a regular basis (for instance to update transitive dependencies) by enabling the [`lockFileMaintenance`](https://docs.renovatebot.com/configuration-options/#lockfilemaintenance) option: ```jsx title="renovate.json5" { $schema: "https://docs.renovatebot.com/renovate-schema.json", lockFileMaintenance: { enabled: true, }, } ``` ### Inline script metadata Renovate supports updating dependencies defined using [script inline metadata](../scripts.md/#declaring-script-dependencies). Since it cannot automatically detect which Python files use script inline metadata, their locations need to be explicitly defined using [`fileMatch`](https://docs.renovatebot.com/configuration-options/#filematch), like so: ```jsx title="renovate.json5" { $schema: "https://docs.renovatebot.com/renovate-schema.json", pep723: { fileMatch: [ "scripts/generate_docs\\.py", "scripts/run_server\\.py", ], }, } ``` ## Dependabot Dependabot has announced support for uv, but there are some use cases that are not yet working. See [astral-sh/uv#2512](https://github.com/astral-sh/uv/issues/2512) for updates. Dependabot supports updating `uv.lock` files. To enable it, add the uv `package-ecosystem` to your `updates` list in the `dependabot.yml`: ```yaml title="dependabot.yml" version: 2 updates: - package-ecosystem: "uv" directory: "/" schedule: interval: "weekly" ``` uv-0.9.17+ds1/docs/guides/integration/docker.md000066400000000000000000000514411520155276700212700ustar00rootroot00000000000000--- title: Using uv in Docker description: A complete guide to using uv in Docker to manage Python dependencies while optimizing build times and image size via multi-stage builds, intermediate layers, and more. --- # Using uv in Docker ## Getting started !!! tip Check out the [`uv-docker-example`](https://github.com/astral-sh/uv-docker-example) project for an example of best practices when using uv to build an application in Docker. uv provides both _distroless_ Docker images, which are useful for [copying uv binaries](#installing-uv) into your own image builds, and images derived from popular base images, which are useful for using uv in a container. The distroless images do not contain anything but the uv binaries. In contrast, the derived images include an operating system with uv pre-installed. As an example, to run uv in a container using a Debian-based image: ```console $ docker run --rm -it ghcr.io/astral-sh/uv:debian uv --help ``` ### Available images The following distroless images are available: - `ghcr.io/astral-sh/uv:latest` - `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.9.17` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.8` (the latest patch version) And the following derived images are available: - Based on `alpine:3.22`: - `ghcr.io/astral-sh/uv:alpine` - `ghcr.io/astral-sh/uv:alpine3.22` - Based on `alpine:3.21`: - `ghcr.io/astral-sh/uv:alpine3.21` - Based on `debian:trixie-slim`: - `ghcr.io/astral-sh/uv:debian-slim` - `ghcr.io/astral-sh/uv:trixie-slim` - Based on `debian:bookworm-slim`: - `ghcr.io/astral-sh/uv:bookworm-slim` - Based on `buildpack-deps:trixie`: - `ghcr.io/astral-sh/uv:debian` - `ghcr.io/astral-sh/uv:trixie` - Based on `buildpack-deps:bookworm`: - `ghcr.io/astral-sh/uv:bookworm` - Based on `python3.x-alpine`: - `ghcr.io/astral-sh/uv:python3.14-alpine` - `ghcr.io/astral-sh/uv:python3.13-alpine` - `ghcr.io/astral-sh/uv:python3.12-alpine` - `ghcr.io/astral-sh/uv:python3.11-alpine` - `ghcr.io/astral-sh/uv:python3.10-alpine` - `ghcr.io/astral-sh/uv:python3.9-alpine` - `ghcr.io/astral-sh/uv:python3.8-alpine` - Based on `python3.x-trixie`: - `ghcr.io/astral-sh/uv:python3.14-trixie` - `ghcr.io/astral-sh/uv:python3.13-trixie` - `ghcr.io/astral-sh/uv:python3.12-trixie` - `ghcr.io/astral-sh/uv:python3.11-trixie` - `ghcr.io/astral-sh/uv:python3.10-trixie` - `ghcr.io/astral-sh/uv:python3.9-trixie` - Based on `python3.x-slim-trixie`: - `ghcr.io/astral-sh/uv:python3.14-trixie-slim` - `ghcr.io/astral-sh/uv:python3.13-trixie-slim` - `ghcr.io/astral-sh/uv:python3.12-trixie-slim` - `ghcr.io/astral-sh/uv:python3.11-trixie-slim` - `ghcr.io/astral-sh/uv:python3.10-trixie-slim` - `ghcr.io/astral-sh/uv:python3.9-trixie-slim` - Based on `python3.x-bookworm`: - `ghcr.io/astral-sh/uv:python3.14-bookworm` - `ghcr.io/astral-sh/uv:python3.13-bookworm` - `ghcr.io/astral-sh/uv:python3.12-bookworm` - `ghcr.io/astral-sh/uv:python3.11-bookworm` - `ghcr.io/astral-sh/uv:python3.10-bookworm` - `ghcr.io/astral-sh/uv:python3.9-bookworm` - `ghcr.io/astral-sh/uv:python3.8-bookworm` - Based on `python3.x-slim-bookworm`: - `ghcr.io/astral-sh/uv:python3.14-bookworm-slim` - `ghcr.io/astral-sh/uv:python3.13-bookworm-slim` - `ghcr.io/astral-sh/uv:python3.12-bookworm-slim` - `ghcr.io/astral-sh/uv:python3.11-bookworm-slim` - `ghcr.io/astral-sh/uv:python3.10-bookworm-slim` - `ghcr.io/astral-sh/uv:python3.9-bookworm-slim` - `ghcr.io/astral-sh/uv:python3.8-bookworm-slim` As with the distroless image, each derived image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and `ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.9.17-alpine`. In addition, starting with `0.8` each derived image also sets `UV_TOOL_BIN_DIR` to `/usr/local/bin` to allow `uv tool install` to work as expected with the default user. For more details, see the [GitHub Container](https://github.com/astral-sh/uv/pkgs/container/uv) page. ### Installing uv Use one of the above images with uv pre-installed or install uv by copying the binary from the official distroless Docker image: ```dockerfile title="Dockerfile" FROM python:3.12-slim-trixie COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ ``` Or, with the installer: ```dockerfile title="Dockerfile" FROM python:3.12-slim-trixie # The installer requires curl (and certificates) to download the release archive RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates # Download the latest installer ADD https://astral.sh/uv/install.sh /uv-installer.sh # Run the installer then remove it RUN sh /uv-installer.sh && rm /uv-installer.sh # Ensure the installed binary is on the `PATH` ENV PATH="/root/.local/bin/:$PATH" ``` Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile COPY --from=ghcr.io/astral-sh/uv:0.9.17 /uv /uvx /bin/ ``` !!! tip While the Dockerfile example above pins to a specific tag, it's also possible to pin a specific SHA256. Pinning a specific SHA256 is considered best practice in environments that require reproducible builds as tags can be moved across different commit SHAs. ```Dockerfile # e.g., using a hash from a previous release COPY --from=ghcr.io/astral-sh/uv@sha256:2381d6aa60c326b71fd40023f921a0a3b8f91b14d5db6b90402e65a635053709 /uv /uvx /bin/ ``` Or, with the installer: ```dockerfile ADD https://astral.sh/uv/0.9.17/install.sh /uv-installer.sh ``` ### Installing a project If you're using uv to manage your project, you can copy it into the image and install it: ```dockerfile title="Dockerfile" # Copy the project into the image COPY . /app # Disable development dependencies ENV UV_NO_DEV=1 # Sync the project into a new environment, asserting the lockfile is up to date WORKDIR /app RUN uv sync --locked ``` !!! important It is best practice to add `.venv` to a [`.dockerignore` file](https://docs.docker.com/build/concepts/context/#dockerignore-files) in your repository to prevent it from being included in image builds. The project virtual environment is dependent on your local platform and should be created from scratch in the image. Then, to start your application by default: ```dockerfile title="Dockerfile" # Presuming there is a `my_app` command provided by the project CMD ["uv", "run", "my_app"] ``` !!! tip It is best practice to use [intermediate layers](#intermediate-layers) separating installation of dependencies and the project itself to improve Docker image build times. See a complete example in the [`uv-docker-example` project](https://github.com/astral-sh/uv-docker-example/blob/main/Dockerfile). ### Using the environment Once the project is installed, you can either _activate_ the project virtual environment by placing its binary directory at the front of the path: ```dockerfile title="Dockerfile" ENV PATH="/app/.venv/bin:$PATH" ``` Or, you can use `uv run` for any commands that require the environment: ```dockerfile title="Dockerfile" RUN uv run some_script.py ``` !!! tip Alternatively, the [`UV_PROJECT_ENVIRONMENT` setting](../../concepts/projects/config.md#project-environment-path) can be set before syncing to install to the system Python environment and skip environment activation entirely. ### Using installed tools To use installed tools, ensure the [tool bin directory](../../concepts/tools.md#tool-executables) is on the path: ```dockerfile title="Dockerfile" ENV PATH=/root/.local/bin:$PATH RUN uv tool install cowsay ``` ```console $ docker run -it $(docker build -q .) /bin/bash -c "cowsay -t hello" _____ | hello | ===== \ \ ^__^ (oo)\_______ (__)\ )\/\ ||----w | || || ``` !!! note The tool bin directory's location can be determined by running the `uv tool dir --bin` command in the container. Alternatively, it can be set to a constant location: ```dockerfile title="Dockerfile" ENV UV_TOOL_BIN_DIR=/opt/uv-bin/ ``` ### Installing Python in ARM musl images While uv will attempt to [install a compatible Python version](../install-python.md) if no such version is available in the image, uv does not yet support installing Python for musl Linux on ARM. For example, if you are using an Alpine Linux base image on an ARM machine, you may need to add it with the system package manager: ```shell apk add --no-cache python3~=3.12 ``` ## Developing in a container When developing, it's useful to mount the project directory into a container. With this setup, changes to the project can be immediately reflected in a containerized service without rebuilding the image. However, it is important _not_ to include the project virtual environment (`.venv`) in the mount, because the virtual environment is platform specific and the one built for the image should be kept. ### Mounting the project with `docker run` Bind mount the project (in the working directory) to `/app` while retaining the `.venv` directory with an [anonymous volume](https://docs.docker.com/engine/storage/#volumes): ```console $ docker run --rm --volume .:/app --volume /app/.venv [...] ``` !!! tip The `--rm` flag is included to ensure the container and anonymous volume are cleaned up when the container exits. See a complete example in the [`uv-docker-example` project](https://github.com/astral-sh/uv-docker-example/blob/main/run.sh). ### Configuring `watch` with `docker compose` When using Docker compose, more sophisticated tooling is available for container development. The [`watch`](https://docs.docker.com/compose/file-watch/#compose-watch-versus-bind-mounts) option allows for greater granularity than is practical with a bind mount and supports triggering updates to the containerized service when files change. !!! note This feature requires Compose 2.22.0 which is bundled with Docker Desktop 4.24. Configure `watch` in your [Docker compose file](https://docs.docker.com/compose/compose-application-model/#the-compose-file) to mount the project directory without syncing the project virtual environment and to rebuild the image when the configuration changes: ```yaml title="compose.yaml" services: example: build: . # ... develop: # Create a `watch` configuration to update the app # watch: # Sync the working directory with the `/app` directory in the container - action: sync path: . target: /app # Exclude the project virtual environment ignore: - .venv/ # Rebuild the image on changes to the `pyproject.toml` - action: rebuild path: ./pyproject.toml ``` Then, run `docker compose watch` to run the container with the development setup. See a complete example in the [`uv-docker-example` project](https://github.com/astral-sh/uv-docker-example/blob/main/compose.yml). ## Optimizations ### Compiling bytecode Compiling Python source files to bytecode is typically desirable for production images as it tends to improve startup time (at the cost of increased installation time). To enable bytecode compilation, use the `--compile-bytecode` flag: ```dockerfile title="Dockerfile" RUN uv sync --compile-bytecode ``` Alternatively, you can set the `UV_COMPILE_BYTECODE` environment variable to ensure that all commands within the Dockerfile compile bytecode: ```dockerfile title="Dockerfile" ENV UV_COMPILE_BYTECODE=1 ``` ### Caching A [cache mount](https://docs.docker.com/build/guide/mounts/#add-a-cache-mount) can be used to improve performance across builds: ```dockerfile title="Dockerfile" ENV UV_LINK_MODE=copy RUN --mount=type=cache,target=/root/.cache/uv \ uv sync ``` Changing the default [`UV_LINK_MODE`](../../reference/settings.md#link-mode) silences warnings about not being able to use hard links since the cache and sync target are on separate file systems. If you're not mounting the cache, image size can be reduced by using the `--no-cache` flag or setting `UV_NO_CACHE`. By default, managed Python installations are not cached before being installed. Setting `UV_PYTHON_CACHE_DIR` can be used in combination with a cache mount: ```dockerfile title="Dockerfile" ENV UV_PYTHON_CACHE_DIR=/root/.cache/uv/python RUN --mount=type=cache,target=/root/.cache/uv \ uv python install ``` !!! note The cache directory's location can be determined by running the `uv cache dir` command in the container. Alternatively, the cache can be set to a constant location: ```dockerfile title="Dockerfile" ENV UV_CACHE_DIR=/opt/uv-cache/ ``` ### Intermediate layers If you're using uv to manage your project, you can improve build times by moving your transitive dependency installation into its own layer via the `--no-install` options. `uv sync --no-install-project` will install the dependencies of the project but not the project itself. Since the project changes frequently, but its dependencies are generally static, this can be a big time saver. ```dockerfile title="Dockerfile" # Install uv FROM python:3.12-slim COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ # Change the working directory to the `app` directory WORKDIR /app # Install dependencies RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --locked --no-install-project # Copy the project into the image COPY . /app # Sync the project RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked ``` Note that the `pyproject.toml` is required to identify the project root and name, but the project _contents_ are not copied into the image until the final `uv sync` command. !!! tip If you want to remove additional, specific packages from the sync, use `--no-install-package `. #### Intermediate layers in workspaces If you're using a [workspace](../../concepts/projects/workspaces.md), then a couple changes are needed: - Use `--frozen` instead of `--locked` during the initially sync. - Use the `--no-install-workspace` flag which excludes the project _and_ any workspace members. ```dockerfile title="Dockerfile" # Install uv FROM python:3.12-slim COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --no-install-workspace COPY . /app RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked ``` uv cannot assert that the `uv.lock` file is up-to-date without each of the workspace member `pyproject.toml` files, so we use `--frozen` instead of `--locked` to skip the check during the initial sync. The next sync, after all the workspace members have been copied, can still use `--locked` and will validate that the lockfile is correct for all workspace members. ### Non-editable installs By default, uv installs projects and workspace members in editable mode, such that changes to the source code are immediately reflected in the environment. `uv sync` and `uv run` both accept a `--no-editable` flag, which instructs uv to install the project in non-editable mode, removing any dependency on the source code. In the context of a multi-stage Docker image, `--no-editable` can be used to include the project in the synced virtual environment from one stage, then copy the virtual environment alone (and not the source code) into the final image. For example: ```dockerfile title="Dockerfile" # Install uv FROM python:3.12-slim AS builder COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ # Change the working directory to the `app` directory WORKDIR /app # Install dependencies RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --locked --no-install-project --no-editable # Copy the project into the intermediate image COPY . /app # Sync the project RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-editable FROM python:3.12-slim # Copy the environment, but not the source code COPY --from=builder --chown=app:app /app/.venv /app/.venv # Run the application CMD ["/app/.venv/bin/hello"] ``` ### Using uv temporarily If uv isn't needed in the final image, the binary can be mounted in each invocation: ```dockerfile title="Dockerfile" RUN --mount=from=ghcr.io/astral-sh/uv,source=/uv,target=/bin/uv \ uv sync ``` ## Using the pip interface ### Installing a package The system Python environment is safe to use this context, since a container is already isolated. The `--system` flag can be used to install in the system environment: ```dockerfile title="Dockerfile" RUN uv pip install --system ruff ``` To use the system Python environment by default, set the `UV_SYSTEM_PYTHON` variable: ```dockerfile title="Dockerfile" ENV UV_SYSTEM_PYTHON=1 ``` Alternatively, a virtual environment can be created and activated: ```dockerfile title="Dockerfile" RUN uv venv /opt/venv # Use the virtual environment automatically ENV VIRTUAL_ENV=/opt/venv # Place entry points in the environment at the front of the path ENV PATH="/opt/venv/bin:$PATH" ``` When using a virtual environment, the `--system` flag should be omitted from uv invocations: ```dockerfile title="Dockerfile" RUN uv pip install ruff ``` ### Installing requirements To install requirements files, copy them into the container: ```dockerfile title="Dockerfile" COPY requirements.txt . RUN uv pip install -r requirements.txt ``` ### Installing a project When installing a project alongside requirements, it is best practice to separate copying the requirements from the rest of the source code. This allows the dependencies of the project (which do not change often) to be cached separately from the project itself (which changes very frequently). ```dockerfile title="Dockerfile" COPY pyproject.toml . RUN uv pip install -r pyproject.toml COPY . . RUN uv pip install -e . ``` ## Verifying image provenance The Docker images are signed during the build process to provide proof of their origin. These attestations can be used to verify that an image was produced from an official channel. For example, you can verify the attestations with the [GitHub CLI tool `gh`](https://cli.github.com/): ```console $ gh attestation verify --owner astral-sh oci://ghcr.io/astral-sh/uv:latest Loaded digest sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx for oci://ghcr.io/astral-sh/uv:latest Loaded 1 attestation from GitHub API The following policy criteria will be enforced: - OIDC Issuer must match:................... https://token.actions.githubusercontent.com - Source Repository Owner URI must match:... https://github.com/astral-sh - Predicate type must match:................ https://slsa.dev/provenance/v1 - Subject Alternative Name must match regex: (?i)^https://github.com/astral-sh/ ✓ Verification succeeded! sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx was attested by: REPO PREDICATE_TYPE WORKFLOW astral-sh/uv https://slsa.dev/provenance/v1 .github/workflows/build-docker.yml@refs/heads/main ``` This tells you that the specific Docker image was built by the official uv GitHub release workflow and hasn't been tampered with since. GitHub attestations build on the [sigstore.dev infrastructure](https://www.sigstore.dev/). As such you can also use the [`cosign` command](https://github.com/sigstore/cosign) to verify the attestation blob against the (multi-platform) manifest for `uv`: ```console $ REPO=astral-sh/uv $ gh attestation download --repo $REPO oci://ghcr.io/${REPO}:latest Wrote attestations to file sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.jsonl. Any previous content has been overwritten The trusted metadata is now available at sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.jsonl $ docker buildx imagetools inspect ghcr.io/${REPO}:latest --format "{{json .Manifest}}" > manifest.json $ cosign verify-blob-attestation \ --new-bundle-format \ --bundle "$(jq -r .digest manifest.json).jsonl" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ --certificate-identity-regexp="^https://github\.com/${REPO}/.*" \ <(jq -j '.|del(.digest,.size)' manifest.json) Verified OK ``` !!! tip These examples use `latest`, but best practice is to verify the attestation for a specific version tag, e.g., `ghcr.io/astral-sh/uv:0.9.17`, or (even better) the specific image digest, such as `ghcr.io/astral-sh/uv:0.5.27@sha256:5adf09a5a526f380237408032a9308000d14d5947eafa687ad6c6a2476787b4f`. uv-0.9.17+ds1/docs/guides/integration/fastapi.md000066400000000000000000000063711520155276700214520ustar00rootroot00000000000000--- title: Using uv with FastAPI description: A guide to using uv with FastAPI to manage Python dependencies, run applications, and deploy with Docker. --- # Using uv with FastAPI [FastAPI](https://github.com/fastapi/fastapi) is a modern, high-performance Python web framework. You can use uv to manage your FastAPI project, including installing dependencies, managing environments, running FastAPI applications, and more. !!! note You can view the source code for this guide in the [uv-fastapi-example](https://github.com/astral-sh/uv-fastapi-example) repository. ## Migrating an existing FastAPI project As an example, consider the sample application defined in the [FastAPI documentation](https://fastapi.tiangolo.com/tutorial/bigger-applications/), structured as follows: ```plaintext project └── app ├── __init__.py ├── main.py ├── dependencies.py ├── routers │ ├── __init__.py │ ├── items.py │ └── users.py └── internal ├── __init__.py └── admin.py ``` To use uv with this application, inside the `project` directory run: ```console $ uv init --app ``` This creates a [project with an application layout](../../concepts/projects/init.md#applications) and a `pyproject.toml` file. Then, add a dependency on FastAPI: ```console $ uv add fastapi --extra standard ``` You should now have the following structure: ```plaintext project ├── pyproject.toml └── app ├── __init__.py ├── main.py ├── dependencies.py ├── routers │ ├── __init__.py │ ├── items.py │ └── users.py └── internal ├── __init__.py └── admin.py ``` And the contents of the `pyproject.toml` file should look something like this: ```toml title="pyproject.toml" [project] name = "uv-fastapi-example" version = "0.1.0" description = "FastAPI project" readme = "README.md" requires-python = ">=3.12" dependencies = [ "fastapi[standard]", ] ``` From there, you can run the FastAPI application with: ```console $ uv run fastapi dev ``` `uv run` will automatically resolve and lock the project dependencies (i.e., create a `uv.lock` alongside the `pyproject.toml`), create a virtual environment, and run the command in that environment. Test the app by opening http://127.0.0.1:8000/?token=jessica in a web browser. ## Deployment To deploy the FastAPI application with Docker, you can use the following `Dockerfile`: ```dockerfile title="Dockerfile" FROM python:3.12-slim # Install uv. COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ # Copy the application into the container. COPY . /app # Install the application dependencies. WORKDIR /app RUN uv sync --frozen --no-cache # Run the application. CMD ["/app/.venv/bin/fastapi", "run", "app/main.py", "--port", "80", "--host", "0.0.0.0"] ``` Build the Docker image with: ```console $ docker build -t fastapi-app . ``` Run the Docker container locally with: ```console $ docker run -p 8000:80 fastapi-app ``` Navigate to http://127.0.0.1:8000/?token=jessica in your browser to verify that the app is running correctly. !!! tip For more on using uv with Docker, see the [Docker guide](./docker.md). uv-0.9.17+ds1/docs/guides/integration/github.md000066400000000000000000000263131520155276700213030ustar00rootroot00000000000000--- title: Using uv in GitHub Actions description: A guide to using uv in GitHub Actions, including installation, setting up Python, installing dependencies, and more. --- # Using uv in GitHub Actions ## Installation For use with GitHub Actions, we recommend the official [`astral-sh/setup-uv`](https://github.com/astral-sh/setup-uv) action, which installs uv, adds it to PATH, (optionally) persists the cache, and more, with support for all uv-supported platforms. To install the latest version of uv: ```yaml title="example.yml" hl_lines="11 12" name: Example jobs: uv-example: name: python runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Install uv uses: astral-sh/setup-uv@v7 ``` It is considered best practice to pin to a specific uv version, e.g., with: ```yaml title="example.yml" hl_lines="14 15" name: Example jobs: uv-example: name: python runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Install uv uses: astral-sh/setup-uv@v7 with: # Install a specific version of uv. version: "0.9.17" ``` ## Setting up Python Python can be installed with the `python install` command: ```yaml title="example.yml" hl_lines="14 15" name: Example jobs: uv-example: name: python runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Install uv uses: astral-sh/setup-uv@v7 - name: Set up Python run: uv python install ``` This will respect the Python version pinned in the project. Alternatively, the official GitHub `setup-python` action can be used. This can be faster, because GitHub caches the Python versions alongside the runner. Set the [`python-version-file`](https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md#using-the-python-version-file-input) option to use the pinned version for the project: ```yaml title="example.yml" hl_lines="14" name: Example jobs: uv-example: name: python runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: "Set up Python" uses: actions/setup-python@v6 with: python-version-file: ".python-version" - name: Install uv uses: astral-sh/setup-uv@v7 ``` Or, specify the `pyproject.toml` file to ignore the pin and use the latest version compatible with the project's `requires-python` constraint: ```yaml title="example.yml" hl_lines="14" name: Example jobs: uv-example: name: python runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: "Set up Python" uses: actions/setup-python@v6 with: python-version-file: "pyproject.toml" - name: Install uv uses: astral-sh/setup-uv@v7 ``` ## Multiple Python versions When using a matrix to test multiple Python versions, set the Python version using `astral-sh/setup-uv`, which will override the Python version specification in the `pyproject.toml` or `.python-version` files: ```yaml title="example.yml" hl_lines="17 18" jobs: build: name: continuous-integration runs-on: ubuntu-latest strategy: matrix: python-version: - "3.10" - "3.11" - "3.12" steps: - uses: actions/checkout@v5 - name: Install uv and set the Python version uses: astral-sh/setup-uv@v7 with: python-version: ${{ matrix.python-version }} ``` If not using the `setup-uv` action, you can set the `UV_PYTHON` environment variable: ```yaml title="example.yml" hl_lines="12" jobs: build: name: continuous-integration runs-on: ubuntu-latest strategy: matrix: python-version: - "3.10" - "3.11" - "3.12" env: UV_PYTHON: ${{ matrix.python-version }} steps: - uses: actions/checkout@v5 ``` ## Syncing and running Once uv and Python are installed, the project can be installed with `uv sync` and commands can be run in the environment with `uv run`: ```yaml title="example.yml" hl_lines="15 17-22" name: Example jobs: uv-example: name: python runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Install uv uses: astral-sh/setup-uv@v7 - name: Install the project run: uv sync --locked --all-extras --dev - name: Run tests # For example, using `pytest` run: uv run pytest tests ``` !!! tip The [`UV_PROJECT_ENVIRONMENT` setting](../../concepts/projects/config.md#project-environment-path) can be used to install to the system Python environment instead of creating a virtual environment. ## Caching It may improve CI times to store uv's cache across workflow runs. The [`astral-sh/setup-uv`](https://github.com/astral-sh/setup-uv) has built-in support for persisting the cache: ```yaml title="example.yml" - name: Enable caching uses: astral-sh/setup-uv@v7 with: enable-cache: true ``` Alternatively, you can manage the cache manually with the `actions/cache` action: ```yaml title="example.yml" jobs: install_job: env: # Configure a constant location for the uv cache UV_CACHE_DIR: /tmp/.uv-cache steps: # ... setup up Python and uv ... - name: Restore uv cache uses: actions/cache@v4 with: path: /tmp/.uv-cache key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} restore-keys: | uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} uv-${{ runner.os }} # ... install packages, run tests, etc ... - name: Minimize uv cache run: uv cache prune --ci ``` The `uv cache prune --ci` command is used to reduce the size of the cache and is optimized for CI. Its effect on performance is dependent on the packages being installed. !!! tip If using `uv pip`, use `requirements.txt` instead of `uv.lock` in the cache key. !!! note [post-job-hook]: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/running-scripts-before-or-after-a-job When using non-ephemeral, self-hosted runners the default cache directory can grow unbounded. In this case, it may not be optimal to share the cache between jobs. Instead, move the cache inside the GitHub Workspace and remove it once the job finishes using a [Post Job Hook][post-job-hook]. ```yaml install_job: env: # Configure a relative location for the uv cache UV_CACHE_DIR: ${{ github.workspace }}/.cache/uv ``` Using a post job hook requires setting the `ACTIONS_RUNNER_HOOK_JOB_STARTED` environment variable on the self-hosted runner to the path of a cleanup script such as the one shown below. ```sh title="clean-uv-cache.sh" #!/usr/bin/env sh uv cache clean ``` ## Using `uv pip` If using the `uv pip` interface instead of the uv project interface, uv requires a virtual environment by default. To allow installing packages into the system environment, use the `--system` flag on all `uv` invocations or set the `UV_SYSTEM_PYTHON` variable. The `UV_SYSTEM_PYTHON` variable can be defined in at different scopes. Opt-in for the entire workflow by defining it at the top level: ```yaml title="example.yml" env: UV_SYSTEM_PYTHON: 1 jobs: ... ``` Or, opt-in for a specific job in the workflow: ```yaml title="example.yml" jobs: install_job: env: UV_SYSTEM_PYTHON: 1 ... ``` Or, opt-in for a specific step in a job: ```yaml title="example.yml" steps: - name: Install requirements run: uv pip install -r requirements.txt env: UV_SYSTEM_PYTHON: 1 ``` To opt-out again, the `--no-system` flag can be used in any uv invocation. ## Private repos If your project has [dependencies](../../concepts/projects/dependencies.md#git) on private GitHub repositories, you will need to configure a [personal access token (PAT)][PAT] to allow uv to fetch them. After creating a PAT that has read access to the private repositories, add it as a [repository secret]. Then, you can use the [`gh`](https://cli.github.com/) CLI (which is installed in GitHub Actions runners by default) to configure a [credential helper for Git](../../concepts/authentication/git.md#git-credential-helpers) to use the PAT for queries to repositories hosted on `github.com`. For example, if you called your repository secret `MY_PAT`: ```yaml title="example.yml" steps: - name: Register the personal access token run: echo "${{ secrets.MY_PAT }}" | gh auth login --with-token - name: Configure the Git credential helper run: gh auth setup-git ``` [PAT]: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens [repository secret]: https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository ## Publishing to PyPI uv can be used to build and publish your package to PyPI from GitHub Actions. We provide a standalone example alongside this guide in [astral-sh/trusted-publishing-examples](https://github.com/astral-sh/trusted-publishing-examples). The workflow uses [trusted publishing](https://docs.pypi.org/trusted-publishers/), so no credentials need to be configured. In the example workflow, we use a script to test that the source distribution and the wheel are both functional and we didn't miss any files. This step is recommended, but optional. First, add a release workflow to your project: ```yaml title=".github/workflows/publish.yml" name: "Publish" on: push: tags: # Publish on any tag starting with a `v`, e.g., v0.1.0 - v* jobs: run: runs-on: ubuntu-latest environment: name: pypi permissions: id-token: write contents: read steps: - name: Checkout uses: actions/checkout@v5 - name: Install uv uses: astral-sh/setup-uv@v7 - name: Install Python 3.13 run: uv python install 3.13 - name: Build run: uv build # Check that basic features work and we didn't miss to include crucial files - name: Smoke test (wheel) run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py - name: Smoke test (source distribution) run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py - name: Publish run: uv publish ``` Then, create the environment defined in the workflow in the GitHub repository under "Settings" -> "Environments". ![GitHub settings dialog showing how to add the "pypi" environment under "Settings" -> "Environments"](../../assets/github-add-environment.png) Add a [trusted publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/) to your PyPI project in the project settings under "Publishing". Ensure that all fields match with your GitHub configuration. ![PyPI project publishing settings dialog showing how to set all fields for a trusted publisher configuration](../../assets/pypi-add-trusted-publisher.png) After saving: ![PyPI project publishing settings dialog showing the configured trusted publishing settings](../../assets/pypi-with-trusted-publisher.png) Finally, tag a release and push it. Make sure it starts with `v` to match the pattern in the workflow. ```console $ git tag -a v0.1.0 -m v0.1.0 $ git push --tags ``` uv-0.9.17+ds1/docs/guides/integration/gitlab.md000066400000000000000000000045361520155276700212660ustar00rootroot00000000000000--- title: Using uv in GitLab CI/CD description: A guide to using uv in GitLab CI/CD, including installation, setting up Python, installing dependencies, and more. --- # Using uv in GitLab CI/CD ## Using the uv image Astral provides [Docker images](docker.md#available-images) with uv preinstalled. Select a variant that is suitable for your workflow. ```yaml title="gitlab-ci.yml" variables: UV_VERSION: "0.9.17" PYTHON_VERSION: "3.12" BASE_LAYER: bookworm-slim # GitLab CI creates a separate mountpoint for the build directory, # so we need to copy instead of using hard links. UV_LINK_MODE: copy uv: image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER script: # your `uv` commands ``` !!! note If you are using a distroless image, you have to specify the entrypoint: ```yaml uv: image: name: ghcr.io/astral-sh/uv:$UV_VERSION entrypoint: [""] # ... ``` ## Caching Persisting the uv cache between workflow runs can improve performance. ```yaml uv-install: variables: UV_CACHE_DIR: .uv-cache cache: - key: files: - uv.lock paths: - $UV_CACHE_DIR script: # Your `uv` commands - uv cache prune --ci ``` See the [GitLab caching documentation](https://docs.gitlab.com/ee/ci/caching/) for more details on configuring caching. Using `uv cache prune --ci` at the end of the job is recommended to reduce cache size. See the [uv cache documentation](../../concepts/cache.md#caching-in-continuous-integration) for more details. ## Using `uv pip` If using the `uv pip` interface instead of the uv project interface, uv requires a virtual environment by default. To allow installing packages into the system environment, use the `--system` flag on all uv invocations or set the `UV_SYSTEM_PYTHON` variable. The `UV_SYSTEM_PYTHON` variable can be defined in at different scopes. You can read more about how [variables and their precedence works in GitLab here](https://docs.gitlab.com/ee/ci/variables/) Opt-in for the entire workflow by defining it at the top level: ```yaml title="gitlab-ci.yml" variables: UV_SYSTEM_PYTHON: 1 # [...] ``` To opt-out again, the `--no-system` flag can be used in any uv invocation. When persisting the cache, you may want to use `requirements.txt` or `pyproject.toml` as your cache key files instead of `uv.lock`. uv-0.9.17+ds1/docs/guides/integration/index.md000066400000000000000000000012351520155276700211240ustar00rootroot00000000000000# Integration guides Learn how to integrate uv with other software: - [Using in Docker images](./docker.md) - [Using with Jupyter notebooks](./jupyter.md) - [Using with marimo notebooks](./marimo.md) - [Using with pre-commit](./pre-commit.md) - [Using in GitHub Actions](./github.md) - [Using in GitLab CI/CD](./gitlab.md) - [Using with alternative package indexes](./alternative-indexes.md) - [Installing PyTorch](./pytorch.md) - [Building a FastAPI application](./fastapi.md) - [Using with AWS Lambda](./aws-lambda.md) - [Using with Coiled](./coiled.md) Or, explore the [concept documentation](../../concepts/index.md) for comprehensive breakdown of each feature. uv-0.9.17+ds1/docs/guides/integration/jupyter.md000066400000000000000000000150501520155276700215170ustar00rootroot00000000000000--- title: Using uv with Jupyter description: A complete guide to using uv with Jupyter notebooks for interactive computing, data analysis, and visualization, including kernel management and virtual environment integration. --- # Using uv with Jupyter The [Jupyter](https://jupyter.org/) notebook is a popular tool for interactive computing, data analysis, and visualization. You can use Jupyter with uv in a few different ways, either to interact with a project, or as a standalone tool. ## Using Jupyter within a project If you're working within a [project](../../concepts/projects/index.md), you can start a Jupyter server with access to the project's virtual environment via the following: ```console $ uv run --with jupyter jupyter lab ``` By default, `jupyter lab` will start the server at [http://localhost:8888/lab](http://localhost:8888/lab). Within a notebook, you can import your project's modules as you would in any other file in the project. For example, if your project depends on `requests`, `import requests` will import `requests` from the project's virtual environment. If you're looking for read-only access to the project's virtual environment, then there's nothing more to it. However, if you need to install additional packages from within the notebook, there are a few extra details to consider. ### Creating a kernel If you need to install packages from within the notebook, we recommend creating a dedicated kernel for your project. Kernels enable the Jupyter server to run in one environment, with individual notebooks running in their own, separate environments. In the context of uv, we can create a kernel for a project while installing Jupyter itself in an isolated environment, as in `uv run --with jupyter jupyter lab`. Creating a kernel for the project ensures that the notebook is hooked up to the correct environment, and that any packages installed from within the notebook are installed into the project's virtual environment. To create a kernel, you'll need to install `ipykernel` as a development dependency: ```console $ uv add --dev ipykernel ``` Then, you can create the kernel for `project` with: ```console $ uv run ipython kernel install --user --env VIRTUAL_ENV $(pwd)/.venv --name=project ``` From there, start the server with: ```console $ uv run --with jupyter jupyter lab ``` When creating a notebook, select the `project` kernel from the dropdown. Then use `!uv add pydantic` to add `pydantic` to the project's dependencies, or `!uv pip install pydantic` to install `pydantic` into the project's virtual environment without persisting the change to the project `pyproject.toml` or `uv.lock` files. Either command will make `import pydantic` work within the notebook. ### Installing packages without a kernel If you don't want to create a kernel, you can still install packages from within the notebook. However, there are a few caveats to consider. Though `uv run --with jupyter` runs in an isolated environment, within the notebook itself, `!uv add` and related commands will modify the _project's_ environment, even without a kernel. For example, running `!uv add pydantic` from within a notebook will add `pydantic` to the project's dependencies and virtual environment, such that `import pydantic` will work immediately, without further configuration or a server restart. However, since the Jupyter server is the "active" environment, `!uv pip install` will install package's into _Jupyter's_ environment, not the project environment. Such dependencies will persist for the lifetime of the Jupyter server, but may disappear on subsequent `jupyter` invocations. If you're working with a notebook that relies on pip (e.g., via the `%pip` magic), you can include pip in your project's virtual environment by running `uv venv --seed` prior to starting the Jupyter server. For example, given: ```console $ uv venv --seed $ uv run --with jupyter jupyter lab ``` Subsequent `%pip install` invocations within the notebook will install packages into the project's virtual environment. However, such modifications will _not_ be reflected in the project's `pyproject.toml` or `uv.lock` files. ## Using Jupyter as a standalone tool If you ever need ad hoc access to a notebook (i.e., to run a Python snippet interactively), you can start a Jupyter server at any time with `uv tool run jupyter lab`. This will run a Jupyter server in an isolated environment. ## Using Jupyter with a non-project environment If you need to run Jupyter in a virtual environment that isn't associated with a [project](../../concepts/projects/index.md) (e.g., has no `pyproject.toml` or `uv.lock`), you can do so by adding Jupyter to the environment directly. For example: === "macOS and Linux" ```console $ uv venv --seed $ uv pip install pydantic $ uv pip install jupyterlab $ .venv/bin/jupyter lab ``` === "Windows" ```pwsh-session PS> uv venv --seed PS> uv pip install pydantic PS> uv pip install jupyterlab PS> .venv\Scripts\jupyter lab ``` From here, `import pydantic` will work within the notebook, and you can install additional packages via `!uv pip install`, or even `!pip install`. ## Using Jupyter from VS Code You can also engage with Jupyter notebooks from within an editor like VS Code. To connect a uv-managed project to a Jupyter notebook within VS Code, we recommend creating a kernel for the project, as in the following: ```console # Create a project. $ uv init project # Move into the project directory. $ cd project # Add ipykernel as a dev dependency. $ uv add --dev ipykernel # Open the project in VS Code. $ code . ``` Once the project directory is open in VS Code, you can create a new Jupyter notebook by selecting "Create: New Jupyter Notebook" from the command palette. When prompted to select a kernel, choose "Python Environments" and select the virtual environment you created earlier (e.g., `.venv/bin/python` on macOS and Linux, or `.venv\Scripts\python` on Windows). !!! note VS Code requires `ipykernel` to be present in the project environment. If you'd prefer to avoid adding `ipykernel` as a dev dependency, you can install it directly into the project environment with `uv pip install ipykernel`. If you need to manipulate the project's environment from within the notebook, you may need to add `uv` as an explicit development dependency: ```console $ uv add --dev uv ``` From there, you can use `!uv add pydantic` to add `pydantic` to the project's dependencies, or `!uv pip install pydantic` to install `pydantic` into the project's virtual environment without updating the project's `pyproject.toml` or `uv.lock` files. uv-0.9.17+ds1/docs/guides/integration/marimo.md000066400000000000000000000066651520155276700213150ustar00rootroot00000000000000--- title: Using uv with marimo description: A complete guide to using uv with marimo notebooks for interactive computing, script execution, and data apps. --- # Using uv with marimo [marimo](https://github.com/marimo-team/marimo) is an open-source Python notebook that blends interactive computing with the reproducibility and reusability of traditional software, letting you version with Git, run as scripts, and share as apps. Because marimo notebooks are stored as pure Python scripts, they are able to integrate tightly with uv. You can readily use marimo as a standalone tool, as self-contained scripts, in projects, and in non-project environments. ## Using marimo as a standalone tool For ad-hoc access to marimo notebooks, start a marimo server at any time in an isolated environment with: ```console $ uvx marimo edit ``` Start a specific notebook with: ```console $ uvx marimo edit my_notebook.py ``` ## Using marimo with inline script metadata Because marimo notebooks are stored as Python scripts, they can encapsulate their own dependencies using inline script metadata, via uv's [support for scripts](../../guides/scripts.md). For example, to add `numpy` as a dependency to your notebook, use this command: ```console $ uv add --script my_notebook.py numpy ``` To interactively edit a notebook containing inline script metadata, use: ```console $ uvx marimo edit --sandbox my_notebook.py ``` marimo will automatically use uv to start your notebook in an isolated virtual environment with your script's dependencies. Packages installed from the marimo UI will automatically be added to the notebook's script metadata. You can optionally run these notebooks as Python scripts, without opening an interactive session: ```console $ uv run my_notebook.py ``` ## Using marimo within a project If you're working within a [project](../../concepts/projects/index.md), you can start a marimo notebook with access to the project's virtual environment via the following command (assuming marimo is a project dependency): ```console $ uv run marimo edit my_notebook.py ``` To make additional packages available to your notebook, either add them to your project with `uv add`, or use marimo's built-in package installation UI, which will invoke `uv add` on your behalf. If marimo is not a project dependency, you can still run a notebook with the following command: ```console $ uv run --with marimo marimo edit my_notebook.py ``` This will let you import your project's modules while editing your notebook. However, packages installed via marimo's UI when running in this way will not be added to your project, and may disappear on subsequent marimo invocations. ## Using marimo in a non-project environment To run marimo in a virtual environment that isn't associated with a [project](../../concepts/projects/index.md), add marimo to the environment directly: ```console $ uv venv $ uv pip install numpy $ uv pip install marimo $ uv run marimo edit ``` From here, `import numpy` will work within the notebook, and marimo's UI installer will add packages to the environment with `uv pip install` on your behalf. ## Running marimo notebooks as scripts Regardless of how your dependencies are managed (with inline script metadata, within a project, or with a non-project environment), you can run marimo notebooks as scripts with: ```console $ uv run my_notebook.py ``` This executes your notebook as a Python script, without opening an interactive session in your browser. uv-0.9.17+ds1/docs/guides/integration/pre-commit.md000066400000000000000000000041731520155276700220750ustar00rootroot00000000000000--- title: Using uv with pre-commit description: A guide to using uv with pre-commit to automatically update lock files, export requirements, and compile requirements files. --- # Using uv in pre-commit An official pre-commit hook is provided at [`astral-sh/uv-pre-commit`](https://github.com/astral-sh/uv-pre-commit). To use uv with pre-commit, add one of the following examples to the `repos` list in the `.pre-commit-config.yaml`. To make sure your `uv.lock` file is up to date even if your `pyproject.toml` file was changed: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. rev: 0.9.17 hooks: - id: uv-lock ``` To keep a `requirements.txt` file in sync with your `uv.lock` file: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. rev: 0.9.17 hooks: - id: uv-export ``` To compile requirements files: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. rev: 0.9.17 hooks: # Compile requirements - id: pip-compile args: [requirements.in, -o, requirements.txt] ``` To compile alternative requirements files, modify `args` and `files`: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. rev: 0.9.17 hooks: # Compile requirements - id: pip-compile args: [requirements-dev.in, -o, requirements-dev.txt] files: ^requirements-dev\.(in|txt)$ ``` To run the hook over multiple files at the same time, add additional entries: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. rev: 0.9.17 hooks: # Compile requirements - id: pip-compile name: pip-compile requirements.in args: [requirements.in, -o, requirements.txt] - id: pip-compile name: pip-compile requirements-dev.in args: [requirements-dev.in, -o, requirements-dev.txt] files: ^requirements-dev\.(in|txt)$ ``` uv-0.9.17+ds1/docs/guides/integration/pytorch.md000066400000000000000000000334231520155276700215110ustar00rootroot00000000000000--- title: Using uv with PyTorch description: A guide to using uv with PyTorch, including installing PyTorch, configuring per-platform and per-accelerator builds, and more. --- # Using uv with PyTorch The [PyTorch](https://pytorch.org/) ecosystem is a popular choice for deep learning research and development. You can use uv to manage PyTorch projects and PyTorch dependencies across different Python versions and environments, even controlling for the choice of accelerator (e.g., CPU-only vs. CUDA). !!! note Some of the features outlined in this guide require uv version 0.5.3 or later. We recommend upgrading prior to configuring PyTorch. ## Installing PyTorch From a packaging perspective, PyTorch has a few uncommon characteristics: - Many PyTorch wheels are hosted on a dedicated index, rather than the Python Package Index (PyPI). As such, installing PyTorch often requires configuring a project to use the PyTorch index. - PyTorch produces distinct builds for each accelerator (e.g., CPU-only, CUDA). Since there's no standardized mechanism for specifying these accelerators when publishing or installing, PyTorch encodes them in the local version specifier. As such, PyTorch versions will often look like `2.5.1+cpu`, `2.5.1+cu121`, etc. - Builds for different accelerators are published to different indexes. For example, the `+cpu` builds are published on https://download.pytorch.org/whl/cpu, while the `+cu121` builds are published on https://download.pytorch.org/whl/cu121. As such, the necessary packaging configuration will vary depending on both the platforms you need to support and the accelerators you want to enable. To start, consider the following (default) configuration, which would be generated by running `uv init --python 3.14` followed by `uv add torch torchvision`. In this case, PyTorch would be installed from PyPI, which hosts CPU-only wheels for Windows and macOS, and GPU-accelerated wheels on Linux (targeting CUDA 12.8, as of PyTorch 2.9.1): ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.14" dependencies = [ "torch>=2.9.1", "torchvision>=0.24.1", ] ``` This is a valid configuration for projects that want to use CPU builds on Windows and macOS, and CUDA-enabled builds on Linux. However, if you need to support different platforms or accelerators, you'll need to configure the project accordingly. ## Using a PyTorch index In some cases, you may want to use a specific PyTorch variant across all platforms. For example, you may want to use the CPU-only builds on Linux too. In such cases, the first step is to add the relevant PyTorch index to your `pyproject.toml`: === "CPU-only" ```toml [[tool.uv.index]] name = "pytorch-cpu" url = "https://download.pytorch.org/whl/cpu" explicit = true ``` === "CUDA 11.8" ```toml [[tool.uv.index]] name = "pytorch-cu118" url = "https://download.pytorch.org/whl/cu118" explicit = true ``` === "CUDA 12.6" ```toml [[tool.uv.index]] name = "pytorch-cu126" url = "https://download.pytorch.org/whl/cu126" explicit = true ``` === "CUDA 12.8" ```toml [[tool.uv.index]] name = "pytorch-cu128" url = "https://download.pytorch.org/whl/cu128" explicit = true ``` === "CUDA 13.0" ```toml [[tool.uv.index]] name = "pytorch-cu130" url = "https://download.pytorch.org/whl/cu130" explicit = true ``` === "ROCm6" ```toml [[tool.uv.index]] name = "pytorch-rocm" url = "https://download.pytorch.org/whl/rocm6.4" explicit = true ``` === "Intel GPUs" ```toml [[tool.uv.index]] name = "pytorch-xpu" url = "https://download.pytorch.org/whl/xpu" explicit = true ``` We recommend the use of `explicit = true` to ensure that the index is _only_ used for `torch`, `torchvision`, and other PyTorch-related packages, as opposed to generic dependencies like `jinja2`, which should continue to be sourced from the default index (PyPI). Next, update the `pyproject.toml` to point `torch` and `torchvision` to the desired index: === "CPU-only" ```toml [tool.uv.sources] torch = [ { index = "pytorch-cpu" }, ] torchvision = [ { index = "pytorch-cpu" }, ] ``` === "CUDA 11.8" PyTorch doesn't publish CUDA builds for macOS. As such, we gate on `sys_platform` to instruct uv to use the PyTorch index on Linux and Windows, but fall back to PyPI on macOS: ```toml [tool.uv.sources] torch = [ { index = "pytorch-cu118", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] torchvision = [ { index = "pytorch-cu118", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] ``` === "CUDA 12.6" PyTorch doesn't publish CUDA builds for macOS. As such, we gate on `sys_platform` to instruct uv to limit the PyTorch index to Linux and Windows, falling back to PyPI on macOS: ```toml [tool.uv.sources] torch = [ { index = "pytorch-cu126", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] torchvision = [ { index = "pytorch-cu126", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] ``` === "CUDA 12.8" PyTorch doesn't publish CUDA builds for macOS. As such, we gate on `sys_platform` to instruct uv to limit the PyTorch index to Linux and Windows, falling back to PyPI on macOS: ```toml [tool.uv.sources] torch = [ { index = "pytorch-cu128", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] torchvision = [ { index = "pytorch-cu128", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] ``` === "CUDA 13.0" PyTorch doesn't publish CUDA builds for macOS. As such, we gate on `sys_platform` to instruct uv to limit the PyTorch index to Linux and Windows, falling back to PyPI on macOS: ```toml [tool.uv.sources] torch = [ { index = "pytorch-cu130", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] torchvision = [ { index = "pytorch-cu130", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] ``` === "ROCm6" PyTorch doesn't publish ROCm6 builds for macOS or Windows. As such, we gate on `sys_platform` to instruct uv to limit the PyTorch index to Linux, falling back to PyPI on macOS and Windows: ```toml [tool.uv.sources] torch = [ { index = "pytorch-rocm", marker = "sys_platform == 'linux'" }, ] torchvision = [ { index = "pytorch-rocm", marker = "sys_platform == 'linux'" }, ] # ROCm6 support relies on `pytorch-triton-rocm`, which should also be installed from the PyTorch index # (and included in `project.dependencies`). pytorch-triton-rocm = [ { index = "pytorch-rocm", marker = "sys_platform == 'linux'" }, ] ``` === "Intel GPUs" PyTorch doesn't publish Intel GPU builds for macOS. As such, we gate on `sys_platform` to instruct uv to limit the PyTorch index to Linux and Windows, falling back to PyPI on macOS: ```toml [tool.uv.sources] torch = [ { index = "pytorch-xpu", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] torchvision = [ { index = "pytorch-xpu", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] # Intel GPU support relies on `pytorch-triton-xpu`, which should also be installed from the PyTorch index # (and included in `project.dependencies`). pytorch-triton-xpu = [ { index = "pytorch-xpu", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] ``` As a complete example, the following project would use PyTorch's CPU-only builds on all platforms: ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.14.0" dependencies = [ "torch>=2.9.1", "torchvision>=0.24.1", ] [tool.uv.sources] torch = [ { index = "pytorch-cpu" }, ] torchvision = [ { index = "pytorch-cpu" }, ] [[tool.uv.index]] name = "pytorch-cpu" url = "https://download.pytorch.org/whl/cpu" explicit = true ``` ## Configuring accelerators with environment markers In some cases, you may want to use CPU-only builds in one environment (e.g., macOS and Windows), and CUDA-enabled builds in another (e.g., Linux). With `tool.uv.sources`, you can use environment markers to specify the desired index for each platform. For example, the following configuration would use PyTorch's CUDA-enabled builds on Linux, and CPU-only builds on all other platforms (e.g., macOS and Windows): ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.14.0" dependencies = [ "torch>=2.9.1", "torchvision>=0.24.1", ] [tool.uv.sources] torch = [ { index = "pytorch-cpu", marker = "sys_platform != 'linux'" }, { index = "pytorch-cu128", marker = "sys_platform == 'linux'" }, ] torchvision = [ { index = "pytorch-cpu", marker = "sys_platform != 'linux'" }, { index = "pytorch-cu128", marker = "sys_platform == 'linux'" }, ] [[tool.uv.index]] name = "pytorch-cpu" url = "https://download.pytorch.org/whl/cpu" explicit = true [[tool.uv.index]] name = "pytorch-cu128" url = "https://download.pytorch.org/whl/cu128" explicit = true ``` Similarly, the following configuration would use PyTorch's AMD GPU builds on Linux, and CPU-only builds on Windows and macOS (by way of falling back to PyPI): ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.14.0" dependencies = [ "torch>=2.9.1", "torchvision>=0.24.1", "pytorch-triton-rocm>=3.5.1 ; sys_platform == 'linux'", ] [tool.uv.sources] torch = [ { index = "pytorch-rocm", marker = "sys_platform == 'linux'" }, ] torchvision = [ { index = "pytorch-rocm", marker = "sys_platform == 'linux'" }, ] pytorch-triton-rocm = [ { index = "pytorch-rocm", marker = "sys_platform == 'linux'" }, ] [[tool.uv.index]] name = "pytorch-rocm" url = "https://download.pytorch.org/whl/rocm6.4" explicit = true ``` Or, for Intel GPU builds: ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.14.0" dependencies = [ "torch>=2.9.1", "torchvision>=0.24.1", "pytorch-triton-xpu>=3.5.0 ; sys_platform == 'win32' or sys_platform == 'linux'", ] [tool.uv.sources] torch = [ { index = "pytorch-xpu", marker = "sys_platform == 'win32' or sys_platform == 'linux'" }, ] torchvision = [ { index = "pytorch-xpu", marker = "sys_platform == 'win32' or sys_platform == 'linux'" }, ] pytorch-triton-xpu = [ { index = "pytorch-xpu", marker = "sys_platform == 'win32' or sys_platform == 'linux'" }, ] [[tool.uv.index]] name = "pytorch-xpu" url = "https://download.pytorch.org/whl/xpu" explicit = true ``` ## Configuring accelerators with optional dependencies In some cases, you may want to use CPU-only builds in some cases, but CUDA-enabled builds in others, with the choice toggled by a user-provided extra (e.g., `uv sync --extra cpu` vs. `uv sync --extra cu128`). With `tool.uv.sources`, you can use extra markers to specify the desired index for each enabled extra. For example, the following configuration would use PyTorch's CPU-only for `uv sync --extra cpu` and CUDA-enabled builds for `uv sync --extra cu128`: ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.14.0" dependencies = [] [project.optional-dependencies] cpu = [ "torch>=2.9.1", "torchvision>=0.24.1", ] cu128 = [ "torch>=2.9.1", "torchvision>=0.24.1", ] [tool.uv] conflicts = [ [ { extra = "cpu" }, { extra = "cu128" }, ], ] [tool.uv.sources] torch = [ { index = "pytorch-cpu", extra = "cpu" }, { index = "pytorch-cu128", extra = "cu128" }, ] torchvision = [ { index = "pytorch-cpu", extra = "cpu" }, { index = "pytorch-cu128", extra = "cu128" }, ] [[tool.uv.index]] name = "pytorch-cpu" url = "https://download.pytorch.org/whl/cpu" explicit = true [[tool.uv.index]] name = "pytorch-cu128" url = "https://download.pytorch.org/whl/cu128" explicit = true ``` !!! note Since GPU-accelerated builds aren't available on macOS, the above configuration will fail to install on macOS when the `cu128` extra is enabled. ## The `uv pip` interface While the above examples are focused on uv's project interface (`uv lock`, `uv sync`, `uv run`, etc.), PyTorch can also be installed via the `uv pip` interface. PyTorch itself offers a [dedicated interface](https://pytorch.org/get-started/locally/) to determine the appropriate pip command to run for a given target configuration. For example, you can install stable, CPU-only PyTorch on Linux with: ```shell $ pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu ``` To use the same workflow with uv, replace `pip3` with `uv pip`: ```shell $ uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu ``` ## Automatic backend selection uv supports automatic selection of the appropriate PyTorch index via the `--torch-backend=auto` command-line argument (or the `UV_TORCH_BACKEND=auto` environment variable), as in: ```shell $ # With a command-line argument. $ uv pip install torch --torch-backend=auto $ # With an environment variable. $ UV_TORCH_BACKEND=auto uv pip install torch ``` When enabled, uv will query for the installed CUDA driver, AMD GPU versions, and Intel GPU presence, then use the most-compatible PyTorch index for all relevant packages (e.g., `torch`, `torchvision`, etc.). If no such GPU is found, uv will fall back to the CPU-only index. uv will continue to respect existing index configuration for any packages outside the PyTorch ecosystem. You can also select a specific backend (e.g., CUDA 12.8) with `--torch-backend=cu126` (or `UV_TORCH_BACKEND=cu126`): ```shell $ # With a command-line argument. $ uv pip install torch torchvision --torch-backend=cu126 $ # With an environment variable. $ UV_TORCH_BACKEND=cu128 uv pip install torch torchvision ``` At present, `--torch-backend` is only available in the `uv pip` interface. uv-0.9.17+ds1/docs/guides/migration/000077500000000000000000000000001520155276700171405ustar00rootroot00000000000000uv-0.9.17+ds1/docs/guides/migration/index.md000066400000000000000000000006761520155276700206020ustar00rootroot00000000000000# Migration guides Learn how to migrate from other tools to uv: - [Migrate from pip to uv projects](./pip-to-project.md) !!! note Other guides, such as migrating from another project management tool, or from pip to `uv pip` are not yet available. See [#5200](https://github.com/astral-sh/uv/issues/5200) to track progress. Or, explore the [integration guides](../integration/index.md) to learn how to use uv with other software. uv-0.9.17+ds1/docs/guides/migration/pip-to-project.md000066400000000000000000000347571520155276700223560ustar00rootroot00000000000000# Migrating from pip to a uv project This guide will discuss converting from a `pip` and `pip-tools` workflow centered on `requirements` files to uv's project workflow using a `pyproject.toml` and `uv.lock` file. !!! note If you're looking to migrate from `pip` and `pip-tools` to uv's drop-in interface or from an existing workflow where you're already using a `pyproject.toml`, those guides are not yet written. See [#5200](https://github.com/astral-sh/uv/issues/5200) to track progress. We'll start with an overview of developing with `pip`, then discuss migrating to uv. !!! tip If you're familiar with the ecosystem, you can jump ahead to the [requirements file import](#importing-requirements-files) instructions. ## Understanding pip workflows ### Project dependencies When you want to use a package in your project, you need to install it first. `pip` supports imperative installation of packages, e.g.: ```console $ pip install fastapi ``` This installs the package into the environment that `pip` is installed in. This may be a virtual environment, or, the global environment of your system's Python installation. Then, you can run a Python script that requires the package: ```python title="example.py" import fastapi ``` It's best practice to create a virtual environment for each project, to avoid mixing packages between them. For example: ```console $ python -m venv $ source .venv/bin/activate $ pip ... ``` We will revisit this topic in the [project environments section](#project-environments) below. ### Requirements files When sharing projects with others, it's useful to declare all the packages you require upfront. `pip` supports installing requirements from a file, e.g.: ```python title="requirements.txt" fastapi ``` ```console $ pip install -r requirements.txt ``` Notice above that `fastapi` is not "locked" to a specific version — each person working on the project may have a different version of `fastapi` installed. `pip-tools` was created to improve this experience. When using `pip-tools`, requirements files specify both the dependencies for your project and lock dependencies to a specific version — the file extension is used to differentiate between the two. For example, if you require `fastapi` and `pydantic`, you'd specify these in a `requirements.in` file: ```python title="requirements.in" fastapi pydantic>2 ``` Notice there's a version constraint on `pydantic` — this means only `pydantic` versions later than `2.0.0` can be used. In contrast, `fastapi` does not have a version constraint — any version can be used. These dependencies can be compiled into a `requirements.txt` file: ```console $ pip-compile requirements.in -o requirements.txt ``` ```python title="requirements.txt" annotated-types==0.7.0 # via pydantic anyio==4.8.0 # via starlette fastapi==0.115.11 # via -r requirements.in idna==3.10 # via anyio pydantic==2.10.6 # via # -r requirements.in # fastapi pydantic-core==2.27.2 # via pydantic sniffio==1.3.1 # via anyio starlette==0.46.1 # via fastapi typing-extensions==4.12.2 # via # fastapi # pydantic # pydantic-core ``` Here, all the versions constraints are _exact_. Only a single version of each package can be used. The above example was generated with `uv pip compile`, but could also be generated with `pip-compile` from `pip-tools`. Though less common, the `requirements.txt` can also be generated using `pip freeze`, by first installing the input dependencies into the environment then exporting the installed versions: ```console $ pip install -r requirements.in $ pip freeze > requirements.txt ``` ```python title="requirements.txt" annotated-types==0.7.0 anyio==4.8.0 fastapi==0.115.11 idna==3.10 pydantic==2.10.6 pydantic-core==2.27.2 sniffio==1.3.1 starlette==0.46.1 typing-extensions==4.12.2 ``` After compiling dependencies into a locked set of versions, these files are committed to version control and distributed with the project. Then, when someone wants to use the project, they install from the requirements file: ```console $ pip install -r requirements.txt ``` ### Development dependencies The requirements file format can only describe a single set of dependencies at once. This means if you have additional _groups_ of dependencies, such as development dependencies, they need separate files. For example, we'll create a `-dev` dependency file: ```python title="requirements-dev.in" -r requirements.in -c requirements.txt pytest ``` Notice the base requirements are included with `-r requirements.in`. This ensures your development environment considers _all_ of the dependencies together. The `-c requirements.txt` _constrains_ the package version to ensure that the `requirements-dev.txt` uses the same versions as `requirements.txt`. !!! note It's common to use `-r requirements.txt` directly instead of using both `-r requirements.in`, and `-c requirements.txt`. There's no difference in the resulting package versions, but using both files produces annotations which allow you to determine which dependencies are _direct_ (annotated with `-r requirements.in`) and which are _indirect_ (only annotated with `-c requirements.txt`). The compiled development dependencies look like: ```python title="requirements-dev.txt" annotated-types==0.7.0 # via # -c requirements.txt # pydantic anyio==4.8.0 # via # -c requirements.txt # starlette fastapi==0.115.11 # via # -c requirements.txt # -r requirements.in idna==3.10 # via # -c requirements.txt # anyio iniconfig==2.0.0 # via pytest packaging==24.2 # via pytest pluggy==1.5.0 # via pytest pydantic==2.10.6 # via # -c requirements.txt # -r requirements.in # fastapi pydantic-core==2.27.2 # via # -c requirements.txt # pydantic pytest==8.3.5 # via -r requirements-dev.in sniffio==1.3.1 # via # -c requirements.txt # anyio starlette==0.46.1 # via # -c requirements.txt # fastapi typing-extensions==4.12.2 # via # -c requirements.txt # fastapi # pydantic # pydantic-core ``` As with the base dependency files, these are committed to version control and distributed with the project. When someone wants to work on the project, they'll install from the requirements file: ```console $ pip install -r requirements-dev.txt ``` ### Platform-specific dependencies When compiling dependencies with `pip` or `pip-tools`, the result is only usable on the same platform as it is generated on. This poses a problem for projects which need to be usable on multiple platforms, such as Windows and macOS. For example, take a simple dependency: ```python title="requirements.in" tqdm ``` On Linux, this compiles to: ```python title="requirements-linux.txt" tqdm==4.67.1 # via -r requirements.in ``` While on Windows, this compiles to: ```python title="requirements-win.txt" colorama==0.4.6 # via tqdm tqdm==4.67.1 # via -r requirements.in ``` `colorama` is a Windows-only dependency of `tqdm`. When using `pip` and `pip-tools`, a project needs to declare a requirements lock file for each supported platform. !!! note uv's resolver can compile dependencies for multiple platforms at once (see ["universal resolution"](../../concepts/resolution.md#universal-resolution)), allowing you to use a single `requirements.txt` for all platforms: ```console $ uv pip compile --universal requirements.in ``` ```python title="requirements.txt" colorama==0.4.6 ; sys_platform == 'win32' # via tqdm tqdm==4.67.1 # via -r requirements.in ``` This resolution mode is also used when using a `pyproject.toml` and `uv.lock`. ## Migrating to a uv project ### The `pyproject.toml` The `pyproject.toml` is a standardized file for Python project metadata. It replaces `requirements.in` files, allowing you to represent arbitrary groups of project dependencies. It also provides a centralized location for metadata about your project, such as the build system or tool settings. For example, the `requirements.in` and `requirements-dev.in` files above can be translated to a `pyproject.toml` as follows: ```toml title="pyproject.toml" [project] name = "example" version = "0.0.1" dependencies = [ "fastapi", "pydantic>2" ] [dependency-groups] dev = ["pytest"] ``` We'll discuss the commands necessary to automate these imports below. ### The uv lockfile uv uses a lockfile (`uv.lock`) file to lock package versions. The format of this file is specific to uv, allowing uv to support advanced features. It replaces `requirements.txt` files. The lockfile will be automatically created and populated when adding dependencies, but you can explicitly create it with `uv lock`. Unlike `requirements.txt` files, the `uv.lock` file can represent arbitrary groups of dependencies, so multiple files are not needed to lock development dependencies. The uv lockfile is always [universal](../../concepts/resolution.md#universal-resolution), so multiple files are not needed to [lock dependencies for each platform](#platform-specific-dependencies). This ensures that all developers are using consistent, locked versions of dependencies regardless of their machine. The uv lockfile also supports concepts like [pinning packages to specific indexes](../../concepts/indexes.md#pinning-a-package-to-an-index), which is not representable in `requirements.txt` files. !!! tip If you only need to lock for a subset of platforms, use the [`tool.uv.environments`](../../concepts/resolution.md#limited-resolution-environments) setting to limit the resolution and lockfile. To learn more, see the [lockfile](../../concepts/projects/layout.md#the-lockfile) documentation. ### Importing requirements files First, create a `pyproject.toml` if you have not already: ```console $ uv init ``` Then, the easiest way to import requirements is with `uv add`: ```console $ uv add -r requirements.in ``` However, there is some nuance to this transition. Notice we used the `requirements.in` file, which does not pin to exact versions of packages so uv will solve for new versions of these packages. You may want to continue using your previously locked versions from your `requirements.txt` so, when switching over to uv, none of your dependency versions change. The solution is to add your locked versions as _constraints_. uv supports using these on `add` to preserve locked versions: ```console $ uv add -r requirements.in -c requirements.txt ``` Your existing versions will be retained when producing a `uv.lock` file. #### Importing platform-specific constraints If your platform-specific dependencies have been compiled into separate files, you can still transition to a universal lockfile. However, you cannot just use `-c` to specify constraints from your existing platform-specific `requirements.txt` files because they do not include markers describing the environment and will consequently conflict. To add the necessary markers, use `uv pip compile` to convert your existing files. For example, given the following: ```python title="requirements-win.txt" colorama==0.4.6 # via tqdm tqdm==4.67.1 # via -r requirements.in ``` The markers can be added with: ```console $ uv pip compile requirements.in -o requirements-win.txt --python-platform windows --no-strip-markers ``` Notice the resulting output includes a Windows marker on `colorama`: ```python title="requirements-win.txt" colorama==0.4.6 ; sys_platform == 'win32' # via tqdm tqdm==4.67.1 # via -r requirements.in ``` When using `-o`, uv will constrain the versions to match the existing output file, if it can. Markers can be added for other platforms by changing the `--python-platform` and `-o` values for each requirements file you need to import, e.g., to `linux` and `macos`. Once each `requirements.txt` file has been transformed, the dependencies can be imported to the `pyproject.toml` and `uv.lock` with `uv add`: ```console $ uv add -r requirements.in -c requirements-win.txt -c requirements-linux.txt ``` #### Importing development dependency files As discussed in the [development dependencies](#development-dependencies) section, it's common to have groups of dependencies for development purposes. To import development dependencies, use the `--dev` flag during `uv add`: ```console $ uv add --dev -r requirements-dev.in -c requirements-dev.txt ``` If the `requirements-dev.in` includes the parent `requirements.in` via `-r`, it will need to be stripped to avoid adding the base requirements to the `dev` dependency group. The following example uses `sed` to strip lines that start with `-r`, then pipes the result to `uv add`: ```console $ sed '/^-r /d' requirements-dev.in | uv add --dev -r - -c requirements-dev.txt ``` In addition to the `dev` dependency group, uv supports arbitrary group names. For example, if you also have a dedicated set of dependencies for building your documentation, those can be imported to a `docs` group: ```console $ uv add -r requirements-docs.in -c requirements-docs.txt --group docs ``` ### Project environments Unlike `pip`, uv is not centered around the concept of an "active" virtual environment. Instead, uv uses a dedicated virtual environment for each project in a `.venv` directory. This environment is automatically managed, so when you run a command, like `uv add`, the environment is synced with the project dependencies. The preferred way to execute commands in the environment is with `uv run`, e.g.: ```console $ uv run pytest ``` Prior to every `uv run` invocation, uv will verify that the lockfile is up-to-date with the `pyproject.toml`, and that the environment is up-to-date with the lockfile, keeping your project in-sync without the need for manual intervention. `uv run` guarantees that your command is run in a consistent, locked environment. The project environment can also be explicitly created with `uv sync`, e.g., for use with editors. !!! note When in projects, uv will prefer a `.venv` in the project directory and ignore the active environment as declared by the `VIRTUAL_ENV` variable by default. You can opt-in to using the active environment with the `--active` flag. To learn more, see the [project environment](../../concepts/projects/layout.md#the-project-environment) documentation. ## Next steps Now that you've migrated to uv, take a look at the [project concept](../../concepts/projects/index.md) page for more details about uv projects. uv-0.9.17+ds1/docs/guides/package.md000066400000000000000000000166011520155276700170700ustar00rootroot00000000000000--- title: Building and publishing a package description: A guide to using uv to build and publish Python packages to a package index, like PyPI. --- # Building and publishing a package uv supports building Python packages into source and binary distributions via `uv build` and uploading them to a registry with `uv publish`. ## Preparing your project Before attempting to publish your project, you'll want to make sure it's ready to be packaged for distribution. If your project does not include a `[build-system]` definition in the `pyproject.toml`, uv will not build it during `uv sync` operations in the project, but will fall back to the legacy setuptools build system during `uv build`. We strongly recommend configuring a build system. Read more about build systems in the [project configuration](../concepts/projects/config.md#build-systems) documentation. ## Building your package Build your package with `uv build`: ```console $ uv build ``` By default, `uv build` will build the project in the current directory, and place the built artifacts in a `dist/` subdirectory. Alternatively, `uv build ` will build the package in the specified directory, while `uv build --package ` will build the specified package within the current workspace. !!! info By default, `uv build` respects `tool.uv.sources` when resolving build dependencies from the `build-system.requires` section of the `pyproject.toml`. When publishing a package, we recommend running `uv build --no-sources` to ensure that the package builds correctly when `tool.uv.sources` is disabled, as is the case when using other build tools, like [`pypa/build`](https://github.com/pypa/build). ## Updating your version The `uv version` command provides conveniences for updating the version of your package before you publish it. [See the project docs for reading your package's version](./projects.md#viewing-your-version). To update to an exact version, provide it as a positional argument: ```console $ uv version 1.0.0 hello-world 0.7.0 => 1.0.0 ``` To preview the change without updating the `pyproject.toml`, use the `--dry-run` flag: ```console $ uv version 2.0.0 --dry-run hello-world 1.0.0 => 2.0.0 $ uv version hello-world 1.0.0 ``` To increase the version of your package semantics, use the `--bump` option: ```console $ uv version --bump minor hello-world 1.2.3 => 1.3.0 ``` The `--bump` option supports the following common version components: `major`, `minor`, `patch`, `stable`, `alpha`, `beta`, `rc`, `post`, and `dev`. When provided more than once, the components will be applied in order, from largest (`major`) to smallest (`dev`). You can optionally provide a numeric value with `--bump =` to set the resulting component explicitly: ```console $ uv version --bump patch --bump dev=66463664 hello-world 0.0.1 => 0.0.2.dev66463664 ``` To move from a stable to pre-release version, bump one of the major, minor, or patch components in addition to the pre-release component: ```console $ uv version --bump patch --bump beta hello-world 1.3.0 => 1.3.1b1 $ uv version --bump major --bump alpha hello-world 1.3.0 => 2.0.0a1 ``` When moving from a pre-release to a new pre-release version, just bump the relevant pre-release component: ```console $ uv version --bump beta hello-world 1.3.0b1 => 1.3.0b2 ``` When moving from a pre-release to a stable version, the `stable` option can be used to clear the pre-release component: ```console $ uv version --bump stable hello-world 1.3.1b2 => 1.3.1 ``` !!! info By default, when `uv version` modifies the project it will perform a lock and sync. To prevent locking and syncing, use `--frozen`, or, to just prevent syncing, use `--no-sync`. ## Publishing your package !!! note A complete guide to publishing from GitHub Actions to PyPI can be found in the [GitHub Guide](integration/github.md#publishing-to-pypi) Publish your package with `uv publish`: ```console $ uv publish ``` Set a PyPI token with `--token` or `UV_PUBLISH_TOKEN`, or set a username with `--username` or `UV_PUBLISH_USERNAME` and password with `--password` or `UV_PUBLISH_PASSWORD`. For publishing to PyPI from GitHub Actions or another Trusted Publisher, you don't need to set any credentials. Instead, [add a trusted publisher to the PyPI project](https://docs.pypi.org/trusted-publishers/adding-a-publisher/). !!! note PyPI does not support publishing with username and password anymore, instead you need to generate a token. Using a token is equivalent to setting `--username __token__` and using the token as password. If you're using a custom index through `[[tool.uv.index]]`, add `publish-url` and use `uv publish --index `. For example: ```toml [[tool.uv.index]] name = "testpypi" url = "https://test.pypi.org/simple/" publish-url = "https://test.pypi.org/legacy/" explicit = true ``` !!! note When using `uv publish --index `, the `pyproject.toml` must be present, i.e., you need to have a checkout step in a publish CI job. Even though `uv publish` retries failed uploads, it can happen that publishing fails in the middle, with some files uploaded and some files still missing. With PyPI, you can retry the exact same command, existing identical files will be ignored. With other registries, use `--check-url ` with the index URL (not the publishing URL) the packages belong to. When using `--index`, the index URL is used as check URL. uv will skip uploading files that are identical to files in the registry, and it will also handle raced parallel uploads. Note that existing files need to match exactly with those previously uploaded to the registry, this avoids accidentally publishing source distribution and wheels with different contents for the same version. ### Uploading attestations with your package !!! note Some third-party package indexes may not support attestations, and may reject uploads that include them (rather than silently ignoring them). If you encounter issues when uploading, you can use `--no-attestations` or `UV_PUBLISH_NO_ATTESTATIONS` to disable uv's default behavior. !!! tip `uv publish` does not currently generate attestations; attestations must be created separately before publishing. `uv publish` supports uploading [attestations](https://peps.python.org/pep-0740/) to registries that support them, like PyPI. uv will automatically discover and match attestations. For example, given the following `dist/` directory, `uv publish` will upload the attestations along with their corresponding distributions: ```console $ ls dist/ hello_world-1.0.0-py3-none-any.whl hello_world-1.0.0-py3-none-any.whl.publish.attestation hello_world-1.0.0.tar.gz hello_world-1.0.0.tar.gz.publish.attestation ``` ## Installing your package Test that the package can be installed and imported with `uv run`: ```console $ uv run --with --no-project -- python -c "import " ``` The `--no-project` flag is used to avoid installing the package from your local project directory. !!! tip If you have recently installed the package, you may need to include the `--refresh-package ` option to avoid using a cached version of the package. ## Next steps To learn more about publishing packages, check out the [PyPA guides](https://packaging.python.org/en/latest/guides/section-build-and-publish/) on building and publishing. Or, read on for [guides](./integration/index.md) on integrating uv with other software. uv-0.9.17+ds1/docs/guides/projects.md000066400000000000000000000163161520155276700173310ustar00rootroot00000000000000--- title: Working on projects description: A guide to using uv to create and manage Python projects, including adding dependencies, running commands, and building publishable distributions. --- # Working on projects uv supports managing Python projects, which define their dependencies in a `pyproject.toml` file. ## Creating a new project You can create a new Python project using the `uv init` command: ```console $ uv init hello-world $ cd hello-world ``` Alternatively, you can initialize a project in the working directory: ```console $ mkdir hello-world $ cd hello-world $ uv init ``` uv will create the following files: ```text ├── .gitignore ├── .python-version ├── README.md ├── main.py └── pyproject.toml ``` The `main.py` file contains a simple "Hello world" program. Try it out with `uv run`: ```console $ uv run main.py Hello from hello-world! ``` ## Project structure A project consists of a few important parts that work together and allow uv to manage your project. In addition to the files created by `uv init`, uv will create a virtual environment and `uv.lock` file in the root of your project the first time you run a project command, i.e., `uv run`, `uv sync`, or `uv lock`. A complete listing would look like: ```text . ├── .venv │   ├── bin │   ├── lib │   └── pyvenv.cfg ├── .python-version ├── README.md ├── main.py ├── pyproject.toml └── uv.lock ``` ### `pyproject.toml` The `pyproject.toml` contains metadata about your project: ```toml title="pyproject.toml" [project] name = "hello-world" version = "0.1.0" description = "Add your description here" readme = "README.md" dependencies = [] ``` You'll use this file to specify dependencies, as well as details about the project such as its description or license. You can edit this file manually, or use commands like `uv add` and `uv remove` to manage your project from the terminal. !!! tip See the official [`pyproject.toml` guide](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/) for more details on getting started with the `pyproject.toml` format. You'll also use this file to specify uv [configuration options](../concepts/configuration-files.md) in a [`[tool.uv]`](../reference/settings.md) section. ### `.python-version` The `.python-version` file contains the project's default Python version. This file tells uv which Python version to use when creating the project's virtual environment. ### `.venv` The `.venv` folder contains your project's virtual environment, a Python environment that is isolated from the rest of your system. This is where uv will install your project's dependencies. See the [project environment](../concepts/projects/layout.md#the-project-environment) documentation for more details. ### `uv.lock` `uv.lock` is a cross-platform lockfile that contains exact information about your project's dependencies. Unlike the `pyproject.toml` which is used to specify the broad requirements of your project, the lockfile contains the exact resolved versions that are installed in the project environment. This file should be checked into version control, allowing for consistent and reproducible installations across machines. `uv.lock` is a human-readable TOML file but is managed by uv and should not be edited manually. See the [lockfile](../concepts/projects/layout.md#the-lockfile) documentation for more details. ## Managing dependencies You can add dependencies to your `pyproject.toml` with the `uv add` command. This will also update the lockfile and project environment: ```console $ uv add requests ``` You can also specify version constraints or alternative sources: ```console $ # Specify a version constraint $ uv add 'requests==2.31.0' $ # Add a git dependency $ uv add git+https://github.com/psf/requests ``` If you're migrating from a `requirements.txt` file, you can use `uv add` with the `-r` flag to add all dependencies from the file: ```console $ # Add all dependencies from `requirements.txt`. $ uv add -r requirements.txt -c constraints.txt ``` To remove a package, you can use `uv remove`: ```console $ uv remove requests ``` To upgrade a package, run `uv lock` with the `--upgrade-package` flag: ```console $ uv lock --upgrade-package requests ``` The `--upgrade-package` flag will attempt to update the specified package to the latest compatible version, while keeping the rest of the lockfile intact. See the documentation on [managing dependencies](../concepts/projects/dependencies.md) for more details. ## Viewing your version The `uv version` command can be used to read your package's version. To get the version of your package, run `uv version`: ```console $ uv version hello-world 0.7.0 ``` To get the version without the package name, use the `--short` option: ```console $ uv version --short 0.7.0 ``` To get version information in a JSON format, use the `--output-format json` option: ```console $ uv version --output-format json { "package_name": "hello-world", "version": "0.7.0", "commit_info": null } ``` See the [publishing guide](./package.md#updating-your-version) for details on updating your package version. ## Running commands `uv run` can be used to run arbitrary scripts or commands in your project environment. Prior to every `uv run` invocation, uv will verify that the lockfile is up-to-date with the `pyproject.toml`, and that the environment is up-to-date with the lockfile, keeping your project in-sync without the need for manual intervention. `uv run` guarantees that your command is run in a consistent, locked environment. For example, to use `flask`: ```console $ uv add flask $ uv run -- flask run -p 3000 ``` Or, to run a script: ```python title="example.py" # Require a project dependency import flask print("hello world") ``` ```console $ uv run example.py ``` Alternatively, you can use `uv sync` to manually update the environment then activate it before executing a command: === "macOS and Linux" ```console $ uv sync $ source .venv/bin/activate $ flask run -p 3000 $ python example.py ``` === "Windows" ```pwsh-session PS> uv sync PS> .venv\Scripts\activate PS> flask run -p 3000 PS> python example.py ``` !!! note The virtual environment must be active to run scripts and commands in the project without `uv run`. Virtual environment activation differs per shell and platform. See the documentation on [running commands and scripts](../concepts/projects/run.md) in projects for more details. ## Building distributions `uv build` can be used to build source distributions and binary distributions (wheel) for your project. By default, `uv build` will build the project in the current directory, and place the built artifacts in a `dist/` subdirectory: ```console $ uv build $ ls dist/ hello-world-0.1.0-py3-none-any.whl hello-world-0.1.0.tar.gz ``` See the documentation on [building projects](../concepts/projects/build.md) for more details. ## Next steps To learn more about working on projects with uv, see the [projects concept](../concepts/projects/index.md) page and the [command reference](../reference/cli.md#uv). Or, read on to learn how to [export a uv lockfile to different formats](../concepts/projects/export.md). uv-0.9.17+ds1/docs/guides/scripts.md000066400000000000000000000252611520155276700171660ustar00rootroot00000000000000--- title: Running scripts description: A guide to using uv to run Python scripts, including support for inline dependency metadata, reproducible scripts, and more. --- # Running scripts A Python script is a file intended for standalone execution, e.g., with `python