diff --git a/.github/actions/setup-appium-server/action.yml b/.github/actions/setup-appium-server/action.yml new file mode 100644 index 00000000..a9177d4e --- /dev/null +++ b/.github/actions/setup-appium-server/action.yml @@ -0,0 +1,65 @@ +name: 'Start Appium Server' +description: 'Start Appium server with configurable arguments and wait for startup' + +inputs: + port: + description: 'Appium server port' + required: false + default: '4723' + host: + description: 'Appium server host' + required: false + default: '127.0.0.1' + timeout: + description: 'Timeout in seconds to wait for server startup' + required: false + default: '25' + server_args: + description: 'Additional server arguments (space-separated)' + required: false + default: '' + log_file: + description: 'Log file name' + required: false + default: 'appium.log' + +runs: + using: 'composite' + steps: + - name: Start Appium server + shell: bash + run: | + nohup appium server \ + --port=${{ inputs.port }} \ + --address=${{ inputs.host }} \ + --log-no-colors \ + --log-timestamp \ + --keep-alive-timeout 1200 \ + ${{ inputs.server_args }} \ + 2>&1 > ${{ inputs.log_file }} & + + - name: Wait for Appium server to start + shell: bash + run: | + TIMEOUT_SEC=${{ inputs.timeout }} + INTERVAL_SEC=1 + + start_time=$(date +%s) + while true; do + current_time=$(date +%s) + elapsed=$((current_time - start_time)) + + if nc -z ${{ inputs.host }} ${{ inputs.port }}; then + echo "Appium server is running after $elapsed seconds" + cat ${{ inputs.log_file }} + exit 0 + fi + + if [[ "$elapsed" -ge "$TIMEOUT_SEC" ]]; then + echo "${elapsed} seconds timeout reached: Appium server is NOT running" + exit 1 + fi + + echo "Waiting $elapsed seconds for Appium server to start..." + sleep "$INTERVAL_SEC" + done diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..140328bc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: +- package-ecosystem: uv + directory: "/" + schedule: + interval: daily + time: "11:00" + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + time: "11:00" + open-pull-requests-limit: 10 + commit-message: + prefix: "ci" + include: "scope" diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 00000000..6251abfd --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,24 @@ +## The problem + +Briefly describe the issue you are experiencing (or the feature you want to see added to Appium). Tell us what you were trying to do and what happened instead. Remember, this is _not_ a place to ask questions. For that, go to http://discuss.appium.io! + + +## Environment +- Appium version (or git revision) that exhibits the issue: +- Last Appium version that did not exhibit the issue (if applicable): +- Desktop OS/version used to run Appium: +- Node.js version (unless using Appium.app|exe): +- Mobile platform/version under test: +- Real device or emulator/simulator: +- Appium CLI or Appium.app|exe: + +## Details + +If necessary, describe the problem you have been experiencing in more detail. + +## Link to Appium Logs + +Create a [GIST](https://gist.github.com) which is a paste of your _full_ Appium logs, and link them here. + +## Code To reproduce issue + diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml new file mode 100644 index 00000000..c0b7f1d8 --- /dev/null +++ b/.github/workflows/functional-test.yml @@ -0,0 +1,368 @@ +name: Functional Tests + +on: + # Run by manual at this time + workflow_dispatch: + pull_request: + branches: [ master ] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + APPIUM_TEST_SERVER_PORT: '4723' + APPIUM_TEST_SERVER_HOST: '127.0.0.1' + PYTHONUNBUFFERED: 1 + +jobs: + ios_test: + strategy: + fail-fast: false + matrix: + test_targets: + - target: test/functional/ios/safari_tests.py + name: func_test_ios1 + + runs-on: macos-15 + + # Please make sure the available Xcode versions and iOS versions + # on the runner images. https://github.com/actions/runner-images + env: + XCODE_VERSION: '16.4' + IOS_VERSION: '18.5' + IPHONE_MODEL: 'iPhone 16 Plus' + PREBUILT_WDA_PATH: ${{ github.workspace }}/wda/WebDriverAgentRunner-Runner.app + + steps: + - uses: actions/checkout@v6 + + - name: Install Node.js + uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + + - name: Select Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + + - name: Install Appium and drivers + run: | + npm install -g appium + appium driver install xcuitest + appium driver run xcuitest download-wda-sim --platform=ios --outdir=$(dirname "$PREBUILT_WDA_PATH") + + - name: Start Appium server + uses: ./.github/actions/setup-appium-server + with: + port: ${{ env.APPIUM_TEST_SERVER_PORT }} + host: ${{ env.APPIUM_TEST_SERVER_HOST }} + server_args: '--relaxed-security' + + - name: Start iOS Simulator UI + run: | + defaults write com.apple.iphonesimulator PasteboardAutomaticSync -bool false + open -Fn "$(xcode-select --print-path)/Applications/Simulator.app" + - name: Prepare iOS simulator + uses: futureware-tech/simulator-action@v4 + with: + model: ${{ env.IPHONE_MODEL }} + os_version: ${{ env.IOS_VERSION }} + wait_for_boot: true + shutdown_after_job: false + + - name: Finalize iOS simulator boot + run: xcrun --sdk iphonesimulator --show-sdk-version + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: 3.14 + + - name: Cache uv modules + uses: actions/cache@v5 + with: + path: | + ~/.cache/uv + .venv + key: ${{ runner.os }}-uv-shared-${{ hashFiles('**/uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv-shared- + + - name: Install uv + run: make install-uv + + - name: Run Tests + run: | + uv run pytest -v ${{ matrix.test_targets.target}} \ + --doctest-modules \ + --junitxml=junit/test-results.xml \ + --cov=com \ + --cov-report=xml \ + --cov-report=html + env: + LOCAL_PREBUILT_WDA: ${{ env.PREBUILT_WDA_PATH }} + + - name: Save server output + if: ${{ always() }} + uses: actions/upload-artifact@master + with: + name: appium-ios-${{matrix.test_targets.name}}.log + path: appium.log + + + android_test: + strategy: + fail-fast: false + matrix: + test_targets: + - target: test/functional/android/appium_service_tests.py test/functional/android/chrome_tests.py test/functional/android/bidi_tests.py + name: func_test_android1 + + runs-on: ubuntu-latest + + env: + API_LEVEL: 29 + ARCH: x86 + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' + + - name: Install ffmpeg + run: | + sudo apt-get update + sudo apt-get install -y ffmpeg + + - name: Install Node.js + uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + + - name: Install Appium and drivers + run: | + npm install -g appium + appium driver install uiautomator2 + appium driver install espresso + appium plugin install execute-driver + + - name: Start Appium server + uses: ./.github/actions/setup-appium-server + with: + port: ${{ env.APPIUM_TEST_SERVER_PORT }} + host: ${{ env.APPIUM_TEST_SERVER_HOST }} + server_args: '--relaxed-security --use-plugins=execute-driver' + + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: AVD cache + uses: actions/cache@v5 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ env.API_LEVEL }} + - name: Create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ env.API_LEVEL }} + arch: ${{ env.ARCH }} + target: google_apis + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + script: echo "Generated AVD snapshot for caching." + + - name: Set up Python 3.14 + uses: actions/setup-python@v6 + with: + python-version: 3.14 + + - name: Cache uv modules + uses: actions/cache@v5 + with: + path: | + ~/.cache/uv + .venv + key: ${{ runner.os }}-uv-shared-${{ hashFiles('**/uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv-shared- + + - name: Run tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ env.API_LEVEL }} + arch: ${{ env.ARCH }} + script: | + make install-uv + uv run pytest -v ${{ matrix.test_targets.target}} --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html + target: google_apis + profile: Nexus 5X + disable-spellchecker: true + disable-animations: true + + env: + ANDROID_SDK_VERSION: ${{ env.API_LEVEL }} + APPIUM_DRIVER: ${{matrix.test_targets.automation_name}} + IGNORE_VERSION_SKIP: true + CI: true + + - name: Save server output + if: ${{ always() }} + uses: actions/upload-artifact@master + with: + name: appium-android-${{matrix.test_targets.name}}.log + path: appium.log + + flutter_e2e_test: + # These flutter integration driver tests are maintained by: MummanaSubramanya + strategy: + fail-fast: false + matrix: + include: + - platform: macos-15 + e2e-tests: flutter-ios + - platform: ubuntu-latest + e2e-tests: flutter-android + + runs-on: ${{ matrix.platform }} + + env: + API_LEVEL: 28 + ARCH: x86 + CI: true + XCODE_VERSION: 16.4 + IOS_VERSION: 18.5 + IPHONE_MODEL: iPhone 16 + FLUTTER_ANDROID_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/app-debug.apk" + FLUTTER_IOS_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/ios.zip" + PREBUILT_WDA_PATH: ${{ github.workspace }}/wda/WebDriverAgentRunner-Runner.app + + steps: + + - uses: actions/checkout@v6 + + - uses: actions/setup-java@v5 + if: matrix.e2e-tests == 'flutter-android' + with: + distribution: 'zulu' + java-version: '17' + + - name: Enable KVM group perms + if: matrix.e2e-tests == 'flutter-android' + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Set up Python 3.14 + uses: actions/setup-python@v6 + with: + python-version: 3.14 + + - name: Install Node.js + uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + + - name: Select Xcode + if: matrix.e2e-tests == 'flutter-ios' + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + + - name: Install Appium and drivers + if: matrix.e2e-tests == 'flutter-android' + run: | + npm install --location=global appium + appium driver install uiautomator2 + appium driver install appium-flutter-integration-driver --source npm + + - name: Install Appium and drivers + if: matrix.e2e-tests == 'flutter-ios' + run: | + npm install --location=global appium + appium driver install xcuitest + appium driver run xcuitest download-wda-sim --platform=ios --outdir=$(dirname "$PREBUILT_WDA_PATH") + appium driver install appium-flutter-integration-driver --source npm + + - name: Start Appium server + uses: ./.github/actions/setup-appium-server + with: + port: ${{ env.APPIUM_TEST_SERVER_PORT }} + host: ${{ env.APPIUM_TEST_SERVER_HOST }} + server_args: '--relaxed-security' + log_file: appium-${{ matrix.e2e-tests }}.log + + - name: Cache uv modules + uses: actions/cache@v5 + with: + path: | + ~/.cache/uv + .venv + key: ${{ runner.os }}-uv-shared-${{ hashFiles('**/uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv-shared- + + - name: Run Android tests + if: matrix.e2e-tests == 'flutter-android' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ env.API_LEVEL }} + script: | + make install-uv + uv run pytest -v test/functional/flutter_integration/*_test.py --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html + target: default + disable-spellchecker: true + disable-animations: true + + - name: Start iOS Simulator UI + if: matrix.e2e-tests == 'flutter-ios' + run: | + defaults write com.apple.iphonesimulator PasteboardAutomaticSync -bool false + open -Fn "$(xcode-select --print-path)/Applications/Simulator.app" + + - uses: futureware-tech/simulator-action@v4 + if: matrix.e2e-tests == 'flutter-ios' + with: + # https://github.com/actions/runner-images/blob/main/images/macos/macos-14-arm64-Readme.md + model: ${{ env.IPHONE_MODEL }} + os_version: ${{ env.IOS_VERSION }} + wait_for_boot: true + shutdown_after_job: false + + - name: Finalize iOS simulator boot + if: matrix.e2e-tests == 'flutter-ios' + run: xcrun --sdk iphonesimulator --show-sdk-version + + - name: Run IOS tests + if: matrix.e2e-tests == 'flutter-ios' + run: | + make install-uv + export PLATFORM=ios + uv run pytest -v test/functional/flutter_integration/*_test.py \ + --doctest-modules \ + --junitxml=junit/test-results.xml \ + --cov=com \ + --cov-report=xml \ + --cov-report=html + env: + LOCAL_PREBUILT_WDA: ${{ env.PREBUILT_WDA_PATH }} + + - name: Save server output + if: ${{ always() }} + uses: actions/upload-artifact@master + with: + name: appium-${{ matrix.e2e-tests }}.log + path: appium-${{ matrix.e2e-tests }}.log diff --git a/.github/workflows/lock-update.yml b/.github/workflows/lock-update.yml new file mode 100644 index 00000000..05b94d39 --- /dev/null +++ b/.github/workflows/lock-update.yml @@ -0,0 +1,31 @@ +name: Update uv.lock + +on: + pull_request: + types: [ opened, synchronize ] + paths: + - 'pyproject.toml' + +permissions: + contents: write + pull-requests: write + +jobs: + uv-lock-update: + if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ github.head_ref }} + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + - name: Install uv + run: make install-uv + - run: uv lock + - uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "chore: Refresh uv.lock" diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 00000000..929df133 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,10 @@ +name: Conventional Commits +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +jobs: + lint: + uses: appium/appium-workflows/.github/workflows/pr-title.yml@main + with: + config-preset: angular diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..7f1c1aa8 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,50 @@ +name: Release + +on: + workflow_dispatch: + push: + branches: [ master ] + +permissions: + contents: write + id-token: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install uv + run: make install-uv + + - name: Semantic Release Version + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: uv run semantic-release version + + - name: Check if dist directory exists + id: check_dist + run: | + if [ -d "dist" ]; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Publish to PyPI + if: steps.check_dist.outputs.exists == 'true' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + + - name: Publish GitHub Release + if: steps.check_dist.outputs.exists == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: uv run semantic-release publish diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 00000000..ae07d6c1 --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,36 @@ +name: Unit tests + +on: + workflow_dispatch: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test-ubuntu: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Cache uv modules + uses: actions/cache@v5 + with: + path: | + ~/.cache/uv + .venv + key: ${{ runner.os }}-uv-shared-${{ hashFiles('**/uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv-shared- + - name: Install uv + run: make install-uv + - name: Run Checks + run: make check + - name: Run Unit Tests + run: make unittest diff --git a/.gitignore b/.gitignore index fdf9580c..9ad0aa1d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,12 +5,26 @@ *._* *.log *.log.* +_*/ *.pyc *.egg-info -README.txt MANIFEST build dist + +# Cache +.cache +__pycache__ +.idea +.pytest_cache +.mypy_cache + +# Virtual Environments +venv* +.venv +.tox + +.coverage diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..17b86e2a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.9.0 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 00000000..96cebc8f --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,31 @@ +line-length = 128 +indent-width = 4 + +[lint] +select = [ + # Pyflakes + "F", + # Pylint + "PL", + # isort + "I", +] + +[lint.per-file-ignores] +"__init__.py" = [ + # unused-import + "F401", + # import violations + "E402" +] +"**/{test,docs}/*" = [ + # https://docs.astral.sh/ruff/rules/magic-value-comparison/ + "PLR2004" +] + +[lint.pylint] + +max-args = 6 + +[format] +quote-style = "single" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ae709bd5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2230 @@ +# CHANGELOG + + + +## v5.2.6 (2026-02-12) + +### Bug Fixes + +- Update readme and to kick a release to test out auto release + ([`a4cda69`](https://github.com/appium/python-client/commit/a4cda696bc872f3879a2e7d36df852471f672211)) + +### Chores + +- **deps-dev**: Bump ruff from 0.14.14 to 0.15.0 + ([#1198](https://github.com/appium/python-client/pull/1198), + [`1114b10`](https://github.com/appium/python-client/commit/1114b1033865cf0112da0877f56f68e910daac28)) + +- **deps-dev**: Bump types-python-dateutil + ([#1197](https://github.com/appium/python-client/pull/1197), + [`ae2802e`](https://github.com/appium/python-client/commit/ae2802ef16d4337e81dd01cf6dcc4360a60a8509)) + +### Continuous Integration + +- Add skip to publish if not package was made + ([`bb8b4b3`](https://github.com/appium/python-client/commit/bb8b4b35cd3ece54c857568005168b9d0b6e19a8)) + +- Create automatic release action ([#1196](https://github.com/appium/python-client/pull/1196), + [`208c35c`](https://github.com/appium/python-client/commit/208c35c27da10b78bfd346c3ecdd34d78b920473)) + + +## v5.2.5 (2026-01-24) + +### Chores + +- Add a few new caps in the standard ([#1175](https://github.com/appium/python-client/pull/1175), + [`b22b031`](https://github.com/appium/python-client/commit/b22b0316b18e551695344d11e25e940a7f7ec297)) + +- Enable dependabot for uv ([#1181](https://github.com/appium/python-client/pull/1181), + [`fd138de`](https://github.com/appium/python-client/commit/fd138deb519b5ed7cd2a4a80a639294ec8b42ad4)) + +- Update iOS e2e tests ([#1178](https://github.com/appium/python-client/pull/1178), + [`3677e0c`](https://github.com/appium/python-client/commit/3677e0c3e347a457f3f72b204b0644403725be3b)) + +- **deps**: Bump actions/cache from 3 to 4 + ([#1184](https://github.com/appium/python-client/pull/1184), + [`936e146`](https://github.com/appium/python-client/commit/936e146c05216fe1c620efb7c5e34892ae7e7dcc)) + +- **deps**: Bump actions/cache from 4 to 5 + ([#1191](https://github.com/appium/python-client/pull/1191), + [`aa41b80`](https://github.com/appium/python-client/commit/aa41b80ebfd6c05f3f5b45dbc66ec41b12200040)) + +- **deps**: Bump actions/checkout from 4 to 6 + ([#1186](https://github.com/appium/python-client/pull/1186), + [`7ce850b`](https://github.com/appium/python-client/commit/7ce850bfff817a748dc63ea27c00774c333e76c6)) + +- **deps**: Bump actions/setup-java from 4 to 5 + ([#1183](https://github.com/appium/python-client/pull/1183), + [`91e1855`](https://github.com/appium/python-client/commit/91e18558f78f048a423ea69ededea97596685abe)) + +- **deps**: Bump actions/setup-node from 4 to 6 + ([#1188](https://github.com/appium/python-client/pull/1188), + [`3f8eeec`](https://github.com/appium/python-client/commit/3f8eeec1f499efbe7e82007b22bed7d8a687a316)) + +- **deps**: Bump actions/setup-python from 4 to 6 + ([#1187](https://github.com/appium/python-client/pull/1187), + [`7708ebe`](https://github.com/appium/python-client/commit/7708ebe35761969cb5fad80e9a1a709553a119d1)) + +- **deps**: Bump selenium from 4.33.0 to 4.36.0 + ([#1182](https://github.com/appium/python-client/pull/1182), + [`64c0119`](https://github.com/appium/python-client/commit/64c011926cf804dc8edf27e7c15ffeb2568a45f5)) + +- **deps**: Bump stefanzweifel/git-auto-commit-action from 5 to 7 + ([#1185](https://github.com/appium/python-client/pull/1185), + [`8389732`](https://github.com/appium/python-client/commit/8389732af5392254325a1616a00ea42df22b6bbe)) + +- **deps-dev**: Bump pytest-cov from 6.3.0 to 7.0.0 + ([#1194](https://github.com/appium/python-client/pull/1194), + [`32aca30`](https://github.com/appium/python-client/commit/32aca304057ee68bb2f1a3c0495054d245e4b955)) + +- **deps-dev**: Bump python-semantic-release from 10.3.2 to 10.5.3 + ([#1195](https://github.com/appium/python-client/pull/1195), + [`4844534`](https://github.com/appium/python-client/commit/48445348e313f6d256bdee5b9229772e87674470)) + +### Continuous Integration + +- Add commit message configuration for Dependabot + ([`b68fa61`](https://github.com/appium/python-client/commit/b68fa6158e7c1f7df5479b50af50b937b1d2511d)) + +- Add GitHub Actions to Dependabot configuration + ([`c2edb68`](https://github.com/appium/python-client/commit/c2edb6874da805f8a4e7ba345f31303a51ba4fb6)) + +- Use py 3.14 ([#1193](https://github.com/appium/python-client/pull/1193), + [`3bb2021`](https://github.com/appium/python-client/commit/3bb20212f67cdbe73d72ff8ff9541a5f57cc8393)) + +- Use the central repo's workflow + ([`e59b853`](https://github.com/appium/python-client/commit/e59b853497ae04706137cf252bfa3b92e6a1e229)) + +- Use the latest appium flutter integration driver on CI + ([#1176](https://github.com/appium/python-client/pull/1176), + [`f61c059`](https://github.com/appium/python-client/commit/f61c059d189f19e01ca6bd11670e6d07c550ec31)) + +### Documentation + +- Clean up CHANGELOG by removing old sections + ([`d4f3e16`](https://github.com/appium/python-client/commit/d4f3e16d33cdc22651e99f1f161ff8e79f78328e)) + +- Fix the units for bitRate value + ([`4634378`](https://github.com/appium/python-client/commit/4634378e8e1fe78b962b0d5280a6bf1b05b9b15d)) + +### Testing + +- Fix accidental missing deps + ([`b04f65e`](https://github.com/appium/python-client/commit/b04f65e58ca7d86bc3b5a2cf2d3a8f61dd225f81)) + +- Update Android tests to the canonical pytest format + ([#1180](https://github.com/appium/python-client/pull/1180), + [`b9564c7`](https://github.com/appium/python-client/commit/b9564c78bc22d1fc4b0e345199c9ede7584ad1c3)) + +- Update flutter tests to canonic pytest format + ([#1179](https://github.com/appium/python-client/pull/1179), + [`ed27b1f`](https://github.com/appium/python-client/commit/ed27b1fad14467b15779abb2942627373977f243)) + + +## v5.2.4 (2025-09-11) + +### Chores + +- Add an error handling in _adjust_image_payload + ([#1172](https://github.com/appium/python-client/pull/1172), + [`dc301ec`](https://github.com/appium/python-client/commit/dc301ec5303f6d5fa6af0ec4370d1ffe88ef1f1d)) + +## v5.2.3 (2025-09-09) + +### Bug Fixes + +- Adjust images payload for image comparison API + ([#1170](https://github.com/appium/python-client/pull/1170), + [`b471a05`](https://github.com/appium/python-client/commit/b471a050833c59bb5cacf0872013bd5dc1f24491)) + +### Continuous Integration + +- Update CI config ([#1171](https://github.com/appium/python-client/pull/1171), + [`f867ea5`](https://github.com/appium/python-client/commit/f867ea580c86e043b5fbb586149a18ad87d08239)) + + +## v5.2.2 (2025-08-15) + +### Chores + +- Remvoe unnecessary lines in the changelog and readme + ([`840e958`](https://github.com/appium/python-client/commit/840e95831176f80b5a48921c711f62b5457652bf)) + +- Stop using existing release script ([#1166](https://github.com/appium/python-client/pull/1166), + [`a6238e2`](https://github.com/appium/python-client/commit/a6238e278452a23227cef8d6b4896d259b357ffc)) + +### Documentation + +- Add one more step + ([`d99f699`](https://github.com/appium/python-client/commit/d99f6995e8b2b5295180b7c6b8d54e79fe05bf46)) + + +## v5.2.1 (2025-08-14) + +### Chores + +- Revert version created by release script checking + ([#1164](https://github.com/appium/python-client/pull/1164), + [`04a8580`](https://github.com/appium/python-client/commit/04a8580f999843bc9d121c1ed4ce872761350f31)) + +- Use semantic release changelog instead of gitchangelog + ([#1163](https://github.com/appium/python-client/pull/1163), + [`dd3709e`](https://github.com/appium/python-client/commit/dd3709e084e802d6534d51151be1bd45456a4ebd)) + +- Use semantic-release for most of release script execution + ([#1165](https://github.com/appium/python-client/pull/1165), + [`f6c4687`](https://github.com/appium/python-client/commit/f6c46878c12ae0a8a9ff82e1e9755f325eb8e7cf)) + +### Documentation + +- Manage sphinx stuff via uv ([#1162](https://github.com/appium/python-client/pull/1162), + [`df66645`](https://github.com/appium/python-client/commit/df66645ab284193f8f673d491c8daddcce381a71)) + +- Update readme ([#1165](https://github.com/appium/python-client/pull/1165), + [`f6c4687`](https://github.com/appium/python-client/commit/f6c46878c12ae0a8a9ff82e1e9755f325eb8e7cf)) + + +## v5.2.0 (2025-08-07) + +### Bug Fixes + +- Restore mypy linting ([#1156](https://github.com/appium/python-client/pull/1156), + [`d3c7511`](https://github.com/appium/python-client/commit/d3c7511d6cf6de3a4717b6c691d047785106bed5)) + +### Chores + +- Update uv version ([#1160](https://github.com/appium/python-client/pull/1160), + [`ee8af97`](https://github.com/appium/python-client/commit/ee8af97dcbfe7e129fedce066ccb4138c6c43329)) + +- Use uv build to build the package ([#1159](https://github.com/appium/python-client/pull/1159), + [`c095f79`](https://github.com/appium/python-client/commit/c095f79997171ab076584fea94d6fac0bd3e23ce)) + +- Use uv for version ([#1160](https://github.com/appium/python-client/pull/1160), + [`ee8af97`](https://github.com/appium/python-client/commit/ee8af97dcbfe7e129fedce066ccb4138c6c43329)) + +- Use version via uv ([#1160](https://github.com/appium/python-client/pull/1160), + [`ee8af97`](https://github.com/appium/python-client/commit/ee8af97dcbfe7e129fedce066ccb4138c6c43329)) + +### Continuous Integration + +- Add a script to automatically update uv.lock upon dependabot updates + ([#1158](https://github.com/appium/python-client/pull/1158), + [`1a1cf34`](https://github.com/appium/python-client/commit/1a1cf34975224a47b6365a5524d19a409a2ccffd)) + +### Features + +- Switch package management from pipenv to uv + ([#1155](https://github.com/appium/python-client/pull/1155), + [`7af0fd4`](https://github.com/appium/python-client/commit/7af0fd4c1ffc3b52e912dae662199033b6c55f58)) + + +## v5.1.3 (2025-08-07) + +### Chores + +- Fix some mypy ([#1153](https://github.com/appium/python-client/pull/1153), + [`4e1aafd`](https://github.com/appium/python-client/commit/4e1aafd9a0580d9ce3e5aaaa0eb223dad16e4881)) + +- Fix some mypy errors ([#1153](https://github.com/appium/python-client/pull/1153), + [`4e1aafd`](https://github.com/appium/python-client/commit/4e1aafd9a0580d9ce3e5aaaa0eb223dad16e4881)) + +- Revert protocol ([#1152](https://github.com/appium/python-client/pull/1152), + [`ab6c7bb`](https://github.com/appium/python-client/commit/ab6c7bb5aa01ca1be729dfa85cd604c591af0615)) + +- Wrong usage of CanFindElements #1148 ([#1152](https://github.com/appium/python-client/pull/1152), + [`ab6c7bb`](https://github.com/appium/python-client/commit/ab6c7bb5aa01ca1be729dfa85cd604c591af0615)) + +- **deps-dev**: Update pytest-cov requirement from ~=5.0 to ~=6.2 + ([#1136](https://github.com/appium/python-client/pull/1136), + [`5e14916`](https://github.com/appium/python-client/commit/5e149160132d7a83d21d3a3252aeb2fe3f630306)) + +- **deps-dev**: Update ruff requirement from ~=0.12.4 to ~=0.12.5 + ([#1149](https://github.com/appium/python-client/pull/1149), + [`1457866`](https://github.com/appium/python-client/commit/145786676474211a27f7ab1763afc73d545da2b2)) + +- **deps-dev**: Update ruff requirement from ~=0.12.5 to ~=0.12.7 + ([#1151](https://github.com/appium/python-client/pull/1151), + [`abf210a`](https://github.com/appium/python-client/commit/abf210ab77015e475200f3db42f61e4eaa2a6773)) + +### Testing + +- Add bidi example in test ([#1154](https://github.com/appium/python-client/pull/1154), + [`f8922da`](https://github.com/appium/python-client/commit/f8922daa965e25db4322c7d77369a2e5c133e61e)) + +- Add bidi log example test ([#1154](https://github.com/appium/python-client/pull/1154), + [`f8922da`](https://github.com/appium/python-client/commit/f8922daa965e25db4322c7d77369a2e5c133e61e)) + + +## v5.1.2 (2025-07-23) + +### Chores + +- Fix typos ([#1133](https://github.com/appium/python-client/pull/1133), + [`a1b33c3`](https://github.com/appium/python-client/commit/a1b33c3e632382316b73540a7339a51ad57b9a2b)) + +- Inherit CanFindElements ([#1148](https://github.com/appium/python-client/pull/1148), + [`7ca9425`](https://github.com/appium/python-client/commit/7ca942533786b5e9fb991cee3bd4c373fc1e99b4)) + +- Remove unused commands ([#1132](https://github.com/appium/python-client/pull/1132), + [`a5f0147`](https://github.com/appium/python-client/commit/a5f0147dabe7cf7c927b9ffeabb41a7672bef801)) + +- **deps**: Bump selenium from 4.32.0 to 4.33.0 + ([#1130](https://github.com/appium/python-client/pull/1130), + [`617ba6c`](https://github.com/appium/python-client/commit/617ba6cd943ad4ca15148fae6ee1aed949d123ff)) + +- **deps-dev**: Update pytest requirement from ~=8.3 to ~=8.4 + ([#1134](https://github.com/appium/python-client/pull/1134), + [`0f9dd3b`](https://github.com/appium/python-client/commit/0f9dd3bc22274998add55fb0c6fa4ba956f0a47f)) + +- **deps-dev**: Update ruff requirement from ~=0.11.10 to ~=0.11.11 + ([#1128](https://github.com/appium/python-client/pull/1128), + [`2f2205f`](https://github.com/appium/python-client/commit/2f2205f1763a62992279c71e5e3cc518e7fc05c0)) + +- **deps-dev**: Update ruff requirement from ~=0.11.11 to ~=0.11.12 + ([#1131](https://github.com/appium/python-client/pull/1131), + [`ba38ccd`](https://github.com/appium/python-client/commit/ba38ccdf2afd9b13641a8bcfb171329acf937b32)) + +- **deps-dev**: Update ruff requirement from ~=0.11.12 to ~=0.11.13 + ([#1135](https://github.com/appium/python-client/pull/1135), + [`0887cc1`](https://github.com/appium/python-client/commit/0887cc1d6bb9d6accb46542ad83504f5f0718e1b)) + +- **deps-dev**: Update ruff requirement from ~=0.11.13 to ~=0.12.1 + ([#1139](https://github.com/appium/python-client/pull/1139), + [`e75c58f`](https://github.com/appium/python-client/commit/e75c58f5548e930003d0c4fac485ad2a05cda8cb)) + +- **deps-dev**: Update ruff requirement from ~=0.11.8 to ~=0.11.10 + ([#1127](https://github.com/appium/python-client/pull/1127), + [`128666e`](https://github.com/appium/python-client/commit/128666e7d858b52e75fb3778c7de95c39b0182db)) + +- **deps-dev**: Update ruff requirement from ~=0.12.1 to ~=0.12.2 + ([#1142](https://github.com/appium/python-client/pull/1142), + [`884062d`](https://github.com/appium/python-client/commit/884062d045ea47e58d33478e2d0ad477c4aeffe5)) + +- **deps-dev**: Update ruff requirement from ~=0.12.2 to ~=0.12.3 + ([#1144](https://github.com/appium/python-client/pull/1144), + [`772723b`](https://github.com/appium/python-client/commit/772723babc06888141163fd90c893fda1bd73996)) + +- **deps-dev**: Update ruff requirement from ~=0.12.3 to ~=0.12.4 + ([#1146](https://github.com/appium/python-client/pull/1146), + [`d43a190`](https://github.com/appium/python-client/commit/d43a190cc99dcae31f13c074b7f7ac9e7552f9d9)) + +- **deps-dev**: Update tox requirement from ~=4.25 to ~=4.26 + ([#1126](https://github.com/appium/python-client/pull/1126), + [`157ec01`](https://github.com/appium/python-client/commit/157ec011c279a56ebceb7bf5becd733c562ce9f3)) + +- **deps-dev**: Update tox requirement from ~=4.26 to ~=4.27 + ([#1138](https://github.com/appium/python-client/pull/1138), + [`c0ed394`](https://github.com/appium/python-client/commit/c0ed394765cc8f7567edd069b36903b2bbde3883)) + +### Continuous Integration + +- Apply prebuilt wda ([#1141](https://github.com/appium/python-client/pull/1141), + [`e3bcc37`](https://github.com/appium/python-client/commit/e3bcc3768d44b1ff1c85711f5553141443f40d0b)) + +### Documentation + +- Re-run the gen code ([#1129](https://github.com/appium/python-client/pull/1129), + [`3457e49`](https://github.com/appium/python-client/commit/3457e499f04e3d34d881ef6362cabe0751dd7a2d)) + + +## v5.1.1 (2025-05-06) + +### Chores + +- **deps**: Bump selenium from 4.31.0 to 4.32.0 + ([#1124](https://github.com/appium/python-client/pull/1124), + [`9e27a99`](https://github.com/appium/python-client/commit/9e27a998113a31fcc1440e08f52cb79cd26a81d7)) + +### Documentation + +- Update compatibility matrix + ([`9e42c7f`](https://github.com/appium/python-client/commit/9e42c7faf7c298dec1b32d25d8e71328ec17c073)) + +- Update README.md + ([`48371a7`](https://github.com/appium/python-client/commit/48371a7714fbdbb6bd7dabc5ae5762eb80be6fc6)) + + +## v5.1.0 (2025-05-05) + +### Chores + +- **deps**: Bump selenium from 4.29.0 to 4.30.0 + ([#1108](https://github.com/appium/python-client/pull/1108), + [`96f7e6b`](https://github.com/appium/python-client/commit/96f7e6bf2377bf1e07b30f8b3571fc24bdff9c7e)) + +- **deps**: Bump selenium from 4.30.0 to 4.31.0 + ([#1113](https://github.com/appium/python-client/pull/1113), + [`f59f23d`](https://github.com/appium/python-client/commit/f59f23df08e7e3c4f64d63afcb81d22445d21010)) + +- **deps**: Update typing-extensions requirement + ([#1115](https://github.com/appium/python-client/pull/1115), + [`24f9f7f`](https://github.com/appium/python-client/commit/24f9f7f8530abf376f833ea73541288f569c9f31)) + +- **deps**: Update typing-extensions requirement + ([#1112](https://github.com/appium/python-client/pull/1112), + [`e3add90`](https://github.com/appium/python-client/commit/e3add903b79921bc2dc315affadb4571014f2e48)) + +- **deps-dev**: Update ruff requirement from ~=0.10.0 to ~=0.11.2 + ([#1107](https://github.com/appium/python-client/pull/1107), + [`bfaefa1`](https://github.com/appium/python-client/commit/bfaefa1c458c5d98842a3c786d466a5094a824b8)) + +- **deps-dev**: Update ruff requirement from ~=0.11.2 to ~=0.11.3 + ([#1111](https://github.com/appium/python-client/pull/1111), + [`c403235`](https://github.com/appium/python-client/commit/c4032354223833ccc62869f059233cb188f56774)) + +- **deps-dev**: Update ruff requirement from ~=0.11.3 to ~=0.11.4 + ([#1114](https://github.com/appium/python-client/pull/1114), + [`d27b924`](https://github.com/appium/python-client/commit/d27b924185736524e24558575b1100e1b333b39a)) + +- **deps-dev**: Update ruff requirement from ~=0.11.4 to ~=0.11.7 + ([#1118](https://github.com/appium/python-client/pull/1118), + [`c3b86b8`](https://github.com/appium/python-client/commit/c3b86b8adb26c0155ec4b72ffc295f7fbe57fd00)) + +- **deps-dev**: Update ruff requirement from ~=0.11.7 to ~=0.11.8 + ([#1122](https://github.com/appium/python-client/pull/1122), + [`2680a28`](https://github.com/appium/python-client/commit/2680a28e16259089b370d747ef0d90fb8b043198)) + +- **deps-dev**: Update tox requirement from ~=4.24 to ~=4.25 + ([#1109](https://github.com/appium/python-client/pull/1109), + [`a46dd88`](https://github.com/appium/python-client/commit/a46dd88500e48ac89ac05db0f066560eedb38730)) + +### Continuous Integration + +- Tune CC title script + ([`c37352a`](https://github.com/appium/python-client/commit/c37352ad2753aa6005fa5e52e56683335551d039)) + +### Features + +- Add method for interacting with the Flutter integration driver + ([#1123](https://github.com/appium/python-client/pull/1123), + [`635e762`](https://github.com/appium/python-client/commit/635e762678dcfff721dab5e52da41e4609c1d114)) + +### Testing + +- Use timeout in client_config instead of the global var + ([#1120](https://github.com/appium/python-client/pull/1120), + [`727631d`](https://github.com/appium/python-client/commit/727631d33087beca86ccacc6b931e162fd5b8c49)) + + +## v5.0.0 (2025-03-24) + +### Chores + +- **deps-dev**: Update mock requirement from ~=5.1 to ~=5.2 + ([#1100](https://github.com/appium/python-client/pull/1100), + [`bbc1e91`](https://github.com/appium/python-client/commit/bbc1e91543986724f86a07ffa3a2218c38b8d0d8)) + +- **deps-dev**: Update pre-commit requirement from ~=4.1 to ~=4.2 + ([#1104](https://github.com/appium/python-client/pull/1104), + [`d2326b9`](https://github.com/appium/python-client/commit/d2326b9cc82a5be18feb191852061a5596393c70)) + +- **deps-dev**: Update ruff requirement from ~=0.9.10 to ~=0.10.0 + ([#1102](https://github.com/appium/python-client/pull/1102), + [`6707365`](https://github.com/appium/python-client/commit/670736582711df1a5303c794ff58aa7d7127d649)) + +- **deps-dev**: Update ruff requirement from ~=0.9.5 to ~=0.9.7 + ([#1097](https://github.com/appium/python-client/pull/1097), + [`74224e0`](https://github.com/appium/python-client/commit/74224e090e6ccbba51fa9fca4a7d097ea242ff4c)) + +- **deps-dev**: Update ruff requirement from ~=0.9.7 to ~=0.9.9 + ([#1099](https://github.com/appium/python-client/pull/1099), + [`5605d9b`](https://github.com/appium/python-client/commit/5605d9b4bc07554bd66c9d339aeee9bda010aa55)) + +- **deps-dev**: Update ruff requirement from ~=0.9.9 to ~=0.9.10 + ([#1101](https://github.com/appium/python-client/pull/1101), + [`4d8abfa`](https://github.com/appium/python-client/commit/4d8abfa041c67b0cd05f1ce7d7ea96572cb10a58)) + +### Features + +- Define AppiumClientConfig ([#1070](https://github.com/appium/python-client/pull/1070), + [`525d5b8`](https://github.com/appium/python-client/commit/525d5b8b5d8c9919470c4c5a191a6d5c1090027e)) + + +## v4.5.1 (2025-02-22) + +### Bug Fixes + +- Prevent warning log when initialize a webdriver using version 4.5.0 (selenium v4.26+) + ([#1098](https://github.com/appium/python-client/pull/1098), + [`68ceca7`](https://github.com/appium/python-client/commit/68ceca73ac83cef40ec90bdbb73305e384073983)) + +### Chores + +- **deps**: Bump selenium from 4.28.0 to 4.28.1 + ([#1088](https://github.com/appium/python-client/pull/1088), + [`a1ead29`](https://github.com/appium/python-client/commit/a1ead29fc1c0aa156f1144b8f24bddb42358770a)) + +- **deps**: Bump selenium from 4.28.1 to 4.29.0 + ([#1096](https://github.com/appium/python-client/pull/1096), + [`f53deb3`](https://github.com/appium/python-client/commit/f53deb3440a8b06a7f83726596eacc52eb1cfef4)) + +- **deps-dev**: Update pre-commit requirement from ~=3.5 to ~=4.1 + ([#1085](https://github.com/appium/python-client/pull/1085), + [`e5201fd`](https://github.com/appium/python-client/commit/e5201fdb3028df44c993f0375680f605702e8369)) + +- **deps-dev**: Update ruff requirement from ~=0.9.2 to ~=0.9.3 + ([#1089](https://github.com/appium/python-client/pull/1089), + [`f8a4f56`](https://github.com/appium/python-client/commit/f8a4f5693185d7f59156e55f13d80bc002b198a5)) + +- **deps-dev**: Update ruff requirement from ~=0.9.3 to ~=0.9.5 + ([#1092](https://github.com/appium/python-client/pull/1092), + [`82c40b5`](https://github.com/appium/python-client/commit/82c40b50577d4155c3215724babc3ca56b587ac2)) + +- **deps-dev**: Update tox requirement from ~=4.23 to ~=4.24 + ([#1086](https://github.com/appium/python-client/pull/1086), + [`121be97`](https://github.com/appium/python-client/commit/121be9769cd8bd631fd6423a341be9dfb7e1658c)) + +### Documentation + +- Update README.md + ([`733504e`](https://github.com/appium/python-client/commit/733504e8304ca6901363351ec29770fcf9719fe7)) + +### Testing + +- Pytest does not require test classes unless you need grouping or fixtures with class scope. + ([#1094](https://github.com/appium/python-client/pull/1094), + [`2218933`](https://github.com/appium/python-client/commit/22189335caccd89daffc3519bba8c90360be5fd1)) + +- Use pytest without class-based structures, using parameterization for better reusability. + ([#1095](https://github.com/appium/python-client/pull/1095), + [`c2732dd`](https://github.com/appium/python-client/commit/c2732ddc85b7362de3fc9e59d0c97b4e3bd02496)) + + +## v4.5.0 (2025-01-22) + +### Chores + +- Update tags + ([`aa486f3`](https://github.com/appium/python-client/commit/aa486f3bd287fe00185bebb254b5b9b6bc0440fb)) + +- **deps**: Bump selenium from 4.27.1 to 4.28.0 + ([#1084](https://github.com/appium/python-client/pull/1084), + [`b10a11e`](https://github.com/appium/python-client/commit/b10a11e051e5741db7c91bf05f390ce178a2e0bd)) + +- **deps-dev**: Update ruff requirement from ~=0.8.1 to ~=0.8.3 + ([#1074](https://github.com/appium/python-client/pull/1074), + [`2605001`](https://github.com/appium/python-client/commit/2605001d5c1952779d289038966577cb4d2298b4)) + +- **deps-dev**: Update ruff requirement from ~=0.8.3 to ~=0.8.4 + ([#1078](https://github.com/appium/python-client/pull/1078), + [`6d4c633`](https://github.com/appium/python-client/commit/6d4c633901a18c5797c1d84c1c06bf5652fc328b)) + +- **deps-dev**: Update ruff requirement from ~=0.8.4 to ~=0.8.5 + ([#1079](https://github.com/appium/python-client/pull/1079), + [`5db72cf`](https://github.com/appium/python-client/commit/5db72cfb12b5e3c07f3280ad7fb006ceb04ee5b2)) + +- **deps-dev**: Update ruff requirement from ~=0.8.5 to ~=0.8.6 + ([#1080](https://github.com/appium/python-client/pull/1080), + [`a1986d4`](https://github.com/appium/python-client/commit/a1986d4821d2878456eb2760be9487ee83c99c17)) + +- **deps-dev**: Update ruff requirement from ~=0.8.6 to ~=0.9.0 + ([#1081](https://github.com/appium/python-client/pull/1081), + [`4cdbed7`](https://github.com/appium/python-client/commit/4cdbed78c9d45f9b59c0cfabfbcb5acb91901de1)) + +- **deps-dev**: Update ruff requirement from ~=0.9.0 to ~=0.9.1 + ([#1082](https://github.com/appium/python-client/pull/1082), + [`8117add`](https://github.com/appium/python-client/commit/8117add172b4cfd0ad03c96d09873eb0162d649d)) + +- **deps-dev**: Update ruff requirement from ~=0.9.1 to ~=0.9.2 + ([#1083](https://github.com/appium/python-client/pull/1083), + [`0ea049a`](https://github.com/appium/python-client/commit/0ea049af1db4f4cd89ce169879e38a6be354ca90)) + + +## v4.4.0 (2024-11-29) + +### Bug Fixes + +- Adding selenium typing ([#1071](https://github.com/appium/python-client/pull/1071), + [`00e9a6e`](https://github.com/appium/python-client/commit/00e9a6e6e934ab9c9cc1e92aacb05b6bbefd08ff)) + +- Using single quotes ([#1071](https://github.com/appium/python-client/pull/1071), + [`00e9a6e`](https://github.com/appium/python-client/commit/00e9a6e6e934ab9c9cc1e92aacb05b6bbefd08ff)) + +### Chores + +- Dump ruff + ([`e4f06ab`](https://github.com/appium/python-client/commit/e4f06abf1f6bc302bde373071d1e3c007a67c03a)) + +- **deps**: Bump selenium from 4.26.1 to 4.27.0 + ([#1067](https://github.com/appium/python-client/pull/1067), + [`ea61c2e`](https://github.com/appium/python-client/commit/ea61c2e8f80c64365610321ef0e6f512280fdbc0)) + +- **deps**: Bump selenium from 4.27.0 to 4.27.1 + ([#1068](https://github.com/appium/python-client/pull/1068), + [`dd8ef74`](https://github.com/appium/python-client/commit/dd8ef742e850cb74fc0d2ed897e638278d696d42)) + +- **deps-dev**: Update ruff requirement from ~=0.7.3 to ~=0.7.4 + ([#1063](https://github.com/appium/python-client/pull/1063), + [`fef190e`](https://github.com/appium/python-client/commit/fef190ed8064c9e6fe717fb1bdfee5019edc28f3)) + +### Features + +- Added typing for AppiumBy ([#1071](https://github.com/appium/python-client/pull/1071), + [`00e9a6e`](https://github.com/appium/python-client/commit/00e9a6e6e934ab9c9cc1e92aacb05b6bbefd08ff)) + +- Added typing for AppiumBy types ([#1071](https://github.com/appium/python-client/pull/1071), + [`00e9a6e`](https://github.com/appium/python-client/commit/00e9a6e6e934ab9c9cc1e92aacb05b6bbefd08ff)) + + +## v4.3.0 (2024-11-12) + +### Chores + +- Update pre-commit ([#1058](https://github.com/appium/python-client/pull/1058), + [`cd1070a`](https://github.com/appium/python-client/commit/cd1070af807d7ff1c42e4d2270452560738e254d)) + +- **deps-dev**: Update ruff requirement from ~=0.7.0 to ~=0.7.2 + ([#1057](https://github.com/appium/python-client/pull/1057), + [`86f4d48`](https://github.com/appium/python-client/commit/86f4d4847d09e9a1077d3126ebad6e0faca2b3b0)) + +- **deps-dev**: Update ruff requirement from ~=0.7.2 to ~=0.7.3 + ([#1060](https://github.com/appium/python-client/pull/1060), + [`f26f763`](https://github.com/appium/python-client/commit/f26f763f138813781bb8d5382bf3c7c8ae61adf5)) + +### Documentation + +- Update CHANGELOG.rst + ([`6bd041a`](https://github.com/appium/python-client/commit/6bd041a8812bdf5a6a35a44ab4d207efab4a6854)) + +- Update the readme ([#1054](https://github.com/appium/python-client/pull/1054), + [`94a6da7`](https://github.com/appium/python-client/commit/94a6da755ef3e3af88b0fba6322a2e69dc123d37)) + +### Features + +- Require selenium 4.26+ ([#1054](https://github.com/appium/python-client/pull/1054), + [`94a6da7`](https://github.com/appium/python-client/commit/94a6da755ef3e3af88b0fba6322a2e69dc123d37)) + +- Support selenium 4.26+: support ClientConfig and refactoring internal implementation + ([#1054](https://github.com/appium/python-client/pull/1054), + [`94a6da7`](https://github.com/appium/python-client/commit/94a6da755ef3e3af88b0fba6322a2e69dc123d37)) + + +## v4.2.1 (2024-10-31) + + +## v4.1.1 (2024-10-31) + +### Chores + +- Allow selenium binging up to 4.25 ([#1055](https://github.com/appium/python-client/pull/1055), + [`a22306e`](https://github.com/appium/python-client/commit/a22306ea1eb035148d8c801ff2c3321f4c02708c)) + +- Revert unnecessary change ([#1046](https://github.com/appium/python-client/pull/1046), + [`27595c4`](https://github.com/appium/python-client/commit/27595c40cceb33219cecd28c14c0e8fbdb566a37)) + +- Update precommit config + ([`b8daf2c`](https://github.com/appium/python-client/commit/b8daf2c67cbcdbbc10a75b9a45f4a415a5057b95)) + +- Update release script + ([`7ac1fd9`](https://github.com/appium/python-client/commit/7ac1fd9bcdba2fa29bea8c2f746da30f5420920f)) + +- Use proper type declarations for methods returning self instances + ([#1039](https://github.com/appium/python-client/pull/1039), + [`be51520`](https://github.com/appium/python-client/commit/be51520d2e204a63035fc99eaa1f796db3fed615)) + +- Use ruff (isort, pylint and pyflakes) instead of individual isort, pylint and black libraries + ([#1043](https://github.com/appium/python-client/pull/1043), + [`8f2b059`](https://github.com/appium/python-client/commit/8f2b059586f9e73fb431043a655729f655719884)) + +- **deps**: Update selenium requirement from ~=4.24 to ~=4.25 + ([#1026](https://github.com/appium/python-client/pull/1026), + [`5778a50`](https://github.com/appium/python-client/commit/5778a502fb6203395bc1e5043ddb430342593493)) + +- **deps**: Update sphinx requirement from <7.0,>=4.0 to >=4.0,<9.0 + ([#1009](https://github.com/appium/python-client/pull/1009), + [`2dab159`](https://github.com/appium/python-client/commit/2dab159ef8cfd7d1b70ea382d4fd65246c7bc61e)) + +- **deps**: Update sphinx-rtd-theme requirement from <3.0 to <4.0 + ([#1040](https://github.com/appium/python-client/pull/1040), + [`fdbd03a`](https://github.com/appium/python-client/commit/fdbd03ab6a966601223c1d3dadbff21363c2e1d3)) + +- **deps-dev**: Update pytest-cov requirement from ~=4.1 to ~=5.0 + ([#975](https://github.com/appium/python-client/pull/975), + [`2c775ee`](https://github.com/appium/python-client/commit/2c775ee518c17b8a95d8ec1302d9fb1654498d12)) + +- **deps-dev**: Update ruff requirement from ~=0.6.9 to ~=0.7.0 + ([#1049](https://github.com/appium/python-client/pull/1049), + [`36786ef`](https://github.com/appium/python-client/commit/36786ef9c504e06f16212a5730e0d9274dca8fad)) + +- **deps-dev**: Update tox requirement from ~=4.20 to ~=4.21 + ([#1037](https://github.com/appium/python-client/pull/1037), + [`e4b40ae`](https://github.com/appium/python-client/commit/e4b40aefc573429d40d51b60ac03ca2961b3313e)) + +- **deps-dev**: Update tox requirement from ~=4.21 to ~=4.22 + ([#1047](https://github.com/appium/python-client/pull/1047), + [`0a403bc`](https://github.com/appium/python-client/commit/0a403bcd650ae3e759b55ef370618364f897ccfa)) + +- **deps-dev**: Update tox requirement from ~=4.22 to ~=4.23 + ([#1048](https://github.com/appium/python-client/pull/1048), + [`7ac6bb8`](https://github.com/appium/python-client/commit/7ac6bb833022b7dd6c753fd806904ab9f3e9fb79)) + +### Documentation + +- Add options matrix in readme ([#1046](https://github.com/appium/python-client/pull/1046), + [`27595c4`](https://github.com/appium/python-client/commit/27595c40cceb33219cecd28c14c0e8fbdb566a37)) + +- Add tweak pathds ([#1046](https://github.com/appium/python-client/pull/1046), + [`27595c4`](https://github.com/appium/python-client/commit/27595c40cceb33219cecd28c14c0e8fbdb566a37)) + +- Update selenium compatibility matrix + ([`f3632a6`](https://github.com/appium/python-client/commit/f3632a6a1f413dbabff1fd5b7c1f605b5b33fb8b)) + +### Features + +- Add a separate function for service startup validation + ([#1038](https://github.com/appium/python-client/pull/1038), + [`90b9978`](https://github.com/appium/python-client/commit/90b9978601e834518b19092b3d66f241d6c420a5)) + +### Testing + +- Cleanup duplicated tests more ([#1032](https://github.com/appium/python-client/pull/1032), + [`fea88d1`](https://github.com/appium/python-client/commit/fea88d1397d2721fa4da8a48a1a1a5cd6bbde6c7)) + +- Cleanup func tests for ios more ([#1036](https://github.com/appium/python-client/pull/1036), + [`2b48a09`](https://github.com/appium/python-client/commit/2b48a09a707e669b1d8caa9d48ca578ecc34f3e4)) + +- Cleanup functional tests and move to unit test to CI stable + ([#1024](https://github.com/appium/python-client/pull/1024), + [`9cdfe5c`](https://github.com/appium/python-client/commit/9cdfe5c7cb58c4cd9495a15658ed17a5681b79d6)) + +- Cleanup ios ([#1034](https://github.com/appium/python-client/pull/1034), + [`8773351`](https://github.com/appium/python-client/commit/877335152c0e0e705e36867d4449631d5385925a)) + +- Cleanup test more ([#1032](https://github.com/appium/python-client/pull/1032), + [`fea88d1`](https://github.com/appium/python-client/commit/fea88d1397d2721fa4da8a48a1a1a5cd6bbde6c7)) + +- Cleanup tests more ([#1033](https://github.com/appium/python-client/pull/1033), + [`9a3a633`](https://github.com/appium/python-client/commit/9a3a6337c375d3ece124df459e231fbfd0f2d8b1)) + +- Just remove existing ones ([#1032](https://github.com/appium/python-client/pull/1032), + [`fea88d1`](https://github.com/appium/python-client/commit/fea88d1397d2721fa4da8a48a1a1a5cd6bbde6c7)) + +- Remove some functional test which is tested in unit tets + ([#1033](https://github.com/appium/python-client/pull/1033), + [`9a3a633`](https://github.com/appium/python-client/commit/9a3a6337c375d3ece124df459e231fbfd0f2d8b1)) + +- Remvoe location tests ([#1033](https://github.com/appium/python-client/pull/1033), + [`9a3a633`](https://github.com/appium/python-client/commit/9a3a6337c375d3ece124df459e231fbfd0f2d8b1)) + + +## v4.2.0 (2024-09-23) + +### Bug Fixes + +- Add missing __init__.py ([#1029](https://github.com/appium/python-client/pull/1029), + [`25da847`](https://github.com/appium/python-client/commit/25da8476e2826bf8d495030a395080ddc83bc7a7)) + +### Chores + +- **deps**: Update selenium requirement from ~=4.23 to ~=4.24 + ([#1018](https://github.com/appium/python-client/pull/1018), + [`8d53160`](https://github.com/appium/python-client/commit/8d531601d2ed0d2abf1a0ed253214afe630a418f)) + +- **deps-dev**: Update black requirement from <24.0.0 to <25.0.0 + ([#950](https://github.com/appium/python-client/pull/950), + [`87ec961`](https://github.com/appium/python-client/commit/87ec96177e9a4bcec67099fbd3acb6d3e0a838fb)) + +- **deps-dev**: Update pylint requirement from ~=3.2.6 to ~=3.2.7 + ([#1019](https://github.com/appium/python-client/pull/1019), + [`d8c1260`](https://github.com/appium/python-client/commit/d8c126009be3916058b926f1ea38770486fe7a10)) + +- **deps-dev**: Update tox requirement from ~=4.18 to ~=4.19 + ([#1020](https://github.com/appium/python-client/pull/1020), + [`54a9ef1`](https://github.com/appium/python-client/commit/54a9ef17d349d7978b391825a9c0fe2a8ca266bf)) + +- **deps-dev**: Update tox requirement from ~=4.19 to ~=4.20 + ([#1021](https://github.com/appium/python-client/pull/1021), + [`bb8d509`](https://github.com/appium/python-client/commit/bb8d50920f6dc417f38f7ee5fd74a783e9558b72)) + +### Documentation + +- Modify readme + ([`d0ad068`](https://github.com/appium/python-client/commit/d0ad06893d3b1635eacfa06e50a74cdcf874d019)) + +### Features + +- Add flutter integration driver commands and tests + ([#1022](https://github.com/appium/python-client/pull/1022), + [`2ffa930`](https://github.com/appium/python-client/commit/2ffa930270b455131217c2d8373fd32096b2c95c)) + + +## v4.1.0 (2024-08-17) + +### Chores + +- Remove non-reference variables, import and fix test names to run them properly + ([#1006](https://github.com/appium/python-client/pull/1006), + [`e34ca80`](https://github.com/appium/python-client/commit/e34ca80812713d16806ab09af7f35f98e5b7a846)) + +- **deps**: Update selenium requirement from ~=4.22 to ~=4.23 + ([#1003](https://github.com/appium/python-client/pull/1003), + [`1c5321a`](https://github.com/appium/python-client/commit/1c5321abaf238ea752dc9a3581143328ce8b5b03)) + +- **deps-dev**: Update pylint requirement from ~=3.2.2 to ~=3.2.5 + ([#1000](https://github.com/appium/python-client/pull/1000), + [`d20db86`](https://github.com/appium/python-client/commit/d20db86741220b9d155bf16391f41260cc0d552b)) + +- **deps-dev**: Update pylint requirement from ~=3.2.5 to ~=3.2.6 + ([#1005](https://github.com/appium/python-client/pull/1005), + [`6d66d92`](https://github.com/appium/python-client/commit/6d66d92673956c3e077ecf6909b4662cc182dd72)) + +- **deps-dev**: Update pytest requirement from ~=8.2 to ~=8.3 + ([#1004](https://github.com/appium/python-client/pull/1004), + [`e75f8e9`](https://github.com/appium/python-client/commit/e75f8e9274a80a17f324b112c55698b0b298d53c)) + +- **deps-dev**: Update tox requirement from ~=4.15 to ~=4.16 + ([#1002](https://github.com/appium/python-client/pull/1002), + [`3f3f11a`](https://github.com/appium/python-client/commit/3f3f11aa5ab27a96e22fab10c74863b0380d2349)) + +- **deps-dev**: Update tox requirement from ~=4.16 to ~=4.18 + ([#1013](https://github.com/appium/python-client/pull/1013), + [`f7b0256`](https://github.com/appium/python-client/commit/f7b0256d7821eab0d302995765a6bde34931164a)) + +### Continuous Integration + +- Move Azure to GHA (Android) ([#1007](https://github.com/appium/python-client/pull/1007), + [`b148174`](https://github.com/appium/python-client/commit/b148174f14e014ac961f185d3bac715e5c8e32c3)) + +- Moving to GHA ([#1010](https://github.com/appium/python-client/pull/1010), + [`fb06ca1`](https://github.com/appium/python-client/commit/fb06ca12dbe4c1be936f0c0525a864ee932a5614)) + +- Run func_test_android4 ([#1010](https://github.com/appium/python-client/pull/1010), + [`fb06ca1`](https://github.com/appium/python-client/commit/fb06ca12dbe4c1be936f0c0525a864ee932a5614)) + +- Run other android tests on GHA ([#1008](https://github.com/appium/python-client/pull/1008), + [`0e13381`](https://github.com/appium/python-client/commit/0e13381c7c7b7b0f7a00d8a5145fd2b591a3763d)) + +- Run other android tests on GHA a few more + ([#1008](https://github.com/appium/python-client/pull/1008), + [`0e13381`](https://github.com/appium/python-client/commit/0e13381c7c7b7b0f7a00d8a5145fd2b591a3763d)) + +### Documentation + +- Replace badge source ([#1012](https://github.com/appium/python-client/pull/1012), + [`834c854`](https://github.com/appium/python-client/commit/834c8549b82ef0dd0d5a8307fe6635045b6c3ac0)) + +### Features + +- Add app_path property ("appPath") to Mac2Options + ([#1014](https://github.com/appium/python-client/pull/1014), + [`18c4723`](https://github.com/appium/python-client/commit/18c4723e7f7ebfca104bff92a720c667f1269223)) + +### Testing + +- Fix tests ([#1010](https://github.com/appium/python-client/pull/1010), + [`fb06ca1`](https://github.com/appium/python-client/commit/fb06ca12dbe4c1be936f0c0525a864ee932a5614)) + + +## v4.0.1 (2024-07-08) + +### Bug Fixes + +- Typo and update test ([#992](https://github.com/appium/python-client/pull/992), + [`a9af896`](https://github.com/appium/python-client/commit/a9af896bc08735e2927c8ace90be2e420b09ce5e)) + +### Chores + +- Add mobile: replacements to clipboard API wrappers + ([#998](https://github.com/appium/python-client/pull/998), + [`19d4f4b`](https://github.com/appium/python-client/commit/19d4f4b2ab02caed0dcbe781196698e279230d5b)) + +- Remove IOS_UIAUTOMATION ([#979](https://github.com/appium/python-client/pull/979), + [`9e63569`](https://github.com/appium/python-client/commit/9e63569b570d7a897264110a18c621c7a25f72ae)) + +- **deps**: Update selenium requirement from ~=4.18 to ~=4.19 + ([#976](https://github.com/appium/python-client/pull/976), + [`7bd1b06`](https://github.com/appium/python-client/commit/7bd1b0665028771d06036b2b31fe76d9d32490a4)) + +- **deps**: Update selenium requirement from ~=4.19 to ~=4.20 + ([#981](https://github.com/appium/python-client/pull/981), + [`cdc715b`](https://github.com/appium/python-client/commit/cdc715b686e41ab39c61e37448b934f32aa498af)) + +- **deps**: Update selenium requirement from ~=4.20 to ~=4.21 + ([#991](https://github.com/appium/python-client/pull/991), + [`850055d`](https://github.com/appium/python-client/commit/850055db9b5a44f30b0821f73672b19611173a27)) + +- **deps**: Update selenium requirement from ~=4.21 to ~=4.22 + ([#996](https://github.com/appium/python-client/pull/996), + [`6e06805`](https://github.com/appium/python-client/commit/6e06805dc80f382a54442fa5a30de4ce4d3388c2)) + +- **deps**: Update sphinx-rtd-theme requirement from <2.0 to <3.0 + ([#935](https://github.com/appium/python-client/pull/935), + [`81a50e3`](https://github.com/appium/python-client/commit/81a50e344d26e4124e8019d0736f6240ac46267b)) + +- **deps-dev**: Update pylint requirement from ~=3.1.0 to ~=3.2.2 + ([#993](https://github.com/appium/python-client/pull/993), + [`fa7e6d4`](https://github.com/appium/python-client/commit/fa7e6d44e13ea586bcbe73a1ba4464b97516ff51)) + +- **deps-dev**: Update pytest requirement from ~=8.1 to ~=8.2 + ([#983](https://github.com/appium/python-client/pull/983), + [`9c142b8`](https://github.com/appium/python-client/commit/9c142b8916269e420e5feb11b869feabf3eb583b)) + +- **deps-dev**: Update tox requirement from ~=4.14 to ~=4.15 + ([#982](https://github.com/appium/python-client/pull/982), + [`7551deb`](https://github.com/appium/python-client/commit/7551deb2c2bd4331385f8a6fb7dd60762d113865)) + +- **deps-dev**: Update types-python-dateutil requirement + ([#973](https://github.com/appium/python-client/pull/973), + [`1871e4a`](https://github.com/appium/python-client/commit/1871e4af09dd54e66f4483d0951d1df09b60faa0)) + +### Continuous Integration + +- Add initial gha to run by manual ([#984](https://github.com/appium/python-client/pull/984), + [`328c8d3`](https://github.com/appium/python-client/commit/328c8d3d941352503d9ff69baa9dddea40eddfe4)) + +- Bump conventional-pr-action to v3 ([#989](https://github.com/appium/python-client/pull/989), + [`f256501`](https://github.com/appium/python-client/commit/f2565016b4a6f4a5fe5381d843960f46a71a1b02)) + +- Enable trigger + ([`f6e2b53`](https://github.com/appium/python-client/commit/f6e2b5335af8aef0e07c1d444fc85a0d7be6481d)) + +- Move the file + ([`85e921c`](https://github.com/appium/python-client/commit/85e921c8a1149d27f5136f723140f9770ee692dd)) + +- Use gha instead of Azure for iOS in Azure + ([#987](https://github.com/appium/python-client/pull/987), + [`5442e60`](https://github.com/appium/python-client/commit/5442e60b6c219cfb539d73c3d2277148b9f8311c)) + +### Documentation + +- Fix typo ([#992](https://github.com/appium/python-client/pull/992), + [`a9af896`](https://github.com/appium/python-client/commit/a9af896bc08735e2927c8ace90be2e420b09ce5e)) + +- Missing appium python client version in the compatibility matrix + ([`91aa2a1`](https://github.com/appium/python-client/commit/91aa2a11de3d4d9a7f36e062a932d7aba8d77ba1)) + +- Update docstring ([#986](https://github.com/appium/python-client/pull/986), + [`67a561d`](https://github.com/appium/python-client/commit/67a561d40b814b68b76381731a4da99805d79b3f)) + +### Testing + +- Fix one test ([#992](https://github.com/appium/python-client/pull/992), + [`a9af896`](https://github.com/appium/python-client/commit/a9af896bc08735e2927c8ace90be2e420b09ce5e)) + + +## v4.0.0 (2024-03-11) + +### Chores + +- Remove deprecated AppiumBy.WINDOWS_UI_AUTOMATION + ([#968](https://github.com/appium/python-client/pull/968), + [`706f3f5`](https://github.com/appium/python-client/commit/706f3f5e91b666f91f197e9149d959d6126b8b44)) + +- **deps-dev**: Update pylint requirement from ~=3.0.3 to ~=3.1.0 + ([#966](https://github.com/appium/python-client/pull/966), + [`3330b9a`](https://github.com/appium/python-client/commit/3330b9ae75f67b8c1b572f837d7f3f1e2cfe7a82)) + +- **deps-dev**: Update pytest requirement from ~=8.0 to ~=8.1 + ([#969](https://github.com/appium/python-client/pull/969), + [`9136957`](https://github.com/appium/python-client/commit/9136957c42190da413aa8ebf5173cbfe9b5fb39b)) + +- **deps-dev**: Update python-dateutil requirement from ~=2.8 to ~=2.9 + ([#967](https://github.com/appium/python-client/pull/967), + [`08d7fbb`](https://github.com/appium/python-client/commit/08d7fbb4c0346744d34c383ec849fcbcfedb0c09)) + +- **deps-dev**: Update tox requirement from ~=4.12 to ~=4.13 + ([#957](https://github.com/appium/python-client/pull/957), + [`12200e7`](https://github.com/appium/python-client/commit/12200e7adf18cfe145a735de7f083974a19d902f)) + +- **deps-dev**: Update tox requirement from ~=4.13 to ~=4.14 + ([#972](https://github.com/appium/python-client/pull/972), + [`6492c27`](https://github.com/appium/python-client/commit/6492c27339b661b4fcd518a92c1c45d288d9de88)) + +### Documentation + +- Update readme + ([`aca3593`](https://github.com/appium/python-client/commit/aca359309c6e16c0848ca27489458035e72f0c4a)) + +### Features + +- Remove MultiAction and TouchAction ([#960](https://github.com/appium/python-client/pull/960), + [`4d8db65`](https://github.com/appium/python-client/commit/4d8db65bfb672180a9bd0a52a3254ddd1f4c5eb0)) + +### Breaking Changes + +- Remove MultiAction and TouchAction as non-w3c WebDriver-defined methods. Please use w3c actions + instead. + + +## v3.2.1 (2024-02-25) + +### Bug Fixes + +- Unclosed file <_io.BufferedReader name error by proper cleanup of subprocess.Popen process + ([#965](https://github.com/appium/python-client/pull/965), + [`ac9965d`](https://github.com/appium/python-client/commit/ac9965da3839d4709625d2912abc577f52bc2dc1)) + + +## v3.2.0 (2024-02-23) + +### Bug Fixes + +- Add return self in MultiAction#add ([#964](https://github.com/appium/python-client/pull/964), + [`2e0ff4e`](https://github.com/appium/python-client/commit/2e0ff4e043eb8efd114d0f9f1f9f5c99a7d08d96)) + +### Chores + +- **deps**: Update selenium requirement from ~=4.15 to ~=4.17 + ([#948](https://github.com/appium/python-client/pull/948), + [`bdac0b8`](https://github.com/appium/python-client/commit/bdac0b89b51d70ffea12df15adaec86ba4a986a2)) + +- **deps**: Update selenium requirement from ~=4.17 to ~=4.18 + ([#958](https://github.com/appium/python-client/pull/958), + [`1c6dcdf`](https://github.com/appium/python-client/commit/1c6dcdf642aaaa46facc6174d80a339fa49684f4)) + +- **deps-dev**: Update pytest requirement from ~=7.4 to ~=8.0 + ([#953](https://github.com/appium/python-client/pull/953), + [`92583ce`](https://github.com/appium/python-client/commit/92583ce39003b740bb5e57bbf2114d283d884d22)) + +- **deps-dev**: Update tox requirement from ~=4.11 to ~=4.12 + ([#947](https://github.com/appium/python-client/pull/947), + [`5be0ea0`](https://github.com/appium/python-client/commit/5be0ea08ef291cff4651b23fb820812562e8cf0d)) + +### Documentation + +- Tweak docstring ([#961](https://github.com/appium/python-client/pull/961), + [`686d486`](https://github.com/appium/python-client/commit/686d4864185b86bc61038099be116ec68fe3c0c9)) + +- Update example in readme ([#945](https://github.com/appium/python-client/pull/945), + [`e2d238e`](https://github.com/appium/python-client/commit/e2d238e10a526ab041e9fed428d996e14adde1ce)) + +- Update links ([#944](https://github.com/appium/python-client/pull/944), + [`39a89c8`](https://github.com/appium/python-client/commit/39a89c86357d00ee12db2af74ea3906c118fdec0)) + +- Update W3C actions example in readme ([#946](https://github.com/appium/python-client/pull/946), + [`ea9e09e`](https://github.com/appium/python-client/commit/ea9e09e45c0650a39b7861b15ece85f8a77fdbc9)) + +### Features + +- Add pause in drag_and_drop ([#961](https://github.com/appium/python-client/pull/961), + [`686d486`](https://github.com/appium/python-client/commit/686d4864185b86bc61038099be116ec68fe3c0c9)) + + +## v3.1.1 (2023-12-14) + +### Bug Fixes + +- Self.command_executor instance in _update_command_executor + ([#940](https://github.com/appium/python-client/pull/940), + [`17639ea`](https://github.com/appium/python-client/commit/17639ea682c06fe5ea23fb5999dcf009b7baa36c)) + +- Typo in ActionHelpers ([#937](https://github.com/appium/python-client/pull/937), + [`63770e8`](https://github.com/appium/python-client/commit/63770e8cba8c1d9d4bb3324b621bc96b037d242f)) + +### Chores + +- **deps**: Update selenium requirement from ~=4.14 to ~=4.15 + ([#933](https://github.com/appium/python-client/pull/933), + [`876233e`](https://github.com/appium/python-client/commit/876233e115ef68839f614e89f3dd7bd523222d36)) + +- **deps-dev**: Update pylint requirement from ~=3.0.1 to ~=3.0.3 + ([#939](https://github.com/appium/python-client/pull/939), + [`69ca059`](https://github.com/appium/python-client/commit/69ca0595727043645d5c0d9488e2b51e77784075)) + +### Documentation + +- Address options in the migration guide ([#929](https://github.com/appium/python-client/pull/929), + [`1e281bf`](https://github.com/appium/python-client/commit/1e281bf085138ee85187770c263ffd91e5a83e58)) + +- Adress options in the migration guide ([#929](https://github.com/appium/python-client/pull/929), + [`1e281bf`](https://github.com/appium/python-client/commit/1e281bf085138ee85187770c263ffd91e5a83e58)) + +- Update changelog + ([`1a81153`](https://github.com/appium/python-client/commit/1a811534c92427885dfc1954deef4c2976d1c5b3)) + + +## v3.1.0 (2023-10-13) + +### Chores + +- **deps**: Update selenium requirement from ~=4.12 to ~=4.13 + ([#915](https://github.com/appium/python-client/pull/915), + [`894380b`](https://github.com/appium/python-client/commit/894380b697bef35f98510b41a8ddafb2a3d21aa8)) + +- **deps**: Update selenium requirement from ~=4.13 to ~=4.14 + ([#923](https://github.com/appium/python-client/pull/923), + [`6f1cf34`](https://github.com/appium/python-client/commit/6f1cf34aa61551aaf37eb68a534ce4f8aca6a683)) + +- **deps-dev**: Update pylint requirement from ~=2.17.5 to ~=3.0.1 + ([#922](https://github.com/appium/python-client/pull/922), + [`3d7324e`](https://github.com/appium/python-client/commit/3d7324e4aeae731e5eb01d3881f246a5bb753798)) + +### Continuous Integration + +- Use appium from the release branch + ([`8d58eb7`](https://github.com/appium/python-client/commit/8d58eb7e31331ea473d209c5bc42346fd7b8fe3d)) + +### Documentation + +- Update README.md ([#912](https://github.com/appium/python-client/pull/912), + [`fa7ba6e`](https://github.com/appium/python-client/commit/fa7ba6ee5ee91c914f69e34547993af62995462f)) + +- Update README.md for v3 ([#912](https://github.com/appium/python-client/pull/912), + [`fa7ba6e`](https://github.com/appium/python-client/commit/fa7ba6ee5ee91c914f69e34547993af62995462f)) + +### Features + +- Add missing platformVersion and browserName options + ([#925](https://github.com/appium/python-client/pull/925), + [`d93a6ca`](https://github.com/appium/python-client/commit/d93a6caadb071598454fb7af0569576258ac4093)) + + +## v3.0.0 (2023-09-08) + +### Bug Fixes + +- Add missing dependencies for types-python-dateutil + ([#891](https://github.com/appium/python-client/pull/891), + [`78bbb73`](https://github.com/appium/python-client/commit/78bbb73fdea80eac98468096d8e187f2def8f866)) + +- Handle the situation where payload is already a dictionary + ([#892](https://github.com/appium/python-client/pull/892), + [`9edf6eb`](https://github.com/appium/python-client/commit/9edf6ebfff75d4b2d84b67e6601bed764613aa8e)) + +### Chores + +- Run pre-commit autoupdate ([#890](https://github.com/appium/python-client/pull/890), + [`0cf35fc`](https://github.com/appium/python-client/commit/0cf35fc2be341beae51e5ec14407ca73f16eb29e)) + +- Update isort revision to 5.12.0 ([#889](https://github.com/appium/python-client/pull/889), + [`2853ac0`](https://github.com/appium/python-client/commit/2853ac0b8814cea4a6192c69b56b508a667042c8)) + +- **deps**: Update selenium requirement from ~=4.10 to ~=4.11 + ([#899](https://github.com/appium/python-client/pull/899), + [`2223f11`](https://github.com/appium/python-client/commit/2223f11b213162b21984fbece871a210fea48e39)) + +- **deps-dev**: Update mock requirement from ~=5.0 to ~=5.1 + ([#893](https://github.com/appium/python-client/pull/893), + [`5f11530`](https://github.com/appium/python-client/commit/5f11530f93306a3c4c669fce327b2aef30c5e58c)) + +- **deps-dev**: Update pylint requirement from ~=2.17.3 to ~=2.17.5 + ([#897](https://github.com/appium/python-client/pull/897), + [`60b8ed5`](https://github.com/appium/python-client/commit/60b8ed5f9e50d056171f45994d811f6970a3f7d9)) + +- **deps-dev**: Update pytest requirement from ~=7.2 to ~=7.4 + ([#884](https://github.com/appium/python-client/pull/884), + [`fb8415e`](https://github.com/appium/python-client/commit/fb8415edf11e5f7302e24df5c3354c0c69fe8776)) + +- **deps-dev**: Update tox requirement from ~=4.6 to ~=4.8 + ([#902](https://github.com/appium/python-client/pull/902), + [`d5b84b8`](https://github.com/appium/python-client/commit/d5b84b899e4abad6afd914d0e5342476cb955de2)) + +- **deps-dev**: Update tox requirement from ~=4.8 to ~=4.11 + ([#906](https://github.com/appium/python-client/pull/906), + [`e344b7a`](https://github.com/appium/python-client/commit/e344b7a27a55dceebcfda22842efa6dfe965e39b)) + +- **deps-dev**: Update typing-extensions requirement + ([#885](https://github.com/appium/python-client/pull/885), + [`43ad4db`](https://github.com/appium/python-client/commit/43ad4db272b8115e752177a8336a05dc3943e79e)) + +### Continuous Integration + +- Add pylint_quotes for pylint to use single quote as primary method + ([#886](https://github.com/appium/python-client/pull/886), + [`b142e00`](https://github.com/appium/python-client/commit/b142e00e3f47065752c80f71f2f8cb80bf2500f7)) + +### Documentation + +- Update changelogs and version + ([`6496619`](https://github.com/appium/python-client/commit/64966198b542cabd7bdb4d0dd6bdb1a4f8f0bf08)) + +- Update README.md ([#898](https://github.com/appium/python-client/pull/898), + [`a1792ff`](https://github.com/appium/python-client/commit/a1792ffa51f472dd631c406dcf2f92fe78ae47e5)) + +### Features + +- Update selenium dependency to 4.12 ([#908](https://github.com/appium/python-client/pull/908), + [`2e49569`](https://github.com/appium/python-client/commit/2e49569ed45751df4c6953466f9769336698c033)) + +### Refactoring + +- Remove several previously deprecated APIs + ([#909](https://github.com/appium/python-client/pull/909), + [`264f202`](https://github.com/appium/python-client/commit/264f202ca5cd5cbcb1a139ef5cc29095d12e2cce)) + +### Testing + +- Fix broken TestContextSwitching by replacing selendroid with ApiDemos + ([#895](https://github.com/appium/python-client/pull/895), + [`06fe1b5`](https://github.com/appium/python-client/commit/06fe1b5e558059c3608df9dcf3f66420e60952ee)) + +- Remove selendroid-test-app.apk from apps folder + ([#895](https://github.com/appium/python-client/pull/895), + [`06fe1b5`](https://github.com/appium/python-client/commit/06fe1b5e558059c3608df9dcf3f66420e60952ee)) + +- Remove unused import pytest from applications_tests.py + ([#895](https://github.com/appium/python-client/pull/895), + [`06fe1b5`](https://github.com/appium/python-client/commit/06fe1b5e558059c3608df9dcf3f66420e60952ee)) + +- Replace usage of selendroid app from 'test_install_app' in applications_tests.py + ([#895](https://github.com/appium/python-client/pull/895), + [`06fe1b5`](https://github.com/appium/python-client/commit/06fe1b5e558059c3608df9dcf3f66420e60952ee)) + +- Replace usage of selendroid app from 'test_install_app' in applications_tests.py + ([#891](https://github.com/appium/python-client/pull/891), + [`78bbb73`](https://github.com/appium/python-client/commit/78bbb73fdea80eac98468096d8e187f2def8f866)) + +- Selendroid cleanup ([#895](https://github.com/appium/python-client/pull/895), + [`06fe1b5`](https://github.com/appium/python-client/commit/06fe1b5e558059c3608df9dcf3f66420e60952ee)) + +### Breaking Changes + +- Removed obsolete all_sessions and session properties BREAKING CHANGE: Removed the obsolete + start_activity method BREAKING CHANGE: Removed the obsolete end_test_coverage method BREAKING + CHANGE: Removed the following obsolete arguments from the driver constructor: + desired_capabilities, browser_profile, proxy BREAKING CHANGE: Removed obsolete set_value and + set_text methods BREAKING CHANGE: Removed the obsolete MobileBy class BREAKING CHANGE: Removed + obsolete application management methods: launch_app, close_app, reset BREAKING CHANGE: Removed + obsolete IME methods: available_ime_engines, is_ime_active, activate_ime_engine, + deactivate_ime_engine, active_ime_engine + +- The minimum supported Python version set to 3.8 BREAKING CHANGE: The minimum supported selenium + version set to 4.12 + + +## v2.11.1 (2023-06-13) + +### Chores + +- Left a comment + ([`54a082e`](https://github.com/appium/python-client/commit/54a082e3fd2b20cb505a9464d51c6be91c16a926)) + + +## v2.11.0 (2023-06-09) + +### Chores + +- Set version with / ([#793](https://github.com/appium/python-client/pull/793), + [`f304f65`](https://github.com/appium/python-client/commit/f304f6509796580111240770a5f310fa6536f11c)) + +- Update comment ([#793](https://github.com/appium/python-client/pull/793), + [`f304f65`](https://github.com/appium/python-client/commit/f304f6509796580111240770a5f310fa6536f11c)) + +### Features + +- Make the UA format with same as other clients + ([#793](https://github.com/appium/python-client/pull/793), + [`f304f65`](https://github.com/appium/python-client/commit/f304f6509796580111240770a5f310fa6536f11c)) + + +## v2.10.2 (2023-06-08) + +### Bug Fixes + +- Update the constructor for compatibility with python client 4.10 + ([#879](https://github.com/appium/python-client/pull/879), + [`c0f38bf`](https://github.com/appium/python-client/commit/c0f38bf293fbae141bed8a9c49643a7b8b75efed)) + +### Chores + +- Nump the version + ([`8bb7b4c`](https://github.com/appium/python-client/commit/8bb7b4ccac8f54d2922e90ace30108ef7dd70057)) + +- Remove duplicated clean command ([#809](https://github.com/appium/python-client/pull/809), + [`2f45ef9`](https://github.com/appium/python-client/commit/2f45ef935c12dec2ab8de044ce6a1c1e0b9aa46f)) + +- Set the max selenium deps version ([#874](https://github.com/appium/python-client/pull/874), + [`2e7a6a3`](https://github.com/appium/python-client/commit/2e7a6a3e80852883d30d3d5e3859a5fdd1e29eb6)) + +- **deps-dev**: Update pytest-cov requirement from ~=4.0 to ~=4.1 + ([#872](https://github.com/appium/python-client/pull/872), + [`74f39ed`](https://github.com/appium/python-client/commit/74f39eda8129a45a9798b948733d726b76237973)) + +- **deps-dev**: Update tox requirement from ~=4.5 to ~=4.6 + ([#877](https://github.com/appium/python-client/pull/877), + [`a4e4118`](https://github.com/appium/python-client/commit/a4e411848f7e46af4d0484925dbbc55061aee5f7)) + +- **deps-dev**: Update typing-extensions requirement + ([#871](https://github.com/appium/python-client/pull/871), + [`1e4c574`](https://github.com/appium/python-client/commit/1e4c574718bdbbb00fb6c81b7499f1a746294ab3)) + +### Continuous Integration + +- Add py11 for the unit test ([#875](https://github.com/appium/python-client/pull/875), + [`5a4b6d0`](https://github.com/appium/python-client/commit/5a4b6d0729601b3fdb821e93ebe24bf2d248c65b)) + +- Add python 11 ([#874](https://github.com/appium/python-client/pull/874), + [`2e7a6a3`](https://github.com/appium/python-client/commit/2e7a6a3e80852883d30d3d5e3859a5fdd1e29eb6)) + +### Documentation + +- Address version management recommendation in the readme + ([#874](https://github.com/appium/python-client/pull/874), + [`2e7a6a3`](https://github.com/appium/python-client/commit/2e7a6a3e80852883d30d3d5e3859a5fdd1e29eb6)) + +- Improve usage examples ([#873](https://github.com/appium/python-client/pull/873), + [`1f6dec3`](https://github.com/appium/python-client/commit/1f6dec384e7c911a134662ee2393221d0af298b9)) + +- Merge the matrix pr into README.md ([#874](https://github.com/appium/python-client/pull/874), + [`2e7a6a3`](https://github.com/appium/python-client/commit/2e7a6a3e80852883d30d3d5e3859a5fdd1e29eb6)) + + +## v2.10.1 (2023-05-20) + +### Bug Fixes + +- W3C errors to exception classes mapping ([#869](https://github.com/appium/python-client/pull/869), + [`5c20a35`](https://github.com/appium/python-client/commit/5c20a358ae94c996ea3ddd4964ad828005b17801)) + + +## v2.10.0 (2023-05-11) + +### Bug Fixes + +- Update connection manager creation ([#864](https://github.com/appium/python-client/pull/864), + [`2dbce79`](https://github.com/appium/python-client/commit/2dbce790fe6b87ff489a3dcf1d4872f9e2873595)) + +### Chores + +- Bump and correct version + ([`49d38dd`](https://github.com/appium/python-client/commit/49d38ddb18fc7341bb901a6642f15823a7bc80b6)) + +- **deps**: Update selenium requirement from ~=4.7 to ~=4.9 + ([#852](https://github.com/appium/python-client/pull/852), + [`0cfa3ef`](https://github.com/appium/python-client/commit/0cfa3ef79ae93cb917ff76d446b57cefbec5ed8f)) + +- **deps-dev**: Update mypy requirement from ~=1.1 to ~=1.2 + ([#848](https://github.com/appium/python-client/pull/848), + [`bb76339`](https://github.com/appium/python-client/commit/bb76339bc6b9bc3ae8eab7de1c416a1ff906317e)) + +- **deps-dev**: Update pylint requirement from ~=2.17.1 to ~=2.17.2 + ([#847](https://github.com/appium/python-client/pull/847), + [`37e357b`](https://github.com/appium/python-client/commit/37e357b1371f0e76ddbe3d0954d3315df19c15d1)) + +- **deps-dev**: Update pylint requirement from ~=2.17.2 to ~=2.17.3 + ([#853](https://github.com/appium/python-client/pull/853), + [`4031de2`](https://github.com/appium/python-client/commit/4031de2aad7918da0e3b083fc2be5a37865e4d79)) + +- **deps-dev**: Update tox requirement from ~=4.4 to ~=4.5 + ([#854](https://github.com/appium/python-client/pull/854), + [`790ffed`](https://github.com/appium/python-client/commit/790ffedc4440db92666fb1a5b17193e7b5b88343)) + +### Refactoring + +- Move driver-specific commands to use extensions (part1) + ([#856](https://github.com/appium/python-client/pull/856), + [`622f3df`](https://github.com/appium/python-client/commit/622f3df7f3871e7f3442af29ff274d855b10bb49)) + +- Move driver-specific commands to use extensions (part2) + ([#859](https://github.com/appium/python-client/pull/859), + [`d988f3c`](https://github.com/appium/python-client/commit/d988f3c51c03f61cca39289450c009aafb0fe30a)) + + +## v2.9.0 (2023-04-01) + +### Bug Fixes + +- Set_value and set_text sent incorrect data + ([#831](https://github.com/appium/python-client/pull/831), + [`91dc04b`](https://github.com/appium/python-client/commit/91dc04bd4313227b860158cd0d72d45723bcc664)) + +### Chores + +- **deps-dev**: Update mypy requirement from ~=0.991 to ~=1.0 + ([#828](https://github.com/appium/python-client/pull/828), + [`2e19f0d`](https://github.com/appium/python-client/commit/2e19f0d8139c2ab265346f033b809cac4b49d118)) + +- **deps-dev**: Update mypy requirement from ~=1.0 to ~=1.1 + ([#836](https://github.com/appium/python-client/pull/836), + [`8c845c1`](https://github.com/appium/python-client/commit/8c845c1a33c9ed6045e7f60884608ba1f7430817)) + +- **deps-dev**: Update pylint requirement from ~=2.15.10 to ~=2.16.0 + ([#826](https://github.com/appium/python-client/pull/826), + [`6acf1f0`](https://github.com/appium/python-client/commit/6acf1f0577940c3cdf216d27ba1cedf122abee60)) + +- **deps-dev**: Update pylint requirement from ~=2.16.0 to ~=2.16.1 + ([#827](https://github.com/appium/python-client/pull/827), + [`cb7d8c5`](https://github.com/appium/python-client/commit/cb7d8c508a23e5a57e70cfd6d942255c9109ef65)) + +- **deps-dev**: Update pylint requirement from ~=2.16.1 to ~=2.16.2 + ([#829](https://github.com/appium/python-client/pull/829), + [`0cb646d`](https://github.com/appium/python-client/commit/0cb646d28053afa82b16a3daea0a408498893de8)) + +- **deps-dev**: Update pylint requirement from ~=2.16.2 to ~=2.16.3 + ([#834](https://github.com/appium/python-client/pull/834), + [`11beb71`](https://github.com/appium/python-client/commit/11beb71f7b9d9ae4b96abac62a8ef901165bff06)) + +- **deps-dev**: Update pylint requirement from ~=2.16.3 to ~=2.17.0 + ([#838](https://github.com/appium/python-client/pull/838), + [`cbcc539`](https://github.com/appium/python-client/commit/cbcc539827658c7aaa16147f8da7907405c59031)) + +- **deps-dev**: Update pylint requirement from ~=2.17.0 to ~=2.17.1 + ([#843](https://github.com/appium/python-client/pull/843), + [`6d558c0`](https://github.com/appium/python-client/commit/6d558c0411e3e46d06f03c7fe72337e72699e599)) + +- **deps-dev**: Update tox requirement from ~=4.3 to ~=4.4 + ([#823](https://github.com/appium/python-client/pull/823), + [`ec81af5`](https://github.com/appium/python-client/commit/ec81af5cec22ea1f9b390b8147d95895040789bb)) + +- **deps-dev**: Update typing-extensions requirement + ([#830](https://github.com/appium/python-client/pull/830), + [`c2a80fa`](https://github.com/appium/python-client/commit/c2a80fa716d3ff12b850019dd6638bfde909408f)) + +### Features + +- Can provide a custom connection ([#844](https://github.com/appium/python-client/pull/844), + [`2c92c04`](https://github.com/appium/python-client/commit/2c92c04b3d470ec00ce96d2696483ae02f4df0d8)) + +- Respect the given executor ([#844](https://github.com/appium/python-client/pull/844), + [`2c92c04`](https://github.com/appium/python-client/commit/2c92c04b3d470ec00ce96d2696483ae02f4df0d8)) + + +## v2.8.1 (2023-01-20) + +### Chores + +- Update docstring in touch_action.py ([#797](https://github.com/appium/python-client/pull/797), + [`c8cb24a`](https://github.com/appium/python-client/commit/c8cb24a1a7c7e60821ef25462110283b4bc0408e)) + +- Update precommit ([#787](https://github.com/appium/python-client/pull/787), + [`c78e240`](https://github.com/appium/python-client/commit/c78e2406b07ffefceff35ed3ffd52e89ef521dfd)) + +- **deps**: Update selenium requirement from ~=4.5 to ~=4.7 + ([#801](https://github.com/appium/python-client/pull/801), + [`ab13d72`](https://github.com/appium/python-client/commit/ab13d7265d2956995806f73368e242170395d2b3)) + +- **deps**: Update sphinx requirement from <6.0,>=4.0 to >=4.0,<7.0 + ([#814](https://github.com/appium/python-client/pull/814), + [`8b96d05`](https://github.com/appium/python-client/commit/8b96d054f2474f1a2349f144cd3f30d8613962b3)) + +- **deps-dev**: Update black requirement from ~=22.10.0 to ~=22.12.0 + ([#807](https://github.com/appium/python-client/pull/807), + [`8c51dc3`](https://github.com/appium/python-client/commit/8c51dc3d06f6ffdfe6f556176012998fe8db524f)) + +- **deps-dev**: Update isort requirement from ~=5.10 to ~=5.11 + ([#808](https://github.com/appium/python-client/pull/808), + [`8d8fb02`](https://github.com/appium/python-client/commit/8d8fb02bdb081f06cd51b8c39b74971c1ab81a55)) + +- **deps-dev**: Update mock requirement from ~=4.0 to ~=5.0 + ([#812](https://github.com/appium/python-client/pull/812), + [`3c59823`](https://github.com/appium/python-client/commit/3c59823e94b5c19e60b00addc3454868897ac1df)) + +- **deps-dev**: Update mypy requirement from ~=0.982 to ~=0.991 + ([#798](https://github.com/appium/python-client/pull/798), + [`47db483`](https://github.com/appium/python-client/commit/47db48349d7b90bc18f0df1b926e24287ccb5a06)) + +- **deps-dev**: Update pre-commit requirement from ~=2.20 to ~=2.21 + ([#811](https://github.com/appium/python-client/pull/811), + [`55ac01f`](https://github.com/appium/python-client/commit/55ac01faaff90dd14a87b94193fb30bb8511f124)) + +- **deps-dev**: Update pylint requirement from ~=2.15.3 to ~=2.15.4 + ([#788](https://github.com/appium/python-client/pull/788), + [`a41b2fc`](https://github.com/appium/python-client/commit/a41b2fc6c42a6f6318a08b0a7c19f0785cff41d4)) + +- **deps-dev**: Update pylint requirement from ~=2.15.4 to ~=2.15.5 + ([#790](https://github.com/appium/python-client/pull/790), + [`6c27689`](https://github.com/appium/python-client/commit/6c2768931fc11a17dbed9f2b635ba4628c3426cc)) + +- **deps-dev**: Update pylint requirement from ~=2.15.5 to ~=2.15.6 + ([#799](https://github.com/appium/python-client/pull/799), + [`63464f8`](https://github.com/appium/python-client/commit/63464f8a1c1e85afa2b03897e745f1c17d2fcb6c)) + +- **deps-dev**: Update pylint requirement from ~=2.15.6 to ~=2.15.7 + ([#800](https://github.com/appium/python-client/pull/800), + [`36c602c`](https://github.com/appium/python-client/commit/36c602c4efc8385ae0cbf916cfa4e8e50b7868c9)) + +- **deps-dev**: Update pylint requirement from ~=2.15.7 to ~=2.15.8 + ([#804](https://github.com/appium/python-client/pull/804), + [`3903e29`](https://github.com/appium/python-client/commit/3903e29d73cc6bd69fc9bf353a6c32714ec5ab97)) + +- **deps-dev**: Update pylint requirement from ~=2.15.8 to ~=2.15.9 + ([#810](https://github.com/appium/python-client/pull/810), + [`644aa72`](https://github.com/appium/python-client/commit/644aa724b7939aae01bdcd055b0541bf879bde2b)) + +- **deps-dev**: Update pylint requirement from ~=2.15.9 to ~=2.15.10 + ([#816](https://github.com/appium/python-client/pull/816), + [`01d96fd`](https://github.com/appium/python-client/commit/01d96fd8a4645c1ba0df89a8bdd372693f5c98b0)) + +- **deps-dev**: Update pytest requirement from ~=7.1 to ~=7.2 + ([#791](https://github.com/appium/python-client/pull/791), + [`d5f7a25`](https://github.com/appium/python-client/commit/d5f7a2571f3bed15ad26edb5d1fd907c4508f965)) + +- **deps-dev**: Update tox requirement from ~=3.26 to ~=3.27 + ([#792](https://github.com/appium/python-client/pull/792), + [`df5bcb8`](https://github.com/appium/python-client/commit/df5bcb8725e3fead2ada8e0f368fa9cd24ac3555)) + +- **deps-dev**: Update tox requirement from ~=3.27 to ~=4.0 + ([#806](https://github.com/appium/python-client/pull/806), + [`45ca8cf`](https://github.com/appium/python-client/commit/45ca8cf6069dd515c1bd6bc4ae7656f7e2e7ed3a)) + +- **deps-dev**: Update tox requirement from ~=4.0 to ~=4.1 + ([#813](https://github.com/appium/python-client/pull/813), + [`827011e`](https://github.com/appium/python-client/commit/827011e2dbe39c887edfb562848b9fb3eff54a1b)) + +- **deps-dev**: Update tox requirement from ~=4.1 to ~=4.2 + ([#815](https://github.com/appium/python-client/pull/815), + [`7941203`](https://github.com/appium/python-client/commit/79412033c600a1369559d8b5cce084c471f072d9)) + +- **deps-dev**: Update tox requirement from ~=4.2 to ~=4.3 + ([#817](https://github.com/appium/python-client/pull/817), + [`0651afc`](https://github.com/appium/python-client/commit/0651afcf2350e8dec41779d242d7c5972b39b174)) + +### Features + +- Add status tentatively ([#820](https://github.com/appium/python-client/pull/820), + [`431aba1`](https://github.com/appium/python-client/commit/431aba1de859df4d5b34bd1d0216d4a9caa53a0d)) + + +## v2.7.1 (2022-10-11) + +### Chores + +- **deps**: Update selenium requirement from ~=4.4 to ~=4.5 + ([#780](https://github.com/appium/python-client/pull/780), + [`905eca6`](https://github.com/appium/python-client/commit/905eca69daa252238def9618ddd52588e5a28976)) + +- **deps-dev**: Update black requirement from ~=22.8.0 to ~=22.10.0 + ([#784](https://github.com/appium/python-client/pull/784), + [`c9e2632`](https://github.com/appium/python-client/commit/c9e2632e64214c96c1c19aad53734016dff59dfc)) + +- **deps-dev**: Update mypy requirement from ~=0.971 to ~=0.981 + ([#777](https://github.com/appium/python-client/pull/777), + [`38c1fb3`](https://github.com/appium/python-client/commit/38c1fb31199cb22952e1089cb5a8da583f1dfff9)) + +- **deps-dev**: Update mypy requirement from ~=0.981 to ~=0.982 + ([#782](https://github.com/appium/python-client/pull/782), + [`d7edba4`](https://github.com/appium/python-client/commit/d7edba49973613d5d6d74bebdb6e3c7b75f0dcec)) + +- **deps-dev**: Update pylint requirement from ~=2.15.2 to ~=2.15.3 + ([#774](https://github.com/appium/python-client/pull/774), + [`7d82821`](https://github.com/appium/python-client/commit/7d82821a03a42827ebe13c71c2742e175545a192)) + +- **deps-dev**: Update pytest-cov requirement from ~=3.0 to ~=4.0 + ([#779](https://github.com/appium/python-client/pull/779), + [`62bc72c`](https://github.com/appium/python-client/commit/62bc72ca114967490784823b1e1da6d11791f492)) + +- **deps-dev**: Update typing-extensions requirement + ([#783](https://github.com/appium/python-client/pull/783), + [`1848fdf`](https://github.com/appium/python-client/commit/1848fdf915dc57d22cc04964b69239f6bc3c7250)) + +### Continuous Integration + +- Comment out win for now ([#773](https://github.com/appium/python-client/pull/773), + [`46a09b8`](https://github.com/appium/python-client/commit/46a09b81028c0d1b87f21d4d1c42de674c074e07)) + +- Remove unit test section ([#773](https://github.com/appium/python-client/pull/773), + [`46a09b8`](https://github.com/appium/python-client/commit/46a09b81028c0d1b87f21d4d1c42de674c074e07)) + +- Run unit tests on actions ([#773](https://github.com/appium/python-client/pull/773), + [`46a09b8`](https://github.com/appium/python-client/commit/46a09b81028c0d1b87f21d4d1c42de674c074e07)) + +- Tweak trigger ([#773](https://github.com/appium/python-client/pull/773), + [`46a09b8`](https://github.com/appium/python-client/commit/46a09b81028c0d1b87f21d4d1c42de674c074e07)) + +### Refactoring + +- Make service startup failures more helpful + ([#786](https://github.com/appium/python-client/pull/786), + [`033071d`](https://github.com/appium/python-client/commit/033071d0603b9bade3bc49c92459b064ae574a02)) + + +## v2.7.0 (2022-09-22) + +### Bug Fixes + +- Move dev-only dependencies to [dev-packages] section + ([#772](https://github.com/appium/python-client/pull/772), + [`1a0246c`](https://github.com/appium/python-client/commit/1a0246cec0c3bf5f98fbf8ac20e1d5258f8e3e4f)) + +### Chores + +- **deps**: Update pylint requirement from ~=2.15.2 to ~=2.15.3 + ([#770](https://github.com/appium/python-client/pull/770), + [`83f3c81`](https://github.com/appium/python-client/commit/83f3c817bd6f979b1febdf4d643640b2d6c49d82)) + +### Continuous Integration + +- Fix runner name + ([`02a39a3`](https://github.com/appium/python-client/commit/02a39a31b9829635e91498ceea2099af5a4a493f)) + +### Documentation + +- Update changelog for 2.6.2 + ([`7a5fa26`](https://github.com/appium/python-client/commit/7a5fa26f3d066d311952a1b39983afa3e2c44547)) + +### Features + +- Add appArguments option to WindowsOptions + ([#768](https://github.com/appium/python-client/pull/768), + [`85cb104`](https://github.com/appium/python-client/commit/85cb1042373012d4c21fab2482090fe25a9422ac)) + + +## v2.6.2 (2022-09-16) + +### Bug Fixes + +- Use total_seconds property of timedelta ([#767](https://github.com/appium/python-client/pull/767), + [`b31b5eb`](https://github.com/appium/python-client/commit/b31b5ebe19eeeaf4216c712d213ae93f576f5943)) + +### Chores + +- **deps**: Bump black from 22.6.0 to 22.8.0 + ([#763](https://github.com/appium/python-client/pull/763), + [`806d5df`](https://github.com/appium/python-client/commit/806d5dff7f5b60723d68f249e5da7656db6a836b)) + +- **deps**: Update astroid requirement from ~=2.9 to ~=2.12 + ([#762](https://github.com/appium/python-client/pull/762), + [`89193b9`](https://github.com/appium/python-client/commit/89193b9b677cd4bb7c163a778450d17723c89798)) + +- **deps**: Update pylint requirement from ~=2.14.5 to ~=2.15.0 + ([#761](https://github.com/appium/python-client/pull/761), + [`130766c`](https://github.com/appium/python-client/commit/130766c97910d44c997bffbb563edb2c8d6629b3)) + +- **deps**: Update pylint requirement from ~=2.15.0 to ~=2.15.2 + ([#765](https://github.com/appium/python-client/pull/765), + [`7734ea2`](https://github.com/appium/python-client/commit/7734ea2fe3668809545db2729bb41c0f383a77cb)) + +- **deps**: Update tox requirement from ~=3.25 to ~=3.26 + ([#766](https://github.com/appium/python-client/pull/766), + [`fbfccca`](https://github.com/appium/python-client/commit/fbfcccab17b49aa77063a80969a013b8246be861)) + +### Continuous Integration + +- Add Conventional commit format validation + ([#764](https://github.com/appium/python-client/pull/764), + [`047e927`](https://github.com/appium/python-client/commit/047e9277db99c97b00f3fed7e008e3227a8e9f34)) + +- Update Conventional Commits config preset + ([`98ab48d`](https://github.com/appium/python-client/commit/98ab48d6a3ebf108a4c45de20ef55b4aec5b09ea)) + +### Documentation + +- Update changelog for 2.6.1 + ([`0a929c6`](https://github.com/appium/python-client/commit/0a929c6dc3a98a191995d70ba0160aaf256313f7)) + + +## v2.6.1 (2022-08-11) + +### Bug Fixes + +- Backwards compatible behaviour of swipe and scroll in action_helpers + ([#744](https://github.com/appium/python-client/pull/744), + [`677c1a2`](https://github.com/appium/python-client/commit/677c1a2121053a0435f3e0d8f7798077a41aa067)) + +- Fix options in mac2 ([#759](https://github.com/appium/python-client/pull/759), + [`0b711ae`](https://github.com/appium/python-client/commit/0b711aeb304e272f7b1a84ba59aa2bbd1edb4fb0)) + +- Move py.typed to the hierarchy root ([#751](https://github.com/appium/python-client/pull/751), + [`af83bba`](https://github.com/appium/python-client/commit/af83bba3b75d501bb063313e7edb3216765a9042)) + +- Typos/copypaste in various options ([#750](https://github.com/appium/python-client/pull/750), + [`c0b80dc`](https://github.com/appium/python-client/commit/c0b80dc8e8acf2f1c79a9e871f4ca23fb6d0b71d)) + +### Chores + +- **deps**: Update mypy requirement from ~=0.961 to ~=0.971 + ([#749](https://github.com/appium/python-client/pull/749), + [`2a41c39`](https://github.com/appium/python-client/commit/2a41c398b588d88030f5fd128487b9d3eeb0afb5)) + +- **deps**: Update pylint requirement from ~=2.14.3 to ~=2.14.4 + ([#742](https://github.com/appium/python-client/pull/742), + [`23ed3be`](https://github.com/appium/python-client/commit/23ed3be57d613aae4dc7b1737f35e2577ffcc706)) + +- **deps**: Update pylint requirement from ~=2.14.4 to ~=2.14.5 + ([#747](https://github.com/appium/python-client/pull/747), + [`8e10ad8`](https://github.com/appium/python-client/commit/8e10ad83ad1fc0948759a3830ce3d90cd0f53b46)) + +- **deps**: Update selenium requirement from ~=4.3 to ~=4.4 + ([#757](https://github.com/appium/python-client/pull/757), + [`ff50af0`](https://github.com/appium/python-client/commit/ff50af081c75817f7e4bbfc582fcb0755214eac3)) + +- **deps**: Update typing-extensions requirement from ~=4.2 to ~=4.3 + ([#745](https://github.com/appium/python-client/pull/745), + [`8f53696`](https://github.com/appium/python-client/commit/8f53696db85f00dcdaf7d8b969c30bcc24ce7d1d)) + +- **deps-dev**: Update pre-commit requirement from ~=2.19 to ~=2.20 + ([#746](https://github.com/appium/python-client/pull/746), + [`217acf1`](https://github.com/appium/python-client/commit/217acf168e4c772d67a745509a9e51e70e643c10)) + +### Documentation + +- Update changelog for 2.6.0 + ([`970f853`](https://github.com/appium/python-client/commit/970f8533e4b75c01fd592912a446344327dc0ac4)) + + +## v2.6.0 (2022-06-28) + +### Chores + +- Improve autocompletion for methods returning self instance + ([#739](https://github.com/appium/python-client/pull/739), + [`ce4de83`](https://github.com/appium/python-client/commit/ce4de83b443b050e295c5c1af0938ce720198bc8)) + +- **deps**: Bump black from 22.3.0 to 22.6.0 + ([#741](https://github.com/appium/python-client/pull/741), + [`ac39368`](https://github.com/appium/python-client/commit/ac39368b57bcb91ab72bdf09ed7b19f96a9f6544)) + +### Documentation + +- Update changelog for 2.5.0 + ([`50458bb`](https://github.com/appium/python-client/commit/50458bb3b7ebc74a8a1d417c450b95e43201f0b1)) + +### Features + +- Add Android drivers options ([#740](https://github.com/appium/python-client/pull/740), + [`470e836`](https://github.com/appium/python-client/commit/470e83674ecce5e5ff947427ed0c443cb7df4ae1)) + +### Refactoring + +- Remove previously deprecated methods and mark reset/close/launch APIs as deprecated + ([#738](https://github.com/appium/python-client/pull/738), + [`4c166f4`](https://github.com/appium/python-client/commit/4c166f45516a432ec807195f91fc2f208a3a3c08)) + + +## v2.5.0 (2022-06-25) + +### Chores + +- Bump version to 2.4.0 + ([`c400357`](https://github.com/appium/python-client/commit/c400357ca0028e7a83124a91331cf69c31be91c4)) + +- **deps**: Update pylint requirement from ~=2.14.1 to ~=2.14.2 + ([#725](https://github.com/appium/python-client/pull/725), + [`cc999ce`](https://github.com/appium/python-client/commit/cc999cea4a34510b42af22663c9c8d5037ab9bb2)) + +- **deps**: Update pylint requirement from ~=2.14.1 to ~=2.14.2 + ([#725](https://github.com/appium/python-client/pull/725), + [`15b106c`](https://github.com/appium/python-client/commit/15b106c5769fc7b5307f8daecdcfbc7f3cc7dea4)) + +- **deps**: Update pylint requirement from ~=2.14.2 to ~=2.14.3 + ([#733](https://github.com/appium/python-client/pull/733), + [`0a0cff2`](https://github.com/appium/python-client/commit/0a0cff2e02ad19a2cb336c3e92fcc7378ed57fe2)) + +- **deps**: Update selenium requirement from ~=4.2 to ~=4.3 + ([#736](https://github.com/appium/python-client/pull/736), + [`6eca124`](https://github.com/appium/python-client/commit/6eca12427bcd8be6fd6760193b0321d5bc088a2b)) + +### Features + +- Add Gecko driver options ([#735](https://github.com/appium/python-client/pull/735), + [`b4e17a3`](https://github.com/appium/python-client/commit/b4e17a3a1efe33f08ac9ef883891b9fad4449a41)) + +- Add Mac2Driver options ([#730](https://github.com/appium/python-client/pull/730), + [`312c229`](https://github.com/appium/python-client/commit/312c229fa89b6387236553c6f036085a835d3ed8)) + +- Add Safari driver options ([#731](https://github.com/appium/python-client/pull/731), + [`2201e90`](https://github.com/appium/python-client/commit/2201e90eec53cd3df29ed9cfcb5ba94d300fb7a0)) + +- Add Windows driver options ([#732](https://github.com/appium/python-client/pull/732), + [`d480eba`](https://github.com/appium/python-client/commit/d480ebaa1fcc52239d2a71d288e73e103f504429)) + +- Add xcuitest driver options ([#737](https://github.com/appium/python-client/pull/737), + [`0264d81`](https://github.com/appium/python-client/commit/0264d81d60c30b187da7b2e58cc67f6aad0def5f)) + +### Refactoring + +- Make system_port and system_host options common + ([#734](https://github.com/appium/python-client/pull/734), + [`5b958b5`](https://github.com/appium/python-client/commit/5b958b53f1df486314ca82cb35adce9161147aef)) + + +## v2.4.0 (2022-06-17) + +### Chores + +- Add better error handling for session creation responses + ([#727](https://github.com/appium/python-client/pull/727), + [`22dfeca`](https://github.com/appium/python-client/commit/22dfeca560f8f0d5527bfdc0e0200055ee3e30f5)) + +- Update comments to locator patches ([#724](https://github.com/appium/python-client/pull/724), + [`0fcea82`](https://github.com/appium/python-client/commit/0fcea82c9bcaf9137bc7b1591c37b092927751a2)) + +### Documentation + +- Update changelog for 2.3.0 + ([`8321b9c`](https://github.com/appium/python-client/commit/8321b9c5122b1f848c34d0dc230e38d522e09fa9)) + +### Features + +- Add common options ([#728](https://github.com/appium/python-client/pull/728), + [`60ec7ce`](https://github.com/appium/python-client/commit/60ec7ce69a9224eab28af3e89bf9ea1acb920ac4)) + + +## v2.3.0 (2022-06-13) + +### Chores + +- Disable pylint checks fail CI ([#719](https://github.com/appium/python-client/pull/719), + [`a394281`](https://github.com/appium/python-client/commit/a3942815a7b60548785db0ea2f8506b25b0693e2)) + +- **deps**: Update mypy requirement from ~=0.942 to ~=0.950 + ([#712](https://github.com/appium/python-client/pull/712), + [`336a762`](https://github.com/appium/python-client/commit/336a76257e809f5562e287d68322d9f256d880c7)) + +- **deps**: Update mypy requirement from ~=0.950 to ~=0.960 + ([#714](https://github.com/appium/python-client/pull/714), + [`956417a`](https://github.com/appium/python-client/commit/956417a55de28123bcf2404ebbbc95b0d9fcd072)) + +- **deps**: Update mypy requirement from ~=0.960 to ~=0.961 + ([#718](https://github.com/appium/python-client/pull/718), + [`0ff83df`](https://github.com/appium/python-client/commit/0ff83df100a4bae2d5547e0ba501b501890f6374)) + +- **deps**: Update selenium requirement from ~=4.1 to ~=4.2 + ([#715](https://github.com/appium/python-client/pull/715), + [`697bb64`](https://github.com/appium/python-client/commit/697bb64db15378dfbcd6b4bc3f4931da520259a5)) + +- **deps**: Update sphinx requirement from <5.0,>=4.0 to >=4.0,<6.0 + ([#716](https://github.com/appium/python-client/pull/716), + [`1385efc`](https://github.com/appium/python-client/commit/1385efc80d87cb50c600548229ff74d9106c402b)) + +- **deps**: Update tox requirement from ~=3.24 to ~=3.25 + ([#709](https://github.com/appium/python-client/pull/709), + [`f5b0526`](https://github.com/appium/python-client/commit/f5b0526f903eee98f443b47d6fec7a4f1d0a4838)) + +- **deps**: Update typing-extensions requirement from ~=4.1 to ~=4.2 + ([#711](https://github.com/appium/python-client/pull/711), + [`cb1a4ea`](https://github.com/appium/python-client/commit/cb1a4eaed0628a064deb70ba04fe5a5cd53312a4)) + +- **deps-dev**: Update pre-commit requirement from ~=2.17 to ~=2.18 + ([#708](https://github.com/appium/python-client/pull/708), + [`4f8064f`](https://github.com/appium/python-client/commit/4f8064fff2d6d7f197112f2e83be0eacbbec4265)) + +- **deps-dev**: Update pre-commit requirement from ~=2.18 to ~=2.19 + ([#713](https://github.com/appium/python-client/pull/713), + [`df33c85`](https://github.com/appium/python-client/commit/df33c85371d933ed0678f759ccf660343c66662a)) + +### Documentation + +- Update README with the new options format + ([#722](https://github.com/appium/python-client/pull/722), + [`a2bba19`](https://github.com/appium/python-client/commit/a2bba19360a6205fc6f0679b595ae560181e70c3)) + +### Features + +- Add base options for all supported automation names + ([#721](https://github.com/appium/python-client/pull/721), + [`d4c44b4`](https://github.com/appium/python-client/commit/d4c44b4b68c611be88007ee666ee8f59c79ce9f1)) + +- Add support for w3c options ([#720](https://github.com/appium/python-client/pull/720), + [`c27138c`](https://github.com/appium/python-client/commit/c27138c0505a6595f9c5f48f3e4d3ccb996301cd)) + +### Testing + +- Use Appium2 to run functional tests ([#723](https://github.com/appium/python-client/pull/723), + [`b267665`](https://github.com/appium/python-client/commit/b26766583830ae83e20416629c8bdd24b58e5658)) + + +## v2.2.0 (2022-03-30) + +### Chores + +- Relax selenium version as same as before + ([`96681e9`](https://github.com/appium/python-client/commit/96681e924b4310bcaa23c839746b4923925a2d96)) + +- **deps**: Bump black from 22.1.0 to 22.3.0 + ([#705](https://github.com/appium/python-client/pull/705), + [`79406bc`](https://github.com/appium/python-client/commit/79406bc044f564261e83d6a5ae689b0adcfc7632)) + +- **deps**: Update mypy requirement from ~=0.930 to ~=0.941 + ([#696](https://github.com/appium/python-client/pull/696), + [`ce526e6`](https://github.com/appium/python-client/commit/ce526e6f587adcf9c37690dd1a427ec71871de3e)) + +- **deps**: Update mypy requirement from ~=0.941 to ~=0.942 + ([#703](https://github.com/appium/python-client/pull/703), + [`56ce5b0`](https://github.com/appium/python-client/commit/56ce5b029a2e878f0fe0c97042f58372327d7a64)) + +- **deps**: Update pylint requirement from ~=2.12 to ~=2.13 + ([#702](https://github.com/appium/python-client/pull/702), + [`8826bb3`](https://github.com/appium/python-client/commit/8826bb35ea529a480226361b87b3b345206a6493)) + +- **deps**: Update pytest requirement from ~=7.0 to ~=7.1 + ([#694](https://github.com/appium/python-client/pull/694), + [`7dbf323`](https://github.com/appium/python-client/commit/7dbf3237bff5b503049c47d5815250acb8d6180a)) + +- **deps**: Update typing-extensions requirement from ~=4.0 to ~=4.1 + ([#684](https://github.com/appium/python-client/pull/684), + [`23553df`](https://github.com/appium/python-client/commit/23553dfc7cb1a87e2fa79a81bb0f36b00bdc5169)) + +### Documentation + +- Update missing changelog + ([`c972382`](https://github.com/appium/python-client/commit/c97238216e828266819f82538c77a993fdf39cf2)) + +### Features + +- Add non-w3c but still need commands ([#701](https://github.com/appium/python-client/pull/701), + [`09a0cd0`](https://github.com/appium/python-client/commit/09a0cd0c72cc9e63c26c516f8bd8a4ac7b211808)) + + +## v2.1.4 (2022-02-27) + + +## v2.1.3 (2022-02-25) + +### Chores + +- Bump mypy ([#675](https://github.com/appium/python-client/pull/675), + [`72f942d`](https://github.com/appium/python-client/commit/72f942d2152ee1b0d7539f3bf2705b50d01133f7)) + +- Restrict selenium client version ([#686](https://github.com/appium/python-client/pull/686), + [`2c04e4c`](https://github.com/appium/python-client/commit/2c04e4ce59ed04b91088ca55d1d3698653de6ebc)) + +- **deps**: Bump black from 21.12b0 to 22.1.0 + ([#681](https://github.com/appium/python-client/pull/681), + [`7a7be33`](https://github.com/appium/python-client/commit/7a7be33827f8a92c3efbd9003c537ae259ad59cd)) + +- **deps**: Update pytest requirement from ~=6.2 to ~=7.0 + ([#682](https://github.com/appium/python-client/pull/682), + [`588f83f`](https://github.com/appium/python-client/commit/588f83f28007d58e03c17759a5a5b807f4a08ae8)) + +- **deps-dev**: Update pre-commit requirement from ~=2.16 to ~=2.17 + ([#678](https://github.com/appium/python-client/pull/678), + [`71410ba`](https://github.com/appium/python-client/commit/71410bab56522ae3ca36ee7fcb6dee349d6bc65a)) + +### Refactoring + +- Update types descriptions for mixin classes + ([#677](https://github.com/appium/python-client/pull/677), + [`895cde1`](https://github.com/appium/python-client/commit/895cde1aa674aaab2f958ae251de0daefd049c02)) + +### Testing + +- Update find element/s methods ([#674](https://github.com/appium/python-client/pull/674), + [`7dbf4f2`](https://github.com/appium/python-client/commit/7dbf4f2f7ce43f60eded19fa247bb2177b65bafd)) + +- Update tests to use find_element(by...) ([#674](https://github.com/appium/python-client/pull/674), + [`7dbf4f2`](https://github.com/appium/python-client/commit/7dbf4f2f7ce43f60eded19fa247bb2177b65bafd)) + + +## v2.1.2 (2021-12-30) + +### Bug Fixes + +- Default duration in tap ([#673](https://github.com/appium/python-client/pull/673), + [`24b50d8`](https://github.com/appium/python-client/commit/24b50d8138ea6ae008b0557991c0b5dcd75a15d0)) + + +## v2.1.1 (2021-12-24) + +### Chores + +- Specify touch ([#670](https://github.com/appium/python-client/pull/670), + [`6b21a67`](https://github.com/appium/python-client/commit/6b21a6713ff34eb5b8fca71fc24105ff6d2e1c0f)) + +- **deps**: Bump black from 21.11b1 to 21.12b0 + ([#664](https://github.com/appium/python-client/pull/664), + [`02d6c8c`](https://github.com/appium/python-client/commit/02d6c8c5d21a1b3b135a5904edfa6acd03f1ac18)) + +- **deps**: Update astroid requirement from ~=2.8 to ~=2.9 + ([#661](https://github.com/appium/python-client/pull/661), + [`758b2cd`](https://github.com/appium/python-client/commit/758b2cd37f075a08f492575e7b938206f2828fd6)) + +- **deps**: Update pylint requirement from ~=2.11 to ~=2.12 + ([#662](https://github.com/appium/python-client/pull/662), + [`6c7c80c`](https://github.com/appium/python-client/commit/6c7c80c0c7211b9d5a2034a52d576738e53bdccb)) + +- **deps-dev**: Update pre-commit requirement from ~=2.15 to ~=2.16 + ([#663](https://github.com/appium/python-client/pull/663), + [`1b94c5f`](https://github.com/appium/python-client/commit/1b94c5fa3568b90dc90d1d4bb5634a573d07aa5a)) + +### Continuous Integration + +- Remove ==2021.5.29 ([#653](https://github.com/appium/python-client/pull/653), + [`e5cb9ab`](https://github.com/appium/python-client/commit/e5cb9abee0272399289d698fba06a7ddd5fbd039)) + +### Features + +- Use 'touch' pointer action ([#670](https://github.com/appium/python-client/pull/670), + [`6b21a67`](https://github.com/appium/python-client/commit/6b21a6713ff34eb5b8fca71fc24105ff6d2e1c0f)) + + +## v2.1.0 (2021-11-26) + +### Chores + +- Add deprecated mark for find_element_by* + ([#657](https://github.com/appium/python-client/pull/657), + [`d7cb6b5`](https://github.com/appium/python-client/commit/d7cb6b59598fb594b56d77c47abb02f4f07fa452)) + +- Relax selenium version control ([#656](https://github.com/appium/python-client/pull/656), + [`ace98dc`](https://github.com/appium/python-client/commit/ace98dc0dd9be7cd468ddfd775f5f55d24f7fb1d)) + +- Tweak keyword in metadata + ([`2a462be`](https://github.com/appium/python-client/commit/2a462becfe31dc4e18559ba246b0856fd3eb2488)) + +### Features + +- Add AppiumBy instead of MobileBy ([#659](https://github.com/appium/python-client/pull/659), + [`b70422b`](https://github.com/appium/python-client/commit/b70422b67f5254523ed360e1d196df0df04feab4)) + + +## v2.0.0 (2021-11-08) + +### Chores + +- Add Deprecated for -windows uiautomation + ([#649](https://github.com/appium/python-client/pull/649), + [`8ec5441`](https://github.com/appium/python-client/commit/8ec5441d2b5074180eafd8cc7dc511e1d8615496)) + +- Add deprecated mark in MultiAction class + ([#648](https://github.com/appium/python-client/pull/648), + [`1a54fe9`](https://github.com/appium/python-client/commit/1a54fe9010d2305ab9ec2b6f2a6382279832f42d)) + +- Add deprecation mark in touch actions and multi touch + ([#648](https://github.com/appium/python-client/pull/648), + [`1a54fe9`](https://github.com/appium/python-client/commit/1a54fe9010d2305ab9ec2b6f2a6382279832f42d)) + +- Add logger ([#649](https://github.com/appium/python-client/pull/649), + [`8ec5441`](https://github.com/appium/python-client/commit/8ec5441d2b5074180eafd8cc7dc511e1d8615496)) + +- Add Python 3.9 as metadata + ([`f4d5489`](https://github.com/appium/python-client/commit/f4d5489ae576a3ad126aa99bca7531a89c153d6a)) + +- Adding deprecation mark in touch actions and multi touch + ([#648](https://github.com/appium/python-client/pull/648), + [`1a54fe9`](https://github.com/appium/python-client/commit/1a54fe9010d2305ab9ec2b6f2a6382279832f42d)) + +- Cleanup no longer needed code in w3c, bump dev Pipfile + ([#646](https://github.com/appium/python-client/pull/646), + [`658cadd`](https://github.com/appium/python-client/commit/658cadd065411caf5299450a610fe9fd725cdceb)) + +- Deprecate -windows uiautomation ([#649](https://github.com/appium/python-client/pull/649), + [`8ec5441`](https://github.com/appium/python-client/commit/8ec5441d2b5074180eafd8cc7dc511e1d8615496)) + +- **deps**: Update isort requirement from ~=5.9 to ~=5.10 + ([#650](https://github.com/appium/python-client/pull/650), + [`a195bf0`](https://github.com/appium/python-client/commit/a195bf0affb813ad1729a90b2096620b27eb5478)) + +- **deps**: Update pylint requirement from ~=2.10 to ~=2.11 + ([#638](https://github.com/appium/python-client/pull/638), + [`9baa378`](https://github.com/appium/python-client/commit/9baa3786480c68f00e6bb1a6ea8c5b79fc4c0d62)) + +- **deps**: Update pytest-cov requirement from ~=2.12 to ~=3.0 + ([#641](https://github.com/appium/python-client/pull/641), + [`4643402`](https://github.com/appium/python-client/commit/4643402c287dd75ef0ed68dee1e15229116cc00d)) + +- **deps**: Update sphinx requirement from <4.0,>=3.0 to >=3.0,<5.0 + ([#603](https://github.com/appium/python-client/pull/603), + [`2df9031`](https://github.com/appium/python-client/commit/2df9031ea83701decd855f0bc8c33c6aaa09a0e5)) + +- **deps**: Update sphinx-rtd-theme requirement from <1.0 to <2.0 + ([#637](https://github.com/appium/python-client/pull/637), + [`a2d3df1`](https://github.com/appium/python-client/commit/a2d3df1aa65654727aa8becc104e4c1207c84ab2)) + +### Continuous Integration + +- Add --pre ([#651](https://github.com/appium/python-client/pull/651), + [`9871231`](https://github.com/appium/python-client/commit/9871231bb89b089a1ac93bf7cbc35439a5783ea5)) + +- Set pipenv==2021.5.29 to prevent dependencies error + ([#651](https://github.com/appium/python-client/pull/651), + [`9871231`](https://github.com/appium/python-client/commit/9871231bb89b089a1ac93bf7cbc35439a5783ea5)) + +### Documentation + +- Update readme + ([`45b389d`](https://github.com/appium/python-client/commit/45b389d6e183561a57fef9c7c8ea28d15121093d)) + +- Update readme + ([`4bc118b`](https://github.com/appium/python-client/commit/4bc118b2158712f972dd43136d32241f05dc94c7)) + +- Update readme ([#648](https://github.com/appium/python-client/pull/648), + [`1a54fe9`](https://github.com/appium/python-client/commit/1a54fe9010d2305ab9ec2b6f2a6382279832f42d)) + +### Features + +- Change base selenium client version to selenium 4 + ([#636](https://github.com/appium/python-client/pull/636), + [`c7d4193`](https://github.com/appium/python-client/commit/c7d4193a26c766da66fa16ecb89fc698a781826c)) + + +## v1.3.0 (2021-09-26) + +### Chores + +- Add placeholder ([#615](https://github.com/appium/python-client/pull/615), + [`e48085d`](https://github.com/appium/python-client/commit/e48085d27968a93d9cfb5d86964ac84198b52214)) + +- **deps**: Update astroid requirement from ~=2.5 to ~=2.7 + ([#629](https://github.com/appium/python-client/pull/629), + [`76ef1e7`](https://github.com/appium/python-client/commit/76ef1e77b6cb48ac62bee02e5b8003c5fa50a3e2)) + +- **deps**: Update mypy requirement from ~=0.812 to ~=0.910 + ([#616](https://github.com/appium/python-client/pull/616), + [`5f603ce`](https://github.com/appium/python-client/commit/5f603cecb9464335f4e1b99e21a880dff12a958a)) + +- **deps**: Update pylint requirement from ~=2.8 to ~=2.10 + ([#628](https://github.com/appium/python-client/pull/628), + [`36b4990`](https://github.com/appium/python-client/commit/36b4990bd8c34ce32cf013754058b3eb928d2186)) + +- **deps**: Update tox requirement from ~=3.23 to ~=3.24 + ([#619](https://github.com/appium/python-client/pull/619), + [`bc9eb18`](https://github.com/appium/python-client/commit/bc9eb1850f3fb26392e2055ce082c055b86c18dd)) + +- **deps**: Update types-python-dateutil requirement + ([#633](https://github.com/appium/python-client/pull/633), + [`e85d7d4`](https://github.com/appium/python-client/commit/e85d7d402f93f24d21e116040a06d6ee95d2e5a6)) + +- **deps-dev**: Update pre-commit requirement from ~=2.13 to ~=2.15 + ([#634](https://github.com/appium/python-client/pull/634), + [`31eda77`](https://github.com/appium/python-client/commit/31eda777d0761689b84a1503f6b7c896f1df361a)) + +### Features + +- Add command with `setattr` ([#615](https://github.com/appium/python-client/pull/615), + [`e48085d`](https://github.com/appium/python-client/commit/e48085d27968a93d9cfb5d86964ac84198b52214)) + +- Add satellites in set_location ([#620](https://github.com/appium/python-client/pull/620), + [`cfb6ee6`](https://github.com/appium/python-client/commit/cfb6ee6487217d0b01c42b2b9d291779ea17dd85)) + +- Do not raise an error in case method is already defined + ([#632](https://github.com/appium/python-client/pull/632), + [`f8d3a38`](https://github.com/appium/python-client/commit/f8d3a38639557081700a273c5dcc641e42112aba)) + + +## v1.2.0 (2021-06-06) + +### Chores + +- **deps**: Update isort requirement from ~=5.7 to ~=5.8 + ([#596](https://github.com/appium/python-client/pull/596), + [`91a9d67`](https://github.com/appium/python-client/commit/91a9d6701430599637bb6895f9ffb078ee4bd052)) + +- **deps**: Update pylint requirement from ~=2.7 to ~=2.8 + ([#600](https://github.com/appium/python-client/pull/600), + [`18db735`](https://github.com/appium/python-client/commit/18db7355a6f92be5f85fb7ef07b06b78bb9387af)) + +- **deps**: Update pytest-cov requirement from ~=2.11 to ~=2.12 + ([#606](https://github.com/appium/python-client/pull/606), + [`ba408b7`](https://github.com/appium/python-client/commit/ba408b74f0d30fc06a51e77f68fc5cfd4ac8f99a)) + +- **deps-dev**: Update pre-commit requirement from ~=2.11 to ~=2.12 + ([#599](https://github.com/appium/python-client/pull/599), + [`d0bfdd6`](https://github.com/appium/python-client/commit/d0bfdd63ca10a0950306d0e8e10a720317ce4d91)) + +- **deps-dev**: Update pre-commit requirement from ~=2.12 to ~=2.13 + ([#607](https://github.com/appium/python-client/pull/607), + [`37258a3`](https://github.com/appium/python-client/commit/37258a392b630686797e3f7bdcc038bdd6e89d83)) + +### Features + +- Allow to add a command dynamically ([#608](https://github.com/appium/python-client/pull/608), + [`b4c15e2`](https://github.com/appium/python-client/commit/b4c15e2abe0ce477c2b7fc36cb78e8fd9aae12f0)) + + +## v1.1.0 (2021-03-10) + +### Chores + +- Add table for screen_record kwarg ([#582](https://github.com/appium/python-client/pull/582), + [`1a8c736`](https://github.com/appium/python-client/commit/1a8c73675b25aa3b33a591fddb6a56dce82cf647)) + +- Address selenium-4 branch in readme ([#566](https://github.com/appium/python-client/pull/566), + [`9c7fbce`](https://github.com/appium/python-client/commit/9c7fbce68d97187a9d1aa07d13c86e0e291e39e4)) + +- Apply Black code formatter ([#571](https://github.com/appium/python-client/pull/571), + [`344953a`](https://github.com/appium/python-client/commit/344953a49c7d66c77b2fa9b998a89a26d0e1f0d7)) + +- Fix functional keyboard tests with appium v1.21.0-beta.0 + ([#574](https://github.com/appium/python-client/pull/574), + [`70048fc`](https://github.com/appium/python-client/commit/70048fc7c504aea1e57b6bc71c701db7beb60c2c)) + +- Fix iOS app management functional tests ([#575](https://github.com/appium/python-client/pull/575), + [`74f599d`](https://github.com/appium/python-client/commit/74f599d847f9624221ce7b27aa2570231dd56de3)) + +- Update pipfile to respect isort v5 ([#577](https://github.com/appium/python-client/pull/577), + [`173d3aa`](https://github.com/appium/python-client/commit/173d3aae4289015b0e9a28e5e3bb0e7ccc86061f)) + +- **deps**: Update astroid requirement from ~=2.4 to ~=2.5 + ([#587](https://github.com/appium/python-client/pull/587), + [`d5f29f0`](https://github.com/appium/python-client/commit/d5f29f08a2fe9b5a9cca4162726c7cfb4faa42e9)) + +- **deps**: Update isort requirement from ~=5.0 to ~=5.7 + ([#578](https://github.com/appium/python-client/pull/578), + [`3706e87`](https://github.com/appium/python-client/commit/3706e87068bd384895272fac6611c8f0c64716c8)) + +- **deps**: Update mypy requirement from ~=0.800 to ~=0.812 + ([#589](https://github.com/appium/python-client/pull/589), + [`db035dd`](https://github.com/appium/python-client/commit/db035dd0f4cca60c33293951e1bc0761054b0cdc)) + +- **deps**: Update pylint requirement from ~=2.6 to ~=2.7 + ([#588](https://github.com/appium/python-client/pull/588), + [`0ecad2f`](https://github.com/appium/python-client/commit/0ecad2fa1bf7e5e876372eede24f664492fd4fc5)) + +- **deps**: Update tox requirement from ~=3.21 to ~=3.22 + ([#586](https://github.com/appium/python-client/pull/586), + [`3c31a65`](https://github.com/appium/python-client/commit/3c31a65cef9a93e065920e4add2d74b12bc0f436)) + +- **deps**: Update tox requirement from ~=3.22 to ~=3.23 + ([#593](https://github.com/appium/python-client/pull/593), + [`66208fd`](https://github.com/appium/python-client/commit/66208fdbbc8f0a8b0e90376b404135b57e797fa5)) + +- **deps-dev**: Update pre-commit requirement from ~=2.10 to ~=2.11 + ([#595](https://github.com/appium/python-client/pull/595), + [`e49dc78`](https://github.com/appium/python-client/commit/e49dc784d376145f12afe2f61a8ee7348c2ee08e)) + +### Continuous Integration + +- Added py39-dev ([#557](https://github.com/appium/python-client/pull/557), + [`1ae8c25`](https://github.com/appium/python-client/commit/1ae8c25d3e1da457f4a16710c0b04dc709140277)) + +- Added py39-dev for travis ([#557](https://github.com/appium/python-client/pull/557), + [`1ae8c25`](https://github.com/appium/python-client/commit/1ae8c25d3e1da457f4a16710c0b04dc709140277)) + +- Move azure project to Appium CI, update readme + ([#564](https://github.com/appium/python-client/pull/564), + [`3e60e8f`](https://github.com/appium/python-client/commit/3e60e8fff4cfca93541c73c45f9662cdfe6475bc)) + +- Remove travis ([#581](https://github.com/appium/python-client/pull/581), + [`1bf0553`](https://github.com/appium/python-client/commit/1bf05530dbccc2478a58bbd45fb8e0ce092bcceb)) + +- Upgrade xcode and macos ([#556](https://github.com/appium/python-client/pull/556), + [`9402c6f`](https://github.com/appium/python-client/commit/9402c6ff99de70d42a1031db95bb934942b08a41)) + +- Upgrade xcode ver and macos ([#556](https://github.com/appium/python-client/pull/556), + [`9402c6f`](https://github.com/appium/python-client/commit/9402c6ff99de70d42a1031db95bb934942b08a41)) + +- Use node v12 ([#585](https://github.com/appium/python-client/pull/585), + [`8eb4c3f`](https://github.com/appium/python-client/commit/8eb4c3f7c2cb95ee81eb9c664de265edf473d9dc)) + +### Documentation + +- Fix wrong code example in README.md ([#555](https://github.com/appium/python-client/pull/555), + [`17af195`](https://github.com/appium/python-client/commit/17af19565cc1b443b2aecb86291db68a450c420f)) + +### Features + +- Add optional location speed attribute for android devices + ([#594](https://github.com/appium/python-client/pull/594), + [`ce78c0d`](https://github.com/appium/python-client/commit/ce78c0de2e15307ae20a8cc3a496f6c794fdeec6)) + +- Add warning to drop forceMjsonwp for W3C + ([#567](https://github.com/appium/python-client/pull/567), + [`e51bcd2`](https://github.com/appium/python-client/commit/e51bcd269cab41b680971026e2974a2bf468a8a2)) + +- Added descriptions for newly added screenrecord opts + ([#540](https://github.com/appium/python-client/pull/540), + [`d8d4aea`](https://github.com/appium/python-client/commit/d8d4aea950e5c20d5299e131f9a9a5075a7d3aa4)) + +- Added docstring for macOS screenrecord option + ([#580](https://github.com/appium/python-client/pull/580), + [`ed5af31`](https://github.com/appium/python-client/commit/ed5af31a38e3bc34af32f601bf9ca0d800bcbc69)) + + +## v1.0.2 (2020-07-15) + +### Chores + +- Add checking package file count comparison in release script + ([#547](https://github.com/appium/python-client/pull/547), + [`e3bd534`](https://github.com/appium/python-client/commit/e3bd5344697757bb8f1fc5722e132e13fa6e8194)) + +- Add file count in release script ([#547](https://github.com/appium/python-client/pull/547), + [`e3bd534`](https://github.com/appium/python-client/commit/e3bd5344697757bb8f1fc5722e132e13fa6e8194)) + +- Add the workaround to avoid service freezes on Windows + ([#552](https://github.com/appium/python-client/pull/552), + [`95b01c9`](https://github.com/appium/python-client/commit/95b01c945241559c84804d897a1dddde2feb58d9)) + + +## v1.0.1 (2020-05-18) + +### Bug Fixes + +- Broken package ([#545](https://github.com/appium/python-client/pull/545), + [`e4e29f8`](https://github.com/appium/python-client/commit/e4e29f83f9feb4c1a6aa979e3c977af7ff996698)) + + +## v1.0.0 (2020-05-17) + +- Initial Release diff --git a/CHANGELOG.txt b/CHANGELOG.txt deleted file mode 100644 index c9aff327..00000000 --- a/CHANGELOG.txt +++ /dev/null @@ -1,5 +0,0 @@ -Changes in 0.9 (from 0.8) -========================= - -- Add methods for accessing and manipulating the network connection details on Android devices -- Add methods for accessing and manipulating the input methods on Android devices diff --git a/LICENSE b/LICENSE index 25caf405..540e41dc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,192 @@ -Copyright 2012-2014 Appium Committers + 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 JS Foundation and other contributors, https://js.foundation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..4ed823f4 --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +.PHONY: Commands for developers + +.PHONY: check-all +check-all: check unittest + +.PHONY: check +check: check-lint check-format + +.PHONY: check-lint +check-lint: + uv run ruff check . + uv run mypy appium + +.PHONY: check-format +check-format: + uv run ruff format --check . + +.PHONY: fix +fix: fix-lint fix-format + +.PHONY: fix-lint +fix-lint: + uv run ruff check --fix . + +.PHONY: fix-format +fix-format: + uv run ruff format . + +.PHONY: install-uv +install-uv: + @command -v uv >/dev/null 2>&1 || { \ + echo "Installing uv"; \ + curl -LsSf https://astral.sh/uv/install.sh | sh; \ + if [ -n "$$GITHUB_PATH" ]; then \ + echo "PATH=$$HOME/.local/bin:$$PATH" >> $$GITHUB_PATH; \ + else \ + echo "Please restart your shell or run 'exec $$SHELL'"; \ + fi; \ + } + +.PHONY: sync-dev +sync-dev: + uv sync --dev + +.PHONY: unittest +unittest: ## Run unittest + uv run pytest $(ARGS) test/unit/ + +.PHONY: help +help: ## Display this help screen + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' diff --git a/README.md b/README.md index 7b6ffeed..b7768faa 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,26 @@ -Appium Python Client -==================== +# Appium Python Client -An extension library for adding [Selenium 3.0 draft](https://dvcs.w3.org/hg/webdriver/raw-file/tip/webdriver-spec.html) and [Mobile JSON Wire Protocol Specification draft](https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile) -functionality to the Python language bindings, for use with the mobile testing -framework [Appium](https://appium.io). +[![PyPI version](https://badge.fury.io/py/Appium-Python-Client.svg)](https://badge.fury.io/py/Appium-Python-Client) +[![Downloads](https://pepy.tech/badge/appium-python-client)](https://pepy.tech/project/appium-python-client) -# Getting the Appium Python client +[![Functional Tests](https://github.com/appium/python-client/actions/workflows/functional-test.yml/badge.svg)](https://github.com/appium/python-client/actions/workflows/functional-test.yml) -There are two ways to install and use the Appium Python client. +An extension library for adding [WebDriver Protocol](https://www.w3.org/TR/webdriver/) and Appium commands to the Selenium Python language binding for use with the mobile testing framework [Appium](https://appium.io). -1. Install from [PyPi](https://pypi.python.org/pypi), as -['Appium-Python-Client'](https://pypi.python.org/pypi/Appium-Python-Client). +## Getting the Appium Python client + +There are three ways to install and use the Appium Python client. + +1. Install from [PyPi](https://pypi.org), as +['Appium-Python-Client'](https://pypi.org/project/Appium-Python-Client/). ```shell pip install Appium-Python-Client ``` -2. Install from source, via [PyPi](https://pypi.python.org/pypi). From ['Appium-Python-Client'](https://pypi.python.org/pypi/Appium-Python-Client), + You can see the history from [here](https://pypi.org/project/Appium-Python-Client/#history) + +2. Install from source, via [PyPi](https://pypi.org). From ['Appium-Python-Client'](https://pypi.org/project/Appium-Python-Client/), download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz). ```shell @@ -33,15 +37,125 @@ download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz). python setup.py install ``` +## Compatibility Matrix + +|Appium Python Client| Selenium binding| Python version | +|----|----|----| +|`5.1.1`+|`4.26.0`+ | 3.9+ | +|`4.5.0` - `5.1.0`|`4.26.0` - `4.31.0` | 3.9+ | +|`4.3.0` - `4.4.0`|`4.26.0` - `4.31.0` | 3.8+ | +|`3.0.0` - `4.2.1` |`4.12.0` - `4.25.0` | 3.8+ | +|`2.10.0` - `2.11.1` |`4.1.0` - `4.11.2` | 3.7+ | +|`2.2.0` - `2.9.0` |`4.1.0` - `4.9.0` | 3.7+ | +|`2.0.0` - `2.1.4` |`4.0.0` | 3.7+ | +|`1.0.0` - `1.3.0` |`3.x`| 3.7+ | +|`0.52` and below|`3.x`| 2.7, 3.4 - 3.7 | + +The Appium Python Client depends on [Selenium Python binding](https://pypi.org/project/selenium/), thus +the Selenium Python binding update might affect the Appium Python Client behavior. +For example, some changes in the Selenium binding could break the Appium client. + +> **Note** +> We strongly recommend you manage dependencies with version management tools such as +> [uv](https://docs.astral.sh/uv/) to keep compatible version combinations. + + +### Quick migration guide from v4 to v5 +- This change affects only for users who specify `keep_alive`, `direct_connection` and `strict_ssl` arguments for `webdriver.Remote`: + - Please use `AppiumClientConfig` as `client_config` argument similar to how it is specified below: + ```python + SERVER_URL_BASE = 'http://127.0.0.1:4723' + # before + driver = webdriver.Remote( + SERVER_URL_BASE, + options=UiAutomator2Options().load_capabilities(desired_caps), + direct_connection=True, + keep_alive=False, + strict_ssl=False + ) + + # after + from appium.webdriver.client_config import AppiumClientConfig + client_config = AppiumClientConfig( + remote_server_addr=SERVER_URL_BASE, + direct_connection=True, + keep_alive=False, + ignore_certificates=True, + ) + driver = webdriver.Remote( + options=UiAutomator2Options().load_capabilities(desired_caps), + client_config=client_config + ) + ``` + - Note that you can keep using `webdriver.Remote(url, options=options, client_config=client_config)` format as well. + In such case the `remote_server_addr` argument of `AppiumClientConfig` constructor would have priority over the `url` argument of `webdriver.Remote` constructor. +- Use `http://127.0.0.1:4723` as the default server url instead of `http://127.0.0.1:4444/wd/hub` + +### Quick migration guide from v3 to v4 +- Removal + - `MultiAction` and `TouchAction` are removed. Please use W3C WebDriver actions or `mobile:` extensions + - [appium/webdriver/extensions/action_helpers.py](appium/webdriver/extensions/action_helpers.py) + - https://www.selenium.dev/documentation/webdriver/actions_api/ + - https://www.youtube.com/watch?v=oAJ7jwMNFVU + - https://appiumpro.com/editions/30-ios-specific-touch-action-methods + - https://appiumpro.com/editions/29-automating-complex-gestures-with-the-w3c-actions-api + - Deprecated `AppiumBy.WINDOWS_UI_AUTOMATION`, which has no usage right now. + +### Quick migration guide from v2 to v3 +- `options` keyword argument in the `webdriver.Remote` constructor such as `XCUITestOptions` instead of `desired_capabilities` + - Available options are https://github.com/appium/python-client/tree/master/appium/options + - Please check the [Usage](#usage) below as an example. + - Not a "new" change, but the `desired_capabilities` argument has been removed since v3. +- Replacement + - `start_activity` method: Please use [`mobile: startActivity`](https://github.com/appium/appium-uiautomator2-driver?tab=readme-ov-file#mobile-startactivity) + - `launch_app`, `close_app` and `reset` methods: Please refer to https://github.com/appium/appium/issues/15807 + - `available_ime_engines`, `is_ime_active`, `activate_ime_engine`, `deactivate_ime_engine` and `active_ime_engine` methods: Please use [`mobile: shell`](https://github.com/appium/appium-uiautomator2-driver?tab=readme-ov-file#mobile-shell) + - `set_value` and `set_text` methods: Please use `element.send_keys` or `send_keys` by W3C Actions +- Removal + - `end_test_coverage` method is no longer available + - `session` property is no longer available + - `all_sessions` property is no longer available + +### Quick migration guide from v1 to v2 +- Enhancement + - Updated base Selenium Python binding version to v4 + - Removed `forceMjsonwp` since Selenium v4 and Appium Python client v2 expect only W3C WebDriver protocol + - Methods `ActionHelpers#scroll`, `ActionHelpers#drag_and_drop`, `ActionHelpers#tap`, `ActionHelpers#swipe` and `ActionHelpers#flick` now call W3C actions as its backend + - Please check each behavior. Their behaviors could slightly differ. + - Added `strict_ssl` to relax SSL errors such as self-signed ones +- Deprecated + - `MultiAction` and `TouchAction` are deprecated. Please use W3C WebDriver actions or `mobile:` extensions + - `launch_app`, `close_app`, and `reset` are deprecated. Please read [issues#15807](https://github.com/appium/appium/issues/15807) for more details + +#### MultiAction/TouchAction to W3C actions + +Some elements can be handled with `touch` pointer action instead of the default `mouse` pointer action in the Selenium Python client. +For example, the below action builder is to replace the default one with the `touch` pointer action. + +```python +from selenium.webdriver import ActionChains +from selenium.webdriver.common.actions import interaction +from selenium.webdriver.common.actions.action_builder import ActionBuilder +from selenium.webdriver.common.actions.pointer_input import PointerInput + +actions = ActionChains(driver) +# override as 'touch' pointer action +actions.w3c_actions = ActionBuilder(driver, mouse=PointerInput(interaction.POINTER_TOUCH, "touch")) +actions.w3c_actions.pointer_action.move_to_location(start_x, start_y) +actions.w3c_actions.pointer_action.pointer_down() +actions.w3c_actions.pointer_action.pause(2) +actions.w3c_actions.pointer_action.move_to_location(end_x, end_y) +actions.w3c_actions.pointer_action.release() +actions.perform() +``` -# Usage +- [appium/webdriver/extensions/action_helpers.py](appium/webdriver/extensions/action_helpers.py) +- https://www.selenium.dev/documentation/webdriver/actions_api/ -The Appium Python Client is fully compliant with the Selenium 3.0 specification -draft, with some helpers to make mobile testing in Python easier. The majority of -the usage remains as it has been for Selenium 2 (WebDriver), and as the [official -Selenium Python bindings](https://pypi.python.org/pypi/selenium) begins to -implement the new specification that implementation will be used underneath, so -test code can be written that is utilizable with both bindings. +## Usage + +The Appium Python Client is fully compliant with the WebDriver Protocol +including several helpers to make mobile testing in Python easier. To use the new functionality now, and to use the superset of functions, instead of including the Selenium `webdriver` module in your test code, use that from @@ -53,457 +167,380 @@ from appium import webdriver From there much of your test code will work with no change. -As a base for the following code examples, the following sets up the [UnitTest](https://docs.python.org/2/library/unittest.html) +As a base for the following code examples, the following set up the [UnitTest](https://docs.python.org/3/library/unittest.html) environment: ```python -# Android environment -import unittest -from appium import webdriver +# Python/Pytest +import pytest -desired_caps = {} -desired_caps['platformName'] = 'Android' -desired_caps['platformVersion'] = '4.2' -desired_caps['deviceName'] = 'Android Emulator' -desired_caps['app'] = PATH('../../../apps/selendroid-test-app.apk') - -self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) -``` - -```python -# iOS environment -import unittest from appium import webdriver - -desired_caps = {} -desired_caps['platformName'] = 'iOS' -desired_caps['platformVersion'] = '7.1' -desired_caps['deviceName'] = 'iPhone Simulator' -desired_caps['app'] = PATH('../../apps/UICatalog.app.zip') - -self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) -``` - - -## Changed or added functionality - -The methods that do change are... - - -### Switching between 'Native' and 'Webview' - -For mobile testing the Selnium methods for switching between windows was previously -commandeered for switching between native applications and webview contexts. Methods -explicitly for this have been added to the Selenium 3 specification, so moving -forward these 'context' methods are to be used. - -To get the current context, rather than calling `driver.current_window_handle` you -use - -```python -current = driver.current_context +# Options are only available since client version 2.3.0 +# If you use an older client then switch to desired_capabilities +# instead: https://github.com/appium/python-client/pull/720 +from appium.options.android import UiAutomator2Options +from appium.options.ios import XCUITestOptions +from appium.webdriver.appium_service import AppiumService +from appium.webdriver.common.appiumby import AppiumBy + +APPIUM_PORT = 4723 +APPIUM_HOST = '127.0.0.1' + + +# HINT: fixtures below could be extracted into conftest.py +# HINT: and shared across all tests in the suite +@pytest.fixture(scope='session') +def appium_service(): + service = AppiumService() + service.start( + # Check the output of `appium server --help` for the complete list of + # server command line arguments + args=['--address', APPIUM_HOST, '-p', str(APPIUM_PORT)], + timeout_ms=20000, + ) + yield service + service.stop() + + +def create_ios_driver(custom_opts = None): + options = XCUITestOptions() + options.platformVersion = '13.4' + options.udid = '123456789ABC' + if custom_opts is not None: + options.load_capabilities(custom_opts) + # Appium1 points to http://127.0.0.1:4723/wd/hub by default + return webdriver.Remote(f'http://{APPIUM_HOST}:{APPIUM_PORT}', options=options) + + +def create_android_driver(custom_opts = None): + options = UiAutomator2Options() + options.platformVersion = '10' + options.udid = '123456789ABC' + if custom_opts is not None: + options.load_capabilities(custom_opts) + # Appium1 points to http://127.0.0.1:4723/wd/hub by default + return webdriver.Remote(f'http://{APPIUM_HOST}:{APPIUM_PORT}', options=options) + + +@pytest.fixture +def ios_driver_factory(): + return create_ios_driver + + +@pytest.fixture +def ios_driver(): + # prefer this fixture if there is no need to customize driver options in tests + driver = create_ios_driver() + yield driver + driver.quit() + + +@pytest.fixture +def android_driver_factory(): + return create_android_driver + + +@pytest.fixture +def android_driver(): + # prefer this fixture if there is no need to customize driver options in tests + driver = create_android_driver() + yield driver + driver.quit() + + +def test_ios_click(appium_service, ios_driver_factory): + # Usage of the context manager ensures the driver session is closed properly + # after the test completes. Otherwise, make sure to call `driver.quit()` on teardown. + with ios_driver_factory({ + 'appium:app': '/path/to/app/UICatalog.app.zip' + }) as driver: + el = driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='item') + el.click() + + +def test_android_click(appium_service, android_driver_factory): + # Usage of the context manager ensures the driver session is closed properly + # after the test completes. Otherwise, make sure to call `driver.quit()` on teardown. + with android_driver_factory({ + 'appium:app': '/path/to/app/test-app.apk', + 'appium:udid': '567890', + }) as driver: + el = driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='item') + el.click() ``` -The available contexts are not retrieved using `driver.window_handles` but with +### Available `options` -```python -driver.contexts -``` +Appium Python Client has a common options class named `AppiumOptions` but the available commands are minimal. +It does not have driver/automationName specific commands unless adding commands with `add_command` method. -Finally, to switch to a new context, rather than `driver.switch_to.window(name)`, -use the comparable context method +Available options for each automation name below will help to check what options are already defined. +Please use proper options for your automaiton usage. -```python -context_name = "WEBVIEW_1" -driver.switch_to.context(context_name) -``` +`automationName` | Package path +|:---|:-----| +any | `appium.options.common.base.AppiumOptions` +`uiautomator2` | `appium.options.android.Uiautomator2Options` +`espresso` | `appium.options.android.EspressoOptions` +`xcuitest` | `appium.options.ios.XCUITestOptions` +`safari` | `appium.options.ios.SafariOptions` +`mac2` | `appium.options.mac.Mac2Options` +`windows` | `appium.options.WindowsOptions` +`gecko` | `appium.options.GeckoOptions` +`flutterintegration` | `appium.options.flutter_integration.FlutterOptions` +## Direct Connect URLs -### Finding elements by iOS UIAutomation search +If your Selenium/Appium server decorates the new session capabilities response with the following keys: -This allows elements in iOS applications to be found using recursive element -search using the UIAutomation library. Adds the methods `driver.find_element_by_ios_uiautomation` -and `driver.find_elements_by_ios_uiautomation`. +- `directConnectProtocol` +- `directConnectHost` +- `directConnectPort` +- `directConnectPath` -```python -el = self.driver.find_element_by_ios_uiautomation('.elements()[0]') -self.assertEqual('UICatalog', el.get_attribute('name')) -``` +Then python client will switch its endpoint to the one specified by the values of those keys. ```python -els = self.driver.find_elements_by_ios_uiautomation('elements()') -self.assertIsInstance(els, list) +from appium import webdriver +# Options are only available since client version 2.3.0 +# If you use an older client then switch to desired_capabilities +# instead: https://github.com/appium/python-client/pull/720 +from appium.options.ios import XCUITestOptions +from appium.webdriver.client_config import AppiumClientConfig + +# load_capabilities API could be used to +# load options mapping stored in a dictionary +options = XCUITestOptions().load_capabilities({ + 'platformVersion': '13.4', + 'deviceName': 'iPhone Simulator', + 'app': '/full/path/to/app/UICatalog.app.zip', +}) + +client_config = AppiumClientConfig( + remote_server_addr='http://127.0.0.1:4723', + direct_connection=True +) + +driver = webdriver.Remote( + # Appium1 points to http://127.0.0.1:4723/wd/hub by default + 'http://127.0.0.1:4723', + options=options, + client_config=client_config +) ``` +## Relax SSL validation -### Finding elements by Android UIAutomator search - -This allows elements in an Android application to be found using recursive element -search using the UIAutomator library. Adds the methods `driver.find_element_by_android_uiautomator` -and `driver.find_elements_by_android_uiautomator`. +`strict_ssl` option allows you to send commands to an invalid certificate host like a self-signed one. ```python -el = self.driver.find_element_by_android_uiautomator('new UiSelector().description("Animation")') -self.assertIsNotNone(el) -``` - -```python -els = self.driver.find_elements_by_android_uiautomator('new UiSelector().clickable(true)') -self.assertIsInstance(els, list) +from appium import webdriver +# Options are only available since client version 2.3.0 +# If you use an older client then switch to desired_capabilities +# instead: https://github.com/appium/python-client/pull/720 +from appium.options.common import AppiumOptions + +options = AppiumOptions() +options.platform_name = 'mac' +options.automation_name = 'safari' +# set_capability API allows to provide any custom option +# calls to it could be chained +options.set_capability('browser_name', 'safari') + +# Appium1 points to http://127.0.0.1:4723/wd/hub by default +driver = webdriver.Remote('http://127.0.0.1:4723', options=options, strict_ssl=False) ``` - -### Finding elements by Accessibility ID - -Allows for elements to be found using the "Accessibility ID". The methods take a -string representing the accessibility id or label attached to a given element, e.g., for iOS the accessibility identifier and for Android the content-description. Adds the methods -`driver.find_element_by_accessibility_id` and `find_elements_by_accessibility_id`. - -```python -el = self.driver.find_element_by_accessibility_id('Animation') -self.assertIsNotNone(el) -``` +Since Appium Python client v4.3.0, we recommend using `selenium.webdriver.remote.client_config.ClientConfig` +instead of giving `strict_ssl` as an argument of `webdriver.Remote` below to configure the validation. ```python -els = self.driver.find_elements_by_accessibility_id('Animation') -self.assertIsInstance(els, list) -``` - - -### Touch actions - -In order to accomodate mobile touch actions, and touch actions involving -multiple pointers, the Selenium 3.0 draft specifies ["touch gestures"](https://dvcs.w3.org/hg/webdriver/raw-file/tip/webdriver-spec.html#touch-gestures) and ["multi actions"](https://dvcs.w3.org/hg/webdriver/raw-file/tip/webdriver-spec.html#multiactions-1), which build upon the touch actions. - -move_to: note that use keyword arguments if no element - -The API is built around `TouchAction` objects, which are chains of one or more actions to be performed in a sequence. The actions are: - -#### `perform` - -The `perform` method sends the chain to the server in order to be enacted. It also empties the action chain, so the object can be reused. It will be at the end of all single action chains, but is unused when writing multi-action chains. - -#### `tap` - -The `tap` method stands alone, being unable to be chained with other methods. If you need a `tap`-like action that starts a longer chain, use `press`. +from appium import webdriver -It can take either an element with an optional x-y offset, or absolute x-y coordinates for the tap, and an optional count. +from selenium.webdriver.remote.client_config import ClientConfig -```python -el = self.driver.find_element_by_accessibility_id('Animation') -action = TouchAction(self.driver) -action.tap(el).perform() -el = self.driver.find_element_by_accessibility_id('Bouncing Balls') -self.assertIsNotNone(el) +client_config = ClientConfig( + remote_server_addr='http://127.0.0.1:4723', + ignore_certificates=True +) +driver = webdriver.Remote(client_config.remote_server_addr, options=options, client_config=client_config) ``` -#### `press` - -#### `long_press` - -#### `release` - -#### `move_to` +## Set custom `AppiumConnection` -#### `wait` +The first argument of `webdriver.Remote` can set an arbitrary command executor for you. -#### `cancel` - - -### Multi-touch actions - -In addition to chains of actions performed with in a single gesture, it is also possible to perform multiple chains at the same time, to simulate multi-finger actions. This is done through building a `MultiAction` object that comprises a number of individual `TouchAction` objects, one for each "finger". - -Given two lists next to each other, we can scroll them independently but simultaneously: +1. Set init arguments for the pool manager Appium Python client uses to manage HTTP requests. ```python -els = self.driver.find_elements_by_class_name('listView') -a1 = TouchAction() -a1.press(els[0]) \ - .move_to(x=10, y=0).move_to(x=10, y=-75).move_to(x=10, y=-600).release() - -a2 = TouchAction() -a2.press(els[1]) \ - .move_to(x=10, y=10).move_to(x=10, y=-300).move_to(x=10, y=-600).release() - -ma = MultiAction(self.driver, els[0]) -ma.add(a1, a2) -ma.perform(); +from appium import webdriver +from appium.options.ios import XCUITestOptions + +import urllib3 +from appium.webdriver.appium_connection import AppiumConnection + +# Retry connection error up to 3 times. +init_args_for_pool_manage = { + 'retries': urllib3.util.retry.Retry(total=3, connect=3, read=False) +} +appium_executor = AppiumConnection( + remote_server_addr='http://127.0.0.1:4723', + init_args_for_pool_manage=init_args_for_pool_manage +) + +options = XCUITestOptions() +options.platformVersion = '13.4' +options.udid = '123456789ABC' +options.app = '/full/path/to/app/UICatalog.app.zip' +driver = webdriver.Remote(appium_executor, options=options) ``` -### Appium-Specific touch actions - -There are a small number of operations that mobile testers need to do quite a bit that can be relatively complicated to build using the Touch and Multi-touch Action API. For these we provide some convenience methods in the Appium client. -#### `driver.tap` - -This method, on the WebDriver object, allows for tapping with multiple fingers, simply by passing in an array of x-y coordinates to tap. +2. Define a subclass of `AppiumConnection` ```python -el = self.driver.find_element_by_name('Touch Paint') -action.tap(el).perform() - -# set up array of two coordinates -positions = [] -positions.append((100, 200)) -positions.append((100, 400)) - -self.driver.tap(positions) -``` - -#### `driver.swipe` - -Swipe from one point to another point. - -#### `driver.zoom` - -Zoom in on an element, doing a pinch out operation. - -#### `driver.pinch` - -Zoom out on an element, doing a pinch in operation. - - - -### Application management methods +from appium import webdriver +from appium.options.ios import XCUITestOptions -There are times when you want, in your tests, to manage the running application, -such as installing or removing an application, etc. +from appium.webdriver.appium_connection import AppiumConnection +class CustomAppiumConnection(AppiumConnection): + # Can add your own methods for the custom class + pass -#### Backgrounding an application +custom_executor = CustomAppiumConnection(remote_server_addr='http://127.0.0.1:4723') -The method `driver.background_app` sends the running application to the background -for the specified amount of time, in seconds. After that time, the application is -brought back to the foreground. +options = XCUITestOptions().load_capabilities({ + 'platformVersion': '13.4', + 'deviceName': 'iPhone Simulator', + 'app': '/full/path/to/app/UICatalog.app.zip', +}) +driver = webdriver.Remote(custom_executor, options=options) -```python -driver.background_app(1) -sleep(2) -el = driver.find_element_by_name('Animation') -assertIsNotNone(el) ``` +The `AppiumConnection` can set `selenium.webdriver.remote.client_config.ClientConfig` as well. -#### Checking if an application is installed - -To check if an application is currently installed on the device, use the `device.is_app_installed` -method. This method takes the bundle id of the application and return `True` or -`False`. - -```python -assertFalse(self.driver.is_app_installed('sdfsdf')) -assertTrue(self.driver.is_app_installed('com.example.android.apis')) -``` +## Relaxing HTTP request read timeout +Appium Python Client has `120` seconds read timeout on each HTTP request since the version v4.3.0 because of +the corresponding selenium binding version. +You have two methods to extend the read timeout. -#### Installing an application +1. **Recommend** Configure timeout via `appium.webdriver.client_config.AppiumClientConfig` or `selenium.webdriver.remote.client_config.ClientConfig` + - `timeout` argument, or + - `init_args_for_pool_manager` argument for `urllib3.PoolManager` +2. Set `GLOBAL_DEFAULT_TIMEOUT` environment variable + - This env var will be removed from the selenium binding ([issue](https://github.com/SeleniumHQ/selenium/issues/15604)) -To install an uninstalled application on the device, use `device.install_app`, -sending in the path to the application file or archive. +## Documentation -```python -assertFalse(driver.is_app_installed('io.selendroid.testapp')) -driver.install_app('/Users/isaac/code/python-client/test/apps/selendroid-test-app.apk') -assertTrue(driver.is_app_installed('io.selendroid.testapp')) -``` +- https://appium.github.io/python-client-sphinx/ is detailed documentation +- [functional tests](test/functional) also may help to see concrete examples. +## Development -#### Removing an application +- Code Style: [PEP-0008](https://www.python.org/dev/peps/pep-0008/) + - Apply `ruff` as pre commit hook + - Run `make` command for development. See `make help` output for details +- Docstring style: [Google Style](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) -If you need to remove an application from the device, use `device.remove_app`, -passing in the application id. +### Setup -```python -assertTrue(driver.is_app_installed('com.example.android.apis')) -driver.remove_app('com.example.android.apis') -assertFalse(driver.is_app_installed('com.example.android.apis')) +```bash +make install-uv +exec $SHELL +make sync-dev ``` +Running above commands should automatically setup the virtual environment for the project +using the default system Python version and put it into the `.venv` folder under the project root. +If you'd like to customize the Python version then run the following command before `make sync-dev`: -#### Closing and Launching an application - -To launch the application specified in the desired capabilities, call `driver.launch_app`. -Closing that application is initiated by `driver.close_app` - -```python -el = driver.find_element_by_name('Animation') -assertIsNotNone(el) -driver.close_app(); - -try: - driver.find_element_by_name('Animation') -except Exception as e: - pass # should not exist - -driver.launch_app() -el = driver.find_element_by_name('Animation') -assertIsNotNone(el) +```bash +uv venv --python ``` -#### Resetting an application - -To reset the running application, use `driver.reset`. +where `` is the actual Python version, for example `3.12`. -```python -el = driver.find_element_by_name('App') -el.click() - -driver.reset() -sleep(5) +If you want to customize the folder where uv stores the virtual environment by default +(e.g. `.venv`) then add an argument containing the destination folder path to the above command: -el = driver.find_element_by_name('App') -assertIsNotNone(el) +```bash +uv venv /venv/root/folder ``` +In order to activate the newly created virtual environment you may either source it: -### Other methods - - -#### Retrieving application strings - -The property method `driver.app_strings` returns the application strings from -the application on the device. - -```python -strings = driver.app_strings +```bash +source /venv/root/folder/bin/activate ``` +or add it to PATH: -#### Sending a key event to an Android device - -The `driver.keyevent` method sends a keycode to the device. The keycodes can be -found [here](http://developer.android.com/reference/android/view/KeyEvent.html). -Android only. - -```python -# sending 'Home' key event -driver.press_keycode(3) +```bash +export "PATH=/venv/root/folder/bin:$PATH" ``` +### Linting And Formatting -#### Hiding the keyboard in iOS - -To hide the keyboard from view in iOS, use `driver.hide_keyboard`. If a key name -is sent, the keyboard key with that name will be pressed. If no arguments are -passed in, the keyboard will be hidden by tapping on the screen outside the text -field, thus removing focus from it. - -```python -# get focus on text field, so keyboard comes up -el = driver.find_element_by_class_name('android.widget.TextView') -el.set_value('Testing') - -el = driver.find_element_by_class_name('keyboard') -assertTrue(el.is_displayed()) +Run linter and format checks -driver.hide_keyboard('Done') - -assertFalse(el.is_displayed()) +```bash +make check ``` -```python -# get focus on text field, so keyboard comes up -el = driver.find_element_by_class_name('android.widget.TextView') -el.set_value('Testing') - -el = driver.find_element_by__name('keyboard') -assertTrue(el.is_displayed()) - -driver.hide_keyboard() +Address autofixable linter and formatting issues -assertFalse(el.is_displayed()) +```bash +make fix ``` +### Testing -#### Retrieving the current running activity +#### Unit -The property method `driver.current_activity` returns the name of the current -activity running on the device. - -```python -activity = driver.current_activity -assertEquals('.ApiDemos', activity) +```bash +make unittest ``` +Run in parallel (2 threads) -#### Set a value directly on an element - -Sometimes one needs to directly set the value of an element on the device. To do -this, the method `driver.set_value` or `element.set_value` is invoked. - -```python -el = driver.find_element_by_class_name('android.widget.EditText') -driver.set_value(el, 'Testing') - -text = el.get_attribute('text') -assertEqual('Testing', text) - -el.set_value('More testing') -text = el.get_attribute('text') -assertEqual('More testing', text) +```bash +make unittest ARGS="-n 2" ``` +#### Functional -#### Retrieve a file from the device - -To retrieve the contents of a file from the device, use `driver.pull_file`, which -returns the contents of the specified file encoded in [Base64](https://docs.python.org/2/library/base64.html). - -```python -# pulling the strings file for our application -data = driver.pull_file('data/local/tmp/strings.json') -strings = json.loads(data.decode('base64', 'strict')) -assertEqual('You can\'t wipe my data, you are a monkey!', strings[u'monkey_wipe_data']) +```bash +uv run pytest test/functional/ios/search_context/find_by_ios_class_chain_tests.py ``` +#### In parallel for iOS -#### Place a file on the device +1. Create simulators named 'iPhone X - 8100' and 'iPhone X - 8101' +1. Run tests -To put a file onto the device at a particular place, use the `driver.push_file` -method, which takes the path and the data, encoded as [Base64](https://docs.python.org/2/library/base64.html), to be written to the file. - -```python -path = 'data/local/tmp/test_push_file.txt' -data = 'This is the contents of the file to push to the device.' -driver.push_file(path, data.encode('base64')) -data_ret = driver.pull_file('data/local/tmp/test_push_file.txt').decode('base64') -self.assertEqual(data, data_ret) +```bash +uv run pytest -n 2 test/functional/ios/search_context/find_by_ios_class_chain_tests.py ``` +## Release -#### Complex find in Android - -Appium supports a way to do complex searches for elements on an Android device. -This is accessed through `driver.complex_find`. The arguments and use case, -to borrow from Winston Churchill, remain a riddle, wrapped in a mystery, inside -an enigma. - -```python -el = self.driver.complex_find([[[2, 'Ani']]]) -self.assertIsNotNone(el) -``` +In case you need to release a version manually. +```bash +rm -rf dist +# bumping the version, building a package and creating a tag. +uv run semantic-release version --patch|--minor|--major -#### End test coverage +# To publish the version's module to pypi +UV_PUBLISH_TOKEN= uv publish -There is functionality in the Android emulator to instrument certain activities. -For information on this, see the [Appium docs](https://github.com/appium/appium/blob/master/docs/en/android_coverage.md). To end this coverage -and retrieve the data, use `driver.end_test_coverage`, passing in the `intent` -that is being instrumentalized, and the path to the `coverage.ec` file on the -device. - -```python -coverage_ec_file = driver.end_test_coverage(intent='android.intent.action.MAIN', path='') +# To push built modules in 'dist' directory to the GH release page. +uv run semantic-release publish ``` +## License -#### Lock the device - -To lock the device for a certain amount of time, on iOS, use `driver.lock`. The -argument is the number of seconds to wait before unlocking. - - -#### Shake the device - -To shake the device, use `driver.shake`. +Apache License v2 diff --git a/appium/common/exceptions.py b/appium/common/exceptions.py index 084bb79d..3fe1712e 100644 --- a/appium/common/exceptions.py +++ b/appium/common/exceptions.py @@ -16,13 +16,11 @@ class NoSuchContextException(InvalidSwitchToTargetException): - """ - Thrown when context target to be switched doesn't exist. + """Thrown when context target to be switched doesn't exist. To find the current set of active contexts, you can get a list of the active contexts in the following way: - print driver.contexts + print(driver.contexts) """ - pass diff --git a/appium/common/helper.py b/appium/common/helper.py new file mode 100644 index 00000000..1565b96e --- /dev/null +++ b/appium/common/helper.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +# 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. + +import base64 +from typing import Any, Dict + +from appium import version as appium_version + + +def extract_const_attributes(cls: type) -> Dict[str, Any]: + """Return dict with constants attributes and values in the class(e.g. {'VAL1': 1, 'VAL2': 2}) + + Args: + cls: Class to be extracted constants + + Returns: + dict with constants attributes and values in the class + """ + return {attr: value for attr, value in vars(cls).items() if not callable(getattr(cls, attr)) and attr.isupper()} + + +def library_version() -> str: + """Return a version of this python library""" + + return appium_version.version + + +def encode_file_to_base64(file_path: str) -> str: + """Return base64 encoded string for given file""" + with open(file_path, 'rb') as file: + return base64.b64encode(file.read()).decode('utf-8') diff --git a/test/functional/android/desired_capabilities.py b/appium/common/logger.py similarity index 59% rename from test/functional/android/desired_capabilities.py rename to appium/common/logger.py index beafcc2f..014b69e4 100644 --- a/test/functional/android/desired_capabilities.py +++ b/appium/common/logger.py @@ -12,20 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os +import logging +import sys -# Returns abs path relative to this file and not cwd -PATH = lambda p: os.path.abspath( - os.path.join(os.path.dirname(__file__), p) -) +def setup_logger(level: int = logging.NOTSET) -> None: + logger.propagate = False + logger.setLevel(level) + handler = logging.StreamHandler(stream=sys.stderr) + logger.addHandler(handler) -def get_desired_capabilities(app): - desired_caps = { - 'platformName': 'Android', - 'platformVersion': '4.2', - 'deviceName': 'Android Emulator', - 'app': PATH('../../apps/' + app), - } - return desired_caps +# global logger +logger = logging.getLogger(__name__) +setup_logger() diff --git a/appium/options/__init__.py b/appium/options/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/android/__init__.py b/appium/options/android/__init__.py new file mode 100644 index 00000000..8926c5a8 --- /dev/null +++ b/appium/options/android/__init__.py @@ -0,0 +1,2 @@ +from .espresso.base import EspressoOptions +from .uiautomator2.base import UiAutomator2Options diff --git a/appium/options/android/common/__init__.py b/appium/options/android/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/android/common/adb/__init__.py b/appium/options/android/common/adb/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/android/common/adb/adb_exec_timeout_option.py b/appium/options/android/common/adb/adb_exec_timeout_option.py new file mode 100644 index 00000000..f0ea5b80 --- /dev/null +++ b/appium/options/android/common/adb/adb_exec_timeout_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ADB_EXEC_TIMEOUT = 'adbExecTimeout' + + +class AdbExecTimeoutOption(SupportsCapabilities): + @property + def adb_exec_timeout(self) -> Optional[timedelta]: + """ + Maximum time to wait until single ADB command is executed. + """ + value = self.get_capability(ADB_EXEC_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @adb_exec_timeout.setter + def adb_exec_timeout(self, value: Union[timedelta, int]) -> None: + """ + Maximum time to wait until single ADB command is executed. + 20000 ms by default. + """ + self.set_capability(ADB_EXEC_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/options/android/common/adb/adb_port_option.py b/appium/options/android/common/adb/adb_port_option.py new file mode 100644 index 00000000..4685c7a0 --- /dev/null +++ b/appium/options/android/common/adb/adb_port_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ADB_PORT = 'adbPort' + + +class AdbPortOption(SupportsCapabilities): + @property + def adb_port(self) -> Optional[int]: + """ + Number of the port where ADB is running. + """ + return self.get_capability(ADB_PORT) + + @adb_port.setter + def adb_port(self, value: int) -> None: + """ + Set number of the port where ADB is running. 5037 by default + """ + self.set_capability(ADB_PORT, value) diff --git a/appium/options/android/common/adb/allow_delay_adb_option.py b/appium/options/android/common/adb/allow_delay_adb_option.py new file mode 100644 index 00000000..cafd6ff3 --- /dev/null +++ b/appium/options/android/common/adb/allow_delay_adb_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ALLOW_DELAY_ADB = 'allowDelayAdb' + + +class AllowDelayAdbOption(SupportsCapabilities): + @property + def allow_delay_adb(self) -> Optional[bool]: + """ + Whether to prevent the emulator to use -delay-adb feature. + """ + return self.get_capability(ALLOW_DELAY_ADB) + + @allow_delay_adb.setter + def allow_delay_adb(self, value: bool) -> None: + """ + Being set to false prevents emulator to use -delay-adb feature to detect its startup. + See https://github.com/appium/appium/issues/14773 for more details. + """ + self.set_capability(ALLOW_DELAY_ADB, value) diff --git a/appium/options/android/common/adb/build_tools_version_option.py b/appium/options/android/common/adb/build_tools_version_option.py new file mode 100644 index 00000000..a5cd96c7 --- /dev/null +++ b/appium/options/android/common/adb/build_tools_version_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +BUILD_TOOLS_VERSION = 'buildToolsVersion' + + +class BuildToolsVersionOption(SupportsCapabilities): + @property + def build_tools_version(self) -> Optional[str]: + """ + Version of Android build tools to use. + """ + return self.get_capability(BUILD_TOOLS_VERSION) + + @build_tools_version.setter + def build_tools_version(self, value: str) -> None: + """ + The version of Android build tools to use. By default, UiAutomator2 + driver uses the most recent version of build tools installed on + the machine, but sometimes it might be necessary to give it a hint + (let say if there is a known bug in the most recent tools version). + Example: 28.0.3 + """ + self.set_capability(BUILD_TOOLS_VERSION, value) diff --git a/appium/options/android/common/adb/clear_device_logs_on_start_option.py b/appium/options/android/common/adb/clear_device_logs_on_start_option.py new file mode 100644 index 00000000..050cc6a7 --- /dev/null +++ b/appium/options/android/common/adb/clear_device_logs_on_start_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CLEAR_DEVICE_LOGS_ON_START = 'clearDeviceLogsOnStart' + + +class ClearDeviceLogsOnStartOption(SupportsCapabilities): + @property + def clear_device_logs_on_start(self) -> Optional[bool]: + """ + Makes the driver to delete all the existing logs in the + device buffer before starting a new test. + """ + return self.get_capability(CLEAR_DEVICE_LOGS_ON_START) + + @clear_device_logs_on_start.setter + def clear_device_logs_on_start(self, value: bool) -> None: + """ + If set to true then the driver deletes all the existing logs in the + device buffer before starting a new test. + """ + self.set_capability(CLEAR_DEVICE_LOGS_ON_START, value) diff --git a/appium/options/android/common/adb/ignore_hidden_api_policy_error_option.py b/appium/options/android/common/adb/ignore_hidden_api_policy_error_option.py new file mode 100644 index 00000000..4813ed24 --- /dev/null +++ b/appium/options/android/common/adb/ignore_hidden_api_policy_error_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +IGNORE_HIDDEN_API_POLICY_ERROR = 'ignoreHiddenApiPolicyError' + + +class IgnoreHiddenApiPolicyErrorOption(SupportsCapabilities): + @property + def ignore_hidden_api_policy_error(self) -> Optional[bool]: + """ + Whether to ignore a failure while changing hidden API access policies. + """ + return self.get_capability(IGNORE_HIDDEN_API_POLICY_ERROR) + + @ignore_hidden_api_policy_error.setter + def ignore_hidden_api_policy_error(self, value: bool) -> None: + """ + Being set to true ignores a failure while changing hidden API access policies. + Could be useful on some devices, where access to these policies has been locked by its vendor. + false by default. + """ + self.set_capability(IGNORE_HIDDEN_API_POLICY_ERROR, value) diff --git a/appium/options/android/common/adb/logcat_filter_specs_option.py b/appium/options/android/common/adb/logcat_filter_specs_option.py new file mode 100644 index 00000000..8a464d35 --- /dev/null +++ b/appium/options/android/common/adb/logcat_filter_specs_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +LOGCAT_FILTER_SPECS = 'logcatFilterSpecs' + + +class LogcatFilterSpecsOption(SupportsCapabilities): + @property + def logcat_filter_specs(self) -> Optional[str]: + """ + Logcat filter format. + """ + return self.get_capability(LOGCAT_FILTER_SPECS) + + @logcat_filter_specs.setter + def logcat_filter_specs(self, value: str) -> None: + """ + Series of tag[:priority] where tag is a log component tag (or * for all) + and priority is: V Verbose, D Debug, I Info, W Warn, E Error, F Fatal, + S Silent (suppress all output). '' means ':d' and tag by itself means tag:v. + If not specified on the commandline, filterspec is set from ANDROID_LOG_TAGS. + If no filterspec is found, filter defaults to '*:I'. + """ + self.set_capability(LOGCAT_FILTER_SPECS, value) diff --git a/appium/options/android/common/adb/logcat_format_option.py b/appium/options/android/common/adb/logcat_format_option.py new file mode 100644 index 00000000..363a6f5a --- /dev/null +++ b/appium/options/android/common/adb/logcat_format_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +LOGCAT_FORMAT = 'logcatFormat' + + +class LogcatFormatOption(SupportsCapabilities): + @property + def logcat_format(self) -> Optional[str]: + """ + Log print format. + """ + return self.get_capability(LOGCAT_FORMAT) + + @logcat_format.setter + def logcat_format(self, value: str) -> None: + """ + The log print format, where format is one of: brief process tag thread raw time + threadtime long. threadtime is the default value. + """ + self.set_capability(LOGCAT_FORMAT, value) diff --git a/appium/options/android/common/adb/mock_location_app_option.py b/appium/options/android/common/adb/mock_location_app_option.py new file mode 100644 index 00000000..d263ea0e --- /dev/null +++ b/appium/options/android/common/adb/mock_location_app_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +MOCK_LOCATION_APP = 'mockLocationApp' + + +class MockLocationAppOption(SupportsCapabilities): + @property + def mock_location_app(self) -> Optional[str]: + """ + Identifier of the app, which is used as a system mock location provider. + """ + return self.get_capability(MOCK_LOCATION_APP) + + @mock_location_app.setter + def mock_location_app(self, value: str) -> None: + """ + Sets the package identifier of the app, which is used as a system mock location + provider since Appium 1.18.0+. This capability has no effect on emulators. + If the value is set to null or an empty string, then Appium will skip the mocked + location provider setup procedure. Defaults to Appium Setting package + identifier (io.appium.settings). + """ + self.set_capability(MOCK_LOCATION_APP, value) diff --git a/appium/options/android/common/adb/remote_adb_host_option.py b/appium/options/android/common/adb/remote_adb_host_option.py new file mode 100644 index 00000000..1656e9bd --- /dev/null +++ b/appium/options/android/common/adb/remote_adb_host_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +REMOTE_ADB_HOST = 'remoteAdbHost' + + +class RemoteAdbHostOption(SupportsCapabilities): + @property + def remote_adb_host(self) -> Optional[str]: + """ + Address of the host where ADB is running. + """ + return self.get_capability(REMOTE_ADB_HOST) + + @remote_adb_host.setter + def remote_adb_host(self, value: str) -> None: + """ + Address of the host where ADB is running (the value of -H ADB command line option). + Localhost by default. + """ + self.set_capability(REMOTE_ADB_HOST, value) diff --git a/appium/options/android/common/adb/skip_logcat_capture_option.py b/appium/options/android/common/adb/skip_logcat_capture_option.py new file mode 100644 index 00000000..2f9f9074 --- /dev/null +++ b/appium/options/android/common/adb/skip_logcat_capture_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SKIP_LOGCAT_CAPTURE = 'skipLogcatCapture' + + +class SkipLogcatCaptureOption(SupportsCapabilities): + @property + def skip_logcat_capture(self) -> Optional[bool]: + """ + Whether to delete all the existing logs in the + device buffer before starting a new test. + """ + return self.get_capability(SKIP_LOGCAT_CAPTURE) + + @skip_logcat_capture.setter + def skip_logcat_capture(self, value: bool) -> None: + """ + Being set to true disables automatic logcat output collection during the test run. + false by default + """ + self.set_capability(SKIP_LOGCAT_CAPTURE, value) diff --git a/appium/options/android/common/adb/suppress_kill_server_option.py b/appium/options/android/common/adb/suppress_kill_server_option.py new file mode 100644 index 00000000..fb773899 --- /dev/null +++ b/appium/options/android/common/adb/suppress_kill_server_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SUPPRESS_KILL_SERVER = 'suppressKillServer' + + +class SuppressKillServerOption(SupportsCapabilities): + @property + def suppress_kill_server(self) -> Optional[bool]: + """ + Prevents the driver from ever killing the ADB server explicitly. + """ + return self.get_capability(SUPPRESS_KILL_SERVER) + + @suppress_kill_server.setter + def suppress_kill_server(self, value: bool) -> None: + """ + Being set to true prevents the driver from ever killing the ADB server explicitly. + Could be useful if ADB is connected wirelessly. false by default. + """ + self.set_capability(SUPPRESS_KILL_SERVER, value) diff --git a/appium/options/android/common/app/__init__.py b/appium/options/android/common/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/android/common/app/allow_test_packages_option.py b/appium/options/android/common/app/allow_test_packages_option.py new file mode 100644 index 00000000..b0913693 --- /dev/null +++ b/appium/options/android/common/app/allow_test_packages_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ALLOW_TEST_PACKAGES = 'allowTestPackages' + + +class AllowTestPackagesOption(SupportsCapabilities): + @property + def allow_test_packages(self) -> Optional[bool]: + """ + Whether it is possible to use packages built with the test flag for + the automated testing (literally adds -t flag to the adb install command). + """ + return self.get_capability(ALLOW_TEST_PACKAGES) + + @allow_test_packages.setter + def allow_test_packages(self, value: bool) -> None: + """ + If set to true then it would be possible to use packages built with the test flag for + the automated testing (literally adds -t flag to the adb install command). false by default. + """ + self.set_capability(ALLOW_TEST_PACKAGES, value) diff --git a/appium/options/android/common/app/android_install_timeout_option.py b/appium/options/android/common/app/android_install_timeout_option.py new file mode 100644 index 00000000..38a99764 --- /dev/null +++ b/appium/options/android/common/app/android_install_timeout_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ANDROID_INSTALL_TIMEOUT = 'androidInstallTimeout' + + +class AndroidInstallTimeoutOption(SupportsCapabilities): + @property + def android_install_timeout(self) -> Optional[timedelta]: + """ + Maximum amount of time to wait until the application under test is installed. + """ + value = self.get_capability(ANDROID_INSTALL_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @android_install_timeout.setter + def android_install_timeout(self, value: Union[timedelta, int]) -> None: + """ + Maximum amount of time to wait until the application under test is installed. + 90000 ms by default + """ + self.set_capability( + ANDROID_INSTALL_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value + ) diff --git a/appium/options/android/common/app/app_activity_option.py b/appium/options/android/common/app/app_activity_option.py new file mode 100644 index 00000000..41f66eed --- /dev/null +++ b/appium/options/android/common/app/app_activity_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_ACTIVITY = 'appActivity' + + +class AppActivityOption(SupportsCapabilities): + @property + def app_activity(self) -> Optional[str]: + """ + Name of the main app activity. + """ + return self.get_capability(APP_ACTIVITY) + + @app_activity.setter + def app_activity(self, value: str) -> None: + """ + Main application activity identifier. If not provided then the driver + will try to detect it automatically from the package provided by the app capability. + """ + self.set_capability(APP_ACTIVITY, value) diff --git a/appium/options/android/common/app/app_package_option.py b/appium/options/android/common/app/app_package_option.py new file mode 100644 index 00000000..a8c7b065 --- /dev/null +++ b/appium/options/android/common/app/app_package_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_PACKAGE = 'appPackage' + + +class AppPackageOption(SupportsCapabilities): + @property + def app_package(self) -> Optional[str]: + """ + App package identifier. + """ + return self.get_capability(APP_PACKAGE) + + @app_package.setter + def app_package(self, value: str) -> None: + """ + Application package identifier to be started. If not provided then the driver will + try to detect it automatically from the package provided by the app capability. + """ + self.set_capability(APP_PACKAGE, value) diff --git a/appium/options/android/common/app/app_wait_activity_option.py b/appium/options/android/common/app/app_wait_activity_option.py new file mode 100644 index 00000000..5d72f311 --- /dev/null +++ b/appium/options/android/common/app/app_wait_activity_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_WAIT_ACTIVITY = 'appWaitActivity' + + +class AppWaitActivityOption(SupportsCapabilities): + @property + def app_wait_activity(self) -> Optional[str]: + """ + Name of the app activity to wait for. + """ + return self.get_capability(APP_WAIT_ACTIVITY) + + @app_wait_activity.setter + def app_wait_activity(self, value: str) -> None: + """ + Identifier of the activity that the driver should wait for + (not necessarily the main one). + If not provided then defaults to appium:appActivity. + """ + self.set_capability(APP_WAIT_ACTIVITY, value) diff --git a/appium/options/android/common/app/app_wait_duration_option.py b/appium/options/android/common/app/app_wait_duration_option.py new file mode 100644 index 00000000..362cb372 --- /dev/null +++ b/appium/options/android/common/app/app_wait_duration_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_WAIT_DURATION = 'appWaitDuration' + + +class AppWaitDurationOption(SupportsCapabilities): + @property + def app_wait_duration(self) -> Optional[timedelta]: + """ + Identifier of the app package to wait for. + """ + value = self.get_capability(APP_WAIT_DURATION) + return None if value is None else timedelta(milliseconds=value) + + @app_wait_duration.setter + def app_wait_duration(self, value: Union[timedelta, int]) -> None: + """ + Maximum amount of time to wait until the application under test is started + (e.g. an activity returns the control to the caller). 20000 ms by default. + """ + self.set_capability(APP_WAIT_DURATION, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/options/android/common/app/app_wait_for_launch_option.py b/appium/options/android/common/app/app_wait_for_launch_option.py new file mode 100644 index 00000000..5f5b48c5 --- /dev/null +++ b/appium/options/android/common/app/app_wait_for_launch_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_WAIT_FOR_LAUNCH = 'appWaitForLaunch' + + +class AppWaitForLaunchOption(SupportsCapabilities): + @property + def app_wait_for_launch(self) -> Optional[bool]: + """ + Whether to block until the app under test returns the control to the + caller after its activity has been started by Activity Manager. + """ + return self.get_capability(APP_WAIT_FOR_LAUNCH) + + @app_wait_for_launch.setter + def app_wait_for_launch(self, value: bool) -> None: + """ + Whether to block until the app under test returns the control to the + caller after its activity has been started by Activity Manager + (true, the default value) or to continue the test without waiting for that (false). + """ + self.set_capability(APP_WAIT_FOR_LAUNCH, value) diff --git a/appium/options/android/common/app/app_wait_package_option.py b/appium/options/android/common/app/app_wait_package_option.py new file mode 100644 index 00000000..9df4fa0d --- /dev/null +++ b/appium/options/android/common/app/app_wait_package_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_WAIT_PACKAGE = 'appWaitPackage' + + +class AppWaitPackageOption(SupportsCapabilities): + @property + def app_wait_package(self) -> Optional[str]: + """ + Identifier of the app package to wait for. + """ + return self.get_capability(APP_WAIT_PACKAGE) + + @app_wait_package.setter + def app_wait_package(self, value: str) -> None: + """ + Identifier of the package that the driver should wait for + (not necessarily the main one). + If not provided then defaults to appium:appPackage. + """ + self.set_capability(APP_WAIT_PACKAGE, value) diff --git a/appium/options/android/common/app/auto_grant_premissions_option.py b/appium/options/android/common/app/auto_grant_premissions_option.py new file mode 100644 index 00000000..e276e7da --- /dev/null +++ b/appium/options/android/common/app/auto_grant_premissions_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +AUTO_GRANT_PERMISSIONS = 'autoGrantPermissions' + + +class AutoGrantPermissionsOption(SupportsCapabilities): + @property + def auto_grant_permissions(self) -> Optional[bool]: + """ + Whether to grant all the requested application permissions + automatically when a test starts. + """ + return self.get_capability(AUTO_GRANT_PERMISSIONS) + + @auto_grant_permissions.setter + def auto_grant_permissions(self, value: bool) -> None: + """ + Whether to grant all the requested application permissions + automatically when a test starts(true). false by default. + """ + self.set_capability(AUTO_GRANT_PERMISSIONS, value) diff --git a/appium/options/android/common/app/enforce_app_install_option.py b/appium/options/android/common/app/enforce_app_install_option.py new file mode 100644 index 00000000..85d128ed --- /dev/null +++ b/appium/options/android/common/app/enforce_app_install_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ENFORCE_APP_INSTALL = 'enforceAppInstall' + + +class EnforceAppInstallOption(SupportsCapabilities): + @property + def enforce_app_install(self) -> Optional[bool]: + """ + Whether the application under test is always reinstalled even + if a newer version of it already exists on the device under test. + """ + return self.get_capability(ENFORCE_APP_INSTALL) + + @enforce_app_install.setter + def enforce_app_install(self, value: bool) -> None: + """ + Allows setting whether the application under test is always reinstalled even + if a newer version of it already exists on the device under test. false by default. + """ + self.set_capability(ENFORCE_APP_INSTALL, value) diff --git a/appium/options/android/common/app/intent_action_option.py b/appium/options/android/common/app/intent_action_option.py new file mode 100644 index 00000000..8019bcc5 --- /dev/null +++ b/appium/options/android/common/app/intent_action_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +INTENT_ACTION = 'intentAction' + + +class IntentActionOption(SupportsCapabilities): + @property + def intent_action(self) -> Optional[str]: + """ + Intent action to be applied when + starting the given appActivity by Activity Manager. + """ + return self.get_capability(INTENT_ACTION) + + @intent_action.setter + def intent_action(self, value: str) -> None: + """ + Set an optional intent action to be applied when + starting the given appActivity by Activity Manager. + """ + self.set_capability(INTENT_ACTION, value) diff --git a/appium/options/android/common/app/intent_category_option.py b/appium/options/android/common/app/intent_category_option.py new file mode 100644 index 00000000..f6f568c2 --- /dev/null +++ b/appium/options/android/common/app/intent_category_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +INTENT_CATEGORY = 'intentCategory' + + +class IntentCategoryOption(SupportsCapabilities): + @property + def intent_category(self) -> Optional[str]: + """ + Intent category to be applied when + starting the given appActivity by Activity Manager. + """ + return self.get_capability(INTENT_CATEGORY) + + @intent_category.setter + def intent_category(self, value: str) -> None: + """ + Set an optional intent category to be applied when + starting the given appActivity by Activity Manager. + """ + self.set_capability(INTENT_CATEGORY, value) diff --git a/appium/options/android/common/app/intent_flags_option.py b/appium/options/android/common/app/intent_flags_option.py new file mode 100644 index 00000000..141e51c3 --- /dev/null +++ b/appium/options/android/common/app/intent_flags_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +INTENT_FLAGS = 'intentFlags' + + +class IntentFlagsOption(SupportsCapabilities): + @property + def intent_flags(self) -> Optional[str]: + """ + Intent flags to be applied when + starting the given appActivity by Activity Manager. + """ + return self.get_capability(INTENT_FLAGS) + + @intent_flags.setter + def intent_flags(self, value: str) -> None: + """ + Set optional intent flags to be applied when + starting the given appActivity by Activity Manager. + """ + self.set_capability(INTENT_FLAGS, value) diff --git a/appium/options/android/common/app/optional_intent_arguments_option.py b/appium/options/android/common/app/optional_intent_arguments_option.py new file mode 100644 index 00000000..3411fe96 --- /dev/null +++ b/appium/options/android/common/app/optional_intent_arguments_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +OPTIONAL_INTENT_ARGUMENTS = 'optionalIntentArguments' + + +class OptionalIntentArgumentsOption(SupportsCapabilities): + @property + def optional_intent_arguments(self) -> Optional[str]: + """ + Intent arguments to be applied when + starting the given appActivity by Activity Manager. + """ + return self.get_capability(OPTIONAL_INTENT_ARGUMENTS) + + @optional_intent_arguments.setter + def optional_intent_arguments(self, value: str) -> None: + """ + Set optional intent arguments to be applied when + starting the given appActivity by Activity Manager. + """ + self.set_capability(OPTIONAL_INTENT_ARGUMENTS, value) diff --git a/appium/options/android/common/app/remote_apps_cache_limit_option.py b/appium/options/android/common/app/remote_apps_cache_limit_option.py new file mode 100644 index 00000000..0f21ec78 --- /dev/null +++ b/appium/options/android/common/app/remote_apps_cache_limit_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +REMOTE_APPS_CACHE_LIMIT = 'remoteAppsCacheLimit' + + +class RemoteAppsCacheLimitOption(SupportsCapabilities): + @property + def remote_apps_cache_limit(self) -> Optional[int]: + """ + Maximum amount of apps that could be cached on the remote device. + """ + return self.get_capability(REMOTE_APPS_CACHE_LIMIT) + + @remote_apps_cache_limit.setter + def remote_apps_cache_limit(self, value: int) -> None: + """ + Set the maximum amount of application packages to be cached on the device under test. + This is needed for devices that don't support streamed installs (Android 7 and below), + because ADB must push app packages to the device first in order to install them, + which takes some time. Setting this capability to zero disables apps caching. + 10 by default. + """ + self.set_capability(REMOTE_APPS_CACHE_LIMIT, value) diff --git a/appium/options/android/common/app/uninstall_other_packages_option.py b/appium/options/android/common/app/uninstall_other_packages_option.py new file mode 100644 index 00000000..2cd05cdf --- /dev/null +++ b/appium/options/android/common/app/uninstall_other_packages_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +UNINSTALL_OTHER_PACKAGES = 'uninstallOtherPackages' + + +class UninstallOtherPackagesOption(SupportsCapabilities): + @property + def uninstall_other_packages(self) -> Optional[str]: + """ + Identifiers of packages to be uninstalled from the device before a test starts. + """ + return self.get_capability(UNINSTALL_OTHER_PACKAGES) + + @uninstall_other_packages.setter + def uninstall_other_packages(self, value: str) -> None: + """ + Allows to set one or more comma-separated package + identifiers to be uninstalled from the device before a test starts. + """ + self.set_capability(UNINSTALL_OTHER_PACKAGES, value) diff --git a/appium/options/android/common/avd/__init__.py b/appium/options/android/common/avd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/android/common/avd/avd_args_option.py b/appium/options/android/common/avd/avd_args_option.py new file mode 100644 index 00000000..a8b7b750 --- /dev/null +++ b/appium/options/android/common/avd/avd_args_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +AVD_ARGS = 'avdArgs' + + +class AvdArgsOption(SupportsCapabilities): + @property + def avd_args(self) -> Optional[str]: + """ + Emulator command line arguments. + """ + return self.get_capability(AVD_ARGS) + + @avd_args.setter + def avd_args(self, value: str) -> None: + """ + Set emulator command line arguments. + """ + self.set_capability(AVD_ARGS, value) diff --git a/appium/options/android/common/avd/avd_env_option.py b/appium/options/android/common/avd/avd_env_option.py new file mode 100644 index 00000000..a673a95f --- /dev/null +++ b/appium/options/android/common/avd/avd_env_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +AVD_ENV = 'avdEnv' + + +class AvdEnvOption(SupportsCapabilities): + @property + def avd_env(self) -> Optional[Dict[str, str]]: + """ + Mapping of emulator environment variables. + """ + return self.get_capability(AVD_ENV) + + @avd_env.setter + def avd_env(self, value: Dict[str, str]) -> None: + """ + Set the mapping of emulator environment variables. + """ + self.set_capability(AVD_ENV, value) diff --git a/appium/options/android/common/avd/avd_launch_timeout_option.py b/appium/options/android/common/avd/avd_launch_timeout_option.py new file mode 100644 index 00000000..017396ef --- /dev/null +++ b/appium/options/android/common/avd/avd_launch_timeout_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +AVD_LAUNCH_TIMEOUT = 'avdLaunchTimeout' + + +class AvdLaunchTimeoutOption(SupportsCapabilities): + @property + def avd_launch_timeout(self) -> Optional[timedelta]: + """ + Timeout to wait until Android Emulator is started. + """ + value = self.get_capability(AVD_LAUNCH_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @avd_launch_timeout.setter + def avd_launch_timeout(self, value: Union[timedelta, int]) -> None: + """ + Maximum timeout to wait until Android Emulator is started. + 60000 ms by default. + """ + self.set_capability(AVD_LAUNCH_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/options/android/common/avd/avd_option.py b/appium/options/android/common/avd/avd_option.py new file mode 100644 index 00000000..42c543fc --- /dev/null +++ b/appium/options/android/common/avd/avd_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +AVD = 'avd' + + +class AvdOption(SupportsCapabilities): + @property + def avd(self) -> Optional[str]: + """ + Name of Android emulator to run the test on. + """ + return self.get_capability(AVD) + + @avd.setter + def avd(self, value: str) -> None: + """ + The name of Android emulator to run the test on. + Names of currently installed emulators could be listed using + avdmanager list avd command. If the emulator with the given name + is not running then it is going to be started before a test. + """ + self.set_capability(AVD, value) diff --git a/appium/options/android/common/avd/avd_ready_timeout_option.py b/appium/options/android/common/avd/avd_ready_timeout_option.py new file mode 100644 index 00000000..c074b319 --- /dev/null +++ b/appium/options/android/common/avd/avd_ready_timeout_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +AVD_READY_TIMEOUT = 'avdReadyTimeout' + + +class AvdReadyTimeoutOption(SupportsCapabilities): + @property + def avd_ready_timeout(self) -> Optional[timedelta]: + """ + Timeout to wait until Android Emulator is fully booted and is ready for usage. + """ + value = self.get_capability(AVD_READY_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @avd_ready_timeout.setter + def avd_ready_timeout(self, value: Union[timedelta, int]) -> None: + """ + Maximum timeout to wait until Android Emulator is fully booted and is ready for usage. + 60000 ms by default + """ + self.set_capability(AVD_READY_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/options/android/common/avd/gps_enabled_option.py b/appium/options/android/common/avd/gps_enabled_option.py new file mode 100644 index 00000000..c9b3c60b --- /dev/null +++ b/appium/options/android/common/avd/gps_enabled_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +GPS_ENABLED = 'gpsEnabled' + + +class GpsEnabledOption(SupportsCapabilities): + @property + def gps_enabled(self) -> Optional[bool]: + """ + State of the GPS service on emulator. + """ + return self.get_capability(GPS_ENABLED) + + @gps_enabled.setter + def gps_enabled(self, value: bool) -> None: + """ + Set whether to enable (true) or disable (false) GPS service in the Emulator. + Unset by default, which means to not change the current value. + """ + self.set_capability(GPS_ENABLED, value) diff --git a/appium/options/android/common/avd/network_speed_option.py b/appium/options/android/common/avd/network_speed_option.py new file mode 100644 index 00000000..1ab6c1cb --- /dev/null +++ b/appium/options/android/common/avd/network_speed_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +NETWORK_SPEED = 'networkSpeed' + + +class NetworkSpeedOption(SupportsCapabilities): + @property + def network_speed(self) -> Optional[str]: + """ + Desired network speed limit for the emulator. + """ + return self.get_capability(NETWORK_SPEED) + + @network_speed.setter + def network_speed(self, value: str) -> None: + """ + Sets the desired network speed limit for the emulator. + It is only applied if the emulator is not running before + the test starts. See emulator command line arguments description + for more details. + """ + self.set_capability(NETWORK_SPEED, value) diff --git a/appium/options/android/common/context/__init__.py b/appium/options/android/common/context/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/android/common/context/auto_webview_timeout_option.py b/appium/options/android/common/context/auto_webview_timeout_option.py new file mode 100644 index 00000000..77457431 --- /dev/null +++ b/appium/options/android/common/context/auto_webview_timeout_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +AUTO_WEBVIEW_TIMEOUT = 'autoWebviewTimeout' + + +class AutoWebviewTimeoutOption(SupportsCapabilities): + @property + def auto_webview_timeout(self) -> Optional[timedelta]: + """ + Set the maximum timeout to wait until a web view is + available if autoWebview capability is set to true. 2000 ms by default. + """ + value = self.get_capability(AUTO_WEBVIEW_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @auto_webview_timeout.setter + def auto_webview_timeout(self, value: Union[timedelta, int]) -> None: + """ + Timeout to wait until a web view is available. + """ + self.set_capability(AUTO_WEBVIEW_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/options/android/common/context/chrome_logging_prefs_option.py b/appium/options/android/common/context/chrome_logging_prefs_option.py new file mode 100644 index 00000000..0c2b69c4 --- /dev/null +++ b/appium/options/android/common/context/chrome_logging_prefs_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Any, Dict, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CHROME_LOGGING_PREFS = 'chromeLoggingPrefs' + + +class ChromeLoggingPrefsOption(SupportsCapabilities): + @property + def chrome_logging_prefs(self) -> Optional[Dict[str, Any]]: + """ + Chrome logging preferences. + """ + return self.get_capability(CHROME_LOGGING_PREFS) + + @chrome_logging_prefs.setter + def chrome_logging_prefs(self, value: Dict[str, Any]) -> None: + """ + Chrome logging preferences mapping. Basically the same as + [goog:loggingPrefs](https://newbedev.com/ + getting-console-log-output-from-chrome-with-selenium-python-api-bindings). + It is set to {"browser": "ALL"} by default. + """ + self.set_capability(CHROME_LOGGING_PREFS, value) diff --git a/appium/options/android/common/context/chrome_options_option.py b/appium/options/android/common/context/chrome_options_option.py new file mode 100644 index 00000000..3adb9ceb --- /dev/null +++ b/appium/options/android/common/context/chrome_options_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Any, Dict, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CHROME_OPTIONS = 'chromeOptions' + + +class ChromeOptionsOption(SupportsCapabilities): + @property + def chrome_options(self) -> Optional[Dict[str, Any]]: + """ + Chrome options. + """ + return self.get_capability(CHROME_OPTIONS) + + @chrome_options.setter + def chrome_options(self, value: Dict[str, Any]) -> None: + """ + A mapping, that allows to customize chromedriver options. + See https://chromedriver.chromium.org/capabilities for the list + of available entries. + """ + self.set_capability(CHROME_OPTIONS, value) diff --git a/appium/options/android/common/context/chromedriver_args_option.py b/appium/options/android/common/context/chromedriver_args_option.py new file mode 100644 index 00000000..b3633b10 --- /dev/null +++ b/appium/options/android/common/context/chromedriver_args_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import List, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CHROMEDRIVER_ARGS = 'chromedriverArgs' + + +class ChromedriverArgsOption(SupportsCapabilities): + @property + def chromedriver_args(self) -> Optional[List[str]]: + """ + Array of chromedriver CLI arguments. + """ + return self.get_capability(CHROMEDRIVER_ARGS) + + @chromedriver_args.setter + def chromedriver_args(self, value: List[str]) -> None: + """ + Array of chromedriver [command line + arguments](http://www.assertselenium.com/java/list-of-chrome-driver-command-line-arguments/). + Note, that not all command line arguments that are available for the desktop + browser are also available for the mobile one. + """ + self.set_capability(CHROMEDRIVER_ARGS, value) diff --git a/appium/options/android/common/context/chromedriver_chrome_mapping_file_option.py b/appium/options/android/common/context/chromedriver_chrome_mapping_file_option.py new file mode 100644 index 00000000..cc84bed1 --- /dev/null +++ b/appium/options/android/common/context/chromedriver_chrome_mapping_file_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CHROMEDRIVER_CHROME_MAPPING_FILE = 'chromedriverChromeMappingFile' + + +class ChromedriverChromeMappingFileOption(SupportsCapabilities): + @property + def chromedriver_chrome_mapping_file(self) -> Optional[str]: + """ + Full path to the chromedrivers mapping file is located. + """ + return self.get_capability(CHROMEDRIVER_CHROME_MAPPING_FILE) + + @chromedriver_chrome_mapping_file.setter + def chromedriver_chrome_mapping_file(self, value: str) -> None: + """ + Full path to the chromedrivers mapping file. This file is used to statically + map webview/browser versions to the chromedriver versions that are capable + of automating them. Read [Automatic Chromedriver Discovery](https://github.com/ + appium/appium/blob/master/docs/en/writing-running-appium/web/ + chromedriver.md#automatic-discovery-of-compatible-chromedriver) + article for more details. + """ + self.set_capability(CHROMEDRIVER_CHROME_MAPPING_FILE, value) diff --git a/appium/options/android/common/context/chromedriver_disable_build_check_option.py b/appium/options/android/common/context/chromedriver_disable_build_check_option.py new file mode 100644 index 00000000..16886ab1 --- /dev/null +++ b/appium/options/android/common/context/chromedriver_disable_build_check_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CHROMEDRIVER_DISABLE_BUILD_CHECK = 'chromedriverDisableBuildCheck' + + +class ChromedriverDisableBuildCheckOption(SupportsCapabilities): + @property + def chromedriver_disable_build_check(self) -> Optional[bool]: + """ + Whether to disable the compatibility validation between the current + chromedriver and the destination browser/web view. + """ + return self.get_capability(CHROMEDRIVER_DISABLE_BUILD_CHECK) + + @chromedriver_disable_build_check.setter + def chromedriver_disable_build_check(self, value: bool) -> None: + """ + Being set to true disables the compatibility validation between the current + chromedriver and the destination browser/web view. Use it with care. + false by default. + """ + self.set_capability(CHROMEDRIVER_DISABLE_BUILD_CHECK, value) diff --git a/appium/options/android/common/context/chromedriver_executable_dir_option.py b/appium/options/android/common/context/chromedriver_executable_dir_option.py new file mode 100644 index 00000000..64e96c98 --- /dev/null +++ b/appium/options/android/common/context/chromedriver_executable_dir_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CHROMEDRIVER_EXECUTABLE_DIR = 'chromedriverExecutableDir' + + +class ChromedriverExecutableDirOption(SupportsCapabilities): + @property + def chromedriver_executable_dir(self) -> Optional[str]: + """ + Full path to the folder where chromedriver executables are located. + """ + return self.get_capability(CHROMEDRIVER_EXECUTABLE_DIR) + + @chromedriver_executable_dir.setter + def chromedriver_executable_dir(self, value: str) -> None: + """ + Full path to the folder where chromedriver executables are located. + This folder is used then to store the downloaded chromedriver executables + if automatic download is enabled. Read [Automatic Chromedriver + Discovery](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/ + web/chromedriver.md#automatic-discovery-of-compatible-chromedriver) + article for more details. + """ + self.set_capability(CHROMEDRIVER_EXECUTABLE_DIR, value) diff --git a/appium/options/android/common/context/chromedriver_executable_option.py b/appium/options/android/common/context/chromedriver_executable_option.py new file mode 100644 index 00000000..79f30ca1 --- /dev/null +++ b/appium/options/android/common/context/chromedriver_executable_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CHROMEDRIVER_EXECUTABLE = 'chromedriverExecutable' + + +class ChromedriverExecutableOption(SupportsCapabilities): + @property + def chromedriver_executable(self) -> Optional[str]: + """ + Path to the chromedriver executable on the server file system. + """ + return self.get_capability(CHROMEDRIVER_EXECUTABLE) + + @chromedriver_executable.setter + def chromedriver_executable(self, value: str) -> None: + """ + Full path to the chromedriver executable on the server file system. + """ + self.set_capability(CHROMEDRIVER_EXECUTABLE, value) diff --git a/appium/options/android/common/context/chromedriver_port_option.py b/appium/options/android/common/context/chromedriver_port_option.py new file mode 100644 index 00000000..8a7ef8ab --- /dev/null +++ b/appium/options/android/common/context/chromedriver_port_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CHROMEDRIVER_PORT = 'chromedriverPort' + + +class ChromedriverPortOption(SupportsCapabilities): + @property + def chromedriver_port(self) -> Optional[int]: + """ + Local port number to use for Chromedriver communication. + """ + return self.get_capability(CHROMEDRIVER_PORT) + + @chromedriver_port.setter + def chromedriver_port(self, value: int) -> None: + """ + The port number to use for Chromedriver communication. + Any free port number is selected by default if unset. + """ + self.set_capability(CHROMEDRIVER_PORT, value) diff --git a/appium/options/android/common/context/chromedriver_ports_option.py b/appium/options/android/common/context/chromedriver_ports_option.py new file mode 100644 index 00000000..5e13c987 --- /dev/null +++ b/appium/options/android/common/context/chromedriver_ports_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import List, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CHROMEDRIVER_PORTS = 'chromedriverPorts' + + +class ChromedriverPortsOption(SupportsCapabilities): + @property + def chromedriver_ports(self) -> Optional[List[int]]: + """ + Local port numbers to use for Chromedriver communication. + """ + return self.get_capability(CHROMEDRIVER_PORTS) + + @chromedriver_ports.setter + def chromedriver_ports(self, value: List[int]) -> None: + """ + Array of possible port numbers to assign for Chromedriver communication. + If none of the port in this array is free then a server error is thrown. + """ + self.set_capability(CHROMEDRIVER_PORTS, value) diff --git a/appium/options/android/common/context/chromedriver_use_system_executable_option.py b/appium/options/android/common/context/chromedriver_use_system_executable_option.py new file mode 100644 index 00000000..b164ed13 --- /dev/null +++ b/appium/options/android/common/context/chromedriver_use_system_executable_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CHROMEDRIVER_USE_SYSTEM_EXECUTABLE = 'chromedriverUseSystemExecutable' + + +class ChromedriverUseSystemExecutableOption(SupportsCapabilities): + @property + def chromedriver_use_system_executable(self) -> Optional[bool]: + """ + Whether to use the system chromedriver. + """ + return self.get_capability(CHROMEDRIVER_USE_SYSTEM_EXECUTABLE) + + @chromedriver_use_system_executable.setter + def chromedriver_use_system_executable(self, value: bool) -> None: + """ + Set it to true in order to enforce the usage of chromedriver, which gets + downloaded by Appium automatically upon installation. This driver might not + be compatible with the destination browser or a web view. false by default. + """ + self.set_capability(CHROMEDRIVER_USE_SYSTEM_EXECUTABLE, value) diff --git a/appium/options/android/common/context/ensure_webviews_have_pages_option.py b/appium/options/android/common/context/ensure_webviews_have_pages_option.py new file mode 100644 index 00000000..d1f549a9 --- /dev/null +++ b/appium/options/android/common/context/ensure_webviews_have_pages_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ENSURE_WEBVIEWS_HAVE_PAGES = 'ensureWebviewsHavePages' + + +class EnsureWebviewsHavePagesOption(SupportsCapabilities): + @property + def ensure_webviews_have_pages(self) -> Optional[bool]: + """ + Whether to ensure if web views have pages. + """ + return self.get_capability(ENSURE_WEBVIEWS_HAVE_PAGES) + + @ensure_webviews_have_pages.setter + def ensure_webviews_have_pages(self, value: bool) -> None: + """ + Whether to skip web views that have no pages from being shown in getContexts + output. The driver uses devtools connection to retrieve the information about + existing pages. true by default since Appium 1.19.0, false if lower than 1.19.0. + """ + self.set_capability(ENSURE_WEBVIEWS_HAVE_PAGES, value) diff --git a/appium/options/android/common/context/extract_chrome_android_package_from_context_name_option.py b/appium/options/android/common/context/extract_chrome_android_package_from_context_name_option.py new file mode 100644 index 00000000..95559072 --- /dev/null +++ b/appium/options/android/common/context/extract_chrome_android_package_from_context_name_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +EXTRACT_CHROME_ANDROID_PACKAGE_FROM_CONTEXT_NAME = 'extractChromeAndroidPackageFromContextName' + + +class ExtractChromeAndroidPackageFromContextNameOption(SupportsCapabilities): + @property + def extract_chrome_android_package_from_context_name(self) -> Optional[bool]: + """ + Whether to use the android package identifier associated with the context name. + """ + return self.get_capability(EXTRACT_CHROME_ANDROID_PACKAGE_FROM_CONTEXT_NAME) + + @extract_chrome_android_package_from_context_name.setter + def extract_chrome_android_package_from_context_name(self, value: bool) -> None: + """ + If set to true, tell chromedriver to attach to the android package we have associated + with the context name, rather than the package of the application under test. + false by default. + """ + self.set_capability(EXTRACT_CHROME_ANDROID_PACKAGE_FROM_CONTEXT_NAME, value) diff --git a/appium/options/android/common/context/native_web_screenshot_option.py b/appium/options/android/common/context/native_web_screenshot_option.py new file mode 100644 index 00000000..1c59a60b --- /dev/null +++ b/appium/options/android/common/context/native_web_screenshot_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +NATIVE_WEB_SCREENSHOT = 'nativeWebScreenshot' + + +class NativeWebScreenshotOption(SupportsCapabilities): + @property + def native_web_screenshot(self) -> Optional[bool]: + """ + Whether to use native screenshots in web view context. + """ + return self.get_capability(NATIVE_WEB_SCREENSHOT) + + @native_web_screenshot.setter + def native_web_screenshot(self, value: bool) -> None: + """ + Whether to use screenshoting endpoint provided by UiAutomator framework (true) + rather than the one provided by chromedriver (false, the default value). + Use it when you experience issues with the latter. + """ + self.set_capability(NATIVE_WEB_SCREENSHOT, value) diff --git a/appium/options/android/common/context/recreate_chrome_driver_sessions_option.py b/appium/options/android/common/context/recreate_chrome_driver_sessions_option.py new file mode 100644 index 00000000..4ef8d109 --- /dev/null +++ b/appium/options/android/common/context/recreate_chrome_driver_sessions_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +RECREATE_CHROME_DRIVER_SESSIONS = 'recreateChromeDriverSessions' + + +class RecreateChromeDriverSessionsOption(SupportsCapabilities): + @property + def recreate_chrome_driver_sessions(self) -> Optional[bool]: + """ + Whether chromedriver sessions should be killed and then recreated instead + of just suspending it on context switch. + """ + return self.get_capability(RECREATE_CHROME_DRIVER_SESSIONS) + + @recreate_chrome_driver_sessions.setter + def recreate_chrome_driver_sessions(self, value: bool) -> None: + """ + If this capability is set to true then chromedriver session is always going + to be killed and then recreated instead of just suspending it on context + switching. false by default. + """ + self.set_capability(RECREATE_CHROME_DRIVER_SESSIONS, value) diff --git a/appium/options/android/common/context/show_chromedriver_log_option.py b/appium/options/android/common/context/show_chromedriver_log_option.py new file mode 100644 index 00000000..da4f283a --- /dev/null +++ b/appium/options/android/common/context/show_chromedriver_log_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SHOW_CHROMEDRIVER_LOG = 'showChromedriverLog' + + +class ShowChromedriverLogOption(SupportsCapabilities): + @property + def show_chromedriver_log(self) -> Optional[bool]: + """ + Whether to forward chromedriver output to the Appium server log. + """ + return self.get_capability(SHOW_CHROMEDRIVER_LOG) + + @show_chromedriver_log.setter + def show_chromedriver_log(self, value: bool) -> None: + """ + If set to true then all the output from chromedriver binary will be + forwarded to the Appium server log. false by default. + """ + self.set_capability(SHOW_CHROMEDRIVER_LOG, value) diff --git a/appium/options/android/common/context/webview_devtools_port_option.py b/appium/options/android/common/context/webview_devtools_port_option.py new file mode 100644 index 00000000..2f0e6d7a --- /dev/null +++ b/appium/options/android/common/context/webview_devtools_port_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WEBVIEW_DEVTOOLS_PORT = 'webviewDevtoolsPort' + + +class WebviewDevtoolsPortOption(SupportsCapabilities): + @property + def webview_devtools_port(self) -> Optional[int]: + """ + Local port number to use for devtools communication. + """ + return self.get_capability(WEBVIEW_DEVTOOLS_PORT) + + @webview_devtools_port.setter + def webview_devtools_port(self, value: int) -> None: + """ + The local port number to use for devtools communication. By default, the first + free port from 10900..11000 range is selected. Consider setting the custom + value if you are running parallel tests. + """ + self.set_capability(WEBVIEW_DEVTOOLS_PORT, value) diff --git a/appium/options/android/common/localization/__init__.py b/appium/options/android/common/localization/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/android/common/localization/locale_script_option.py b/appium/options/android/common/localization/locale_script_option.py new file mode 100644 index 00000000..abcd0852 --- /dev/null +++ b/appium/options/android/common/localization/locale_script_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +LOCALE_SCRIPT = 'localeScript' + + +class LocaleScriptOption(SupportsCapabilities): + @property + def locale_script(self) -> Optional[str]: + """ + Canonical name of the locale to be set for the app under test. + """ + return self.get_capability(LOCALE_SCRIPT) + + @locale_script.setter + def locale_script(self, value: str) -> None: + """ + Set canonical name of the locale to be set for the app under test, + for example zh-Hans-CN. + See https://developer.android.com/reference/java/util/Locale.html for more details. + """ + self.set_capability(LOCALE_SCRIPT, value) diff --git a/appium/options/android/common/locking/__init__.py b/appium/options/android/common/locking/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/android/common/locking/skip_unlock_option.py b/appium/options/android/common/locking/skip_unlock_option.py new file mode 100644 index 00000000..c80a6efa --- /dev/null +++ b/appium/options/android/common/locking/skip_unlock_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SKIP_UNLOCK = 'skipUnlock' + + +class SkipUnlockOption(SupportsCapabilities): + @property + def skip_unlock(self) -> Optional[bool]: + """ + Whether to skip the check for lock screen presence. + """ + return self.get_capability(SKIP_UNLOCK) + + @skip_unlock.setter + def skip_unlock(self, value: bool) -> None: + """ + Whether to skip the check for lock screen presence (true). By default, + the driver tries to detect if the device's screen is locked + before starting the test and to unlock that (which sometimes might be unstable). + Note, that this operation takes some time, so it is highly recommended to set + this capability to true and disable screen locking on devices under test. + """ + self.set_capability(SKIP_UNLOCK, value) diff --git a/appium/options/android/common/locking/unlock_key_option.py b/appium/options/android/common/locking/unlock_key_option.py new file mode 100644 index 00000000..ffee5a1a --- /dev/null +++ b/appium/options/android/common/locking/unlock_key_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +UNLOCK_KEY = 'unlockKey' + + +class UnlockKeyOption(SupportsCapabilities): + @property + def unlock_key(self) -> Optional[str]: + """ + Unlock key. + """ + return self.get_capability(UNLOCK_KEY) + + @unlock_key.setter + def unlock_key(self, value: str) -> None: + """ + Allows to set an unlock key. + Read [Unlock tutorial](https://github.com/appium/appium-android-driver/blob/master/docs/UNLOCK.md) + for more details. + """ + self.set_capability(UNLOCK_KEY, value) diff --git a/appium/options/android/common/locking/unlock_strategy_option.py b/appium/options/android/common/locking/unlock_strategy_option.py new file mode 100644 index 00000000..fb884518 --- /dev/null +++ b/appium/options/android/common/locking/unlock_strategy_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +UNLOCK_STRATEGY = 'unlockStrategy' + + +class UnlockStrategyOption(SupportsCapabilities): + @property + def unlock_strategy(self) -> Optional[str]: + """ + Unlock strategy name. + """ + return self.get_capability(UNLOCK_STRATEGY) + + @unlock_strategy.setter + def unlock_strategy(self, value: str) -> None: + """ + Either 'locksettings' (default) or 'uiautomator'. + Setting it to 'uiautomator' will enforce the driver to avoid using special + ADB shortcuts in order to speed up the unlock procedure. + """ + self.set_capability(UNLOCK_STRATEGY, value) diff --git a/appium/options/android/common/locking/unlock_success_timeout_option.py b/appium/options/android/common/locking/unlock_success_timeout_option.py new file mode 100644 index 00000000..488ae5f9 --- /dev/null +++ b/appium/options/android/common/locking/unlock_success_timeout_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +UNLOCK_SUCCESS_TIMEOUT = 'unlockSuccessTimeout' + + +class UnlockSuccessTimeoutOption(SupportsCapabilities): + @property + def unlock_success_timeout(self) -> Optional[timedelta]: + """ + Timeout to wait until the device is unlocked. + """ + value = self.get_capability(UNLOCK_SUCCESS_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @unlock_success_timeout.setter + def unlock_success_timeout(self, value: Union[timedelta, int]) -> None: + """ + Maximum timeout to wait until the device is unlocked. + 2000 ms by default. + """ + self.set_capability( + UNLOCK_SUCCESS_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value + ) diff --git a/appium/options/android/common/locking/unlock_type_option.py b/appium/options/android/common/locking/unlock_type_option.py new file mode 100644 index 00000000..7026cf34 --- /dev/null +++ b/appium/options/android/common/locking/unlock_type_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +UNLOCK_TYPE = 'unlockType' + + +class UnlockTypeOption(SupportsCapabilities): + @property + def unlock_type(self) -> Optional[str]: + """ + Unlock type. + """ + return self.get_capability(UNLOCK_TYPE) + + @unlock_type.setter + def unlock_type(self, value: str) -> None: + """ + Set one of the possible types of Android lock screens to unlock. + Read the [Unlock tutorial](https://github.com/appium/appium-android-driver/blob/master/docs/UNLOCK.md) + for more details. + """ + self.set_capability(UNLOCK_TYPE, value) diff --git a/appium/options/android/common/mjpeg/__init__.py b/appium/options/android/common/mjpeg/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/android/common/mjpeg/mjpeg_screenshot_url_option.py b/appium/options/android/common/mjpeg/mjpeg_screenshot_url_option.py new file mode 100644 index 00000000..70bad017 --- /dev/null +++ b/appium/options/android/common/mjpeg/mjpeg_screenshot_url_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +MJPEG_SCREENSHOT_URL = 'mjpegScreenshotUrl' + + +class MjpegScreenshotUrlOption(SupportsCapabilities): + @property + def mjpeg_screenshot_url(self) -> Optional[str]: + """ + URL of a service that provides realtime device screenshots in MJPEG format. + """ + return self.get_capability(MJPEG_SCREENSHOT_URL) + + @mjpeg_screenshot_url.setter + def mjpeg_screenshot_url(self, value: str) -> None: + """ + The URL of a service that provides realtime device screenshots in MJPEG format. + If provided then the actual command to retrieve a screenshot will be + requesting pictures from this service rather than directly from the server. + """ + self.set_capability(MJPEG_SCREENSHOT_URL, value) diff --git a/appium/options/android/common/other/__init__.py b/appium/options/android/common/other/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/android/common/other/disable_suppress_accessibility_service_option.py b/appium/options/android/common/other/disable_suppress_accessibility_service_option.py new file mode 100644 index 00000000..980da567 --- /dev/null +++ b/appium/options/android/common/other/disable_suppress_accessibility_service_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +DISABLE_SUPPRESS_ACCESSIBILITY_SERVICE = 'disableSuppressAccessibilityService' + + +class DisableSuppressAccessibilityServiceOption(SupportsCapabilities): + @property + def disable_suppress_accessibility_service(self) -> Optional[bool]: + """ + Whether to suppress accessibility services. + """ + return self.get_capability(DISABLE_SUPPRESS_ACCESSIBILITY_SERVICE) + + @disable_suppress_accessibility_service.setter + def disable_suppress_accessibility_service(self, value: bool) -> None: + """ + Being set to true tells the instrumentation process to not suppress + accessibility services during the automated test. This might be useful + if your automated test needs these services. false by default. + """ + self.set_capability(DISABLE_SUPPRESS_ACCESSIBILITY_SERVICE, value) diff --git a/appium/options/android/common/other/user_profile_option.py b/appium/options/android/common/other/user_profile_option.py new file mode 100644 index 00000000..76871396 --- /dev/null +++ b/appium/options/android/common/other/user_profile_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +USER_PROFILE = 'userProfile' + + +class UserProfileOption(SupportsCapabilities): + @property + def user_profile(self) -> Optional[int]: + """ + Integer identifier of a user profile. + """ + return self.get_capability(USER_PROFILE) + + @user_profile.setter + def user_profile(self, value: int) -> None: + """ + Integer identifier of a user profile. By default, the app under test is + installed for the currently active user, but in case it is necessary to + test how the app performs while being installed for a user profile, + which is different from the current one, this capability might + come in handy. + """ + self.set_capability(USER_PROFILE, value) diff --git a/appium/options/android/common/signing/__init__.py b/appium/options/android/common/signing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/android/common/signing/key_alias_option.py b/appium/options/android/common/signing/key_alias_option.py new file mode 100644 index 00000000..471b28ee --- /dev/null +++ b/appium/options/android/common/signing/key_alias_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +KEY_ALIAS = 'keyAlias' + + +class KeyAliasOption(SupportsCapabilities): + @property + def key_alias(self) -> Optional[str]: + """ + Keystore key alias. + """ + return self.get_capability(KEY_ALIAS) + + @key_alias.setter + def key_alias(self, value: str) -> None: + """ + The alias of the key in the keystore file provided in keystorePath capability. + This option is used in combination with useKeystore, keystorePath, + keystorePassword, keyAlias and keyPassword options. Unset by default + """ + self.set_capability(KEY_ALIAS, value) diff --git a/appium/options/android/common/signing/key_password_option.py b/appium/options/android/common/signing/key_password_option.py new file mode 100644 index 00000000..5ed91d1c --- /dev/null +++ b/appium/options/android/common/signing/key_password_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +KEY_PASSWORD = 'keyPassword' + + +class KeyPasswordOption(SupportsCapabilities): + @property + def key_password(self) -> Optional[str]: + """ + Keystore key password. + """ + return self.get_capability(KEY_PASSWORD) + + @key_password.setter + def key_password(self, value: str) -> None: + """ + The password of the key in the keystore file provided in keystorePath capability. + This option is used in combination with useKeystore, keystorePath, + keystorePassword, keyAlias and keyPassword options. Unset by default + """ + self.set_capability(KEY_PASSWORD, value) diff --git a/appium/options/android/common/signing/keystore_password_option.py b/appium/options/android/common/signing/keystore_password_option.py new file mode 100644 index 00000000..8b766711 --- /dev/null +++ b/appium/options/android/common/signing/keystore_password_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +KEYSTORE_PASSWORD = 'keystorePassword' + + +class KeystorePasswordOption(SupportsCapabilities): + @property + def keystore_password(self) -> Optional[str]: + """ + Keystore password. + """ + return self.get_capability(KEYSTORE_PASSWORD) + + @keystore_password.setter + def keystore_password(self, value: str) -> None: + """ + The password to the keystore file provided in keystorePath capability. + This option is used in combination with useKeystore, keystorePath, + keystorePassword, keyAlias and keyPassword options. Unset by default + """ + self.set_capability(KEYSTORE_PASSWORD, value) diff --git a/appium/options/android/common/signing/keystore_path_option.py b/appium/options/android/common/signing/keystore_path_option.py new file mode 100644 index 00000000..3489efe5 --- /dev/null +++ b/appium/options/android/common/signing/keystore_path_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +KEYSTORE_PATH = 'keystorePath' + + +class KeystorePathOption(SupportsCapabilities): + @property + def keystore_path(self) -> Optional[str]: + """ + The path to keystore. + """ + return self.get_capability(KEYSTORE_PATH) + + @keystore_path.setter + def keystore_path(self, value: str) -> None: + """ + The full path to the keystore file on the server filesystem. + This option is used in combination with useKeystore, keystorePath, + keystorePassword, keyAlias and keyPassword options. Unset by default + """ + self.set_capability(KEYSTORE_PATH, value) diff --git a/appium/options/android/common/signing/no_sign_option.py b/appium/options/android/common/signing/no_sign_option.py new file mode 100644 index 00000000..a5a52841 --- /dev/null +++ b/appium/options/android/common/signing/no_sign_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +NO_SIGN = 'noSign' + + +class NoSignOption(SupportsCapabilities): + @property + def no_sign(self) -> Optional[bool]: + """ + Whether to skip application signing. + """ + return self.get_capability(NO_SIGN) + + @no_sign.setter + def no_sign(self, value: bool) -> None: + """ + Set it to true in order to skip application signing. By default + all apps are always signed with the default Appium debug signature + if they don't have any. This capability cancels all the signing checks + and makes the driver to use the application package as is. This option + does not affect .apks packages as these are expected to be already signed. + """ + self.set_capability(NO_SIGN, value) diff --git a/appium/options/android/common/signing/use_keystore_option.py b/appium/options/android/common/signing/use_keystore_option.py new file mode 100644 index 00000000..f8a947a5 --- /dev/null +++ b/appium/options/android/common/signing/use_keystore_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +USE_KEYSTORE = 'useKeystore' + + +class UseKeystoreOption(SupportsCapabilities): + @property + def use_keystore(self) -> Optional[bool]: + """ + Whether to use custom keystore. + """ + return self.get_capability(USE_KEYSTORE) + + @use_keystore.setter + def use_keystore(self, value: bool) -> None: + """ + Whether to use a custom keystore to sign the app under test. + false by default, which means apps are always signed with the default A + ppium debug certificate (unless canceled by noSign capability). + This option is used in combination with keystorePath, keystorePassword, + keyAlias and keyPassword options. + """ + self.set_capability(USE_KEYSTORE, value) diff --git a/appium/options/android/espresso/__init__.py b/appium/options/android/espresso/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/android/espresso/activity_options_option.py b/appium/options/android/espresso/activity_options_option.py new file mode 100644 index 00000000..a15935fe --- /dev/null +++ b/appium/options/android/espresso/activity_options_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ACTIVITY_OPTIONS = 'activityOptions' + + +class ActivityOptionsOption(SupportsCapabilities): + @property + def activity_options(self) -> Optional[Dict]: + """ + Activity options. + """ + return self.get_capability(ACTIVITY_OPTIONS) + + @activity_options.setter + def activity_options(self, value: Dict) -> None: + """ + The mapping of custom options for the main app activity that is going to + be started. Check + https://github.com/appium/appium-espresso-driver#activity-options + for more details. + """ + self.set_capability(ACTIVITY_OPTIONS, value) diff --git a/appium/options/android/espresso/app_locale_option.py b/appium/options/android/espresso/app_locale_option.py new file mode 100644 index 00000000..01ae2754 --- /dev/null +++ b/appium/options/android/espresso/app_locale_option.py @@ -0,0 +1,44 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_LOCALE = 'appLocale' + + +class AppLocaleOption(SupportsCapabilities): + @property + def app_locale(self) -> Optional[Dict[str, str]]: + """ + Locale for the app under test. + """ + return self.get_capability(APP_LOCALE) + + @app_locale.setter + def app_locale(self, value: Dict[str, str]) -> None: + """ + Sets the locale for the app under test. The main difference between this option + and the above ones is that this option only changes the locale for the application + under test and does not affect other parts of the system. Also, it only uses + public APIs for its purpose. See + https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers to get the + list of available language abbreviations. + Example: {"language": "zh", "country": "CN", "variant": "Hans"}. + """ + self.set_capability(APP_LOCALE, value) diff --git a/appium/options/android/espresso/base.py b/appium/options/android/espresso/base.py new file mode 100644 index 00000000..c725b63a --- /dev/null +++ b/appium/options/android/espresso/base.py @@ -0,0 +1,221 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict + +from appium.options.android.common.adb.adb_exec_timeout_option import AdbExecTimeoutOption +from appium.options.android.common.adb.adb_port_option import AdbPortOption +from appium.options.android.common.adb.allow_delay_adb_option import AllowDelayAdbOption +from appium.options.android.common.adb.build_tools_version_option import BuildToolsVersionOption +from appium.options.android.common.adb.clear_device_logs_on_start_option import ClearDeviceLogsOnStartOption +from appium.options.android.common.adb.ignore_hidden_api_policy_error_option import IgnoreHiddenApiPolicyErrorOption +from appium.options.android.common.adb.logcat_filter_specs_option import LogcatFilterSpecsOption +from appium.options.android.common.adb.logcat_format_option import LogcatFormatOption +from appium.options.android.common.adb.mock_location_app_option import MockLocationAppOption +from appium.options.android.common.adb.remote_adb_host_option import RemoteAdbHostOption +from appium.options.android.common.adb.skip_logcat_capture_option import SkipLogcatCaptureOption +from appium.options.android.common.adb.suppress_kill_server_option import SuppressKillServerOption +from appium.options.android.common.app.allow_test_packages_option import AllowTestPackagesOption +from appium.options.android.common.app.android_install_timeout_option import AndroidInstallTimeoutOption +from appium.options.android.common.app.app_activity_option import AppActivityOption +from appium.options.android.common.app.app_package_option import AppPackageOption +from appium.options.android.common.app.app_wait_activity_option import AppWaitActivityOption +from appium.options.android.common.app.app_wait_duration_option import AppWaitDurationOption +from appium.options.android.common.app.app_wait_for_launch_option import AppWaitForLaunchOption +from appium.options.android.common.app.app_wait_package_option import AppWaitPackageOption +from appium.options.android.common.app.auto_grant_premissions_option import AutoGrantPermissionsOption +from appium.options.android.common.app.enforce_app_install_option import EnforceAppInstallOption +from appium.options.android.common.app.intent_action_option import IntentActionOption +from appium.options.android.common.app.intent_category_option import IntentCategoryOption +from appium.options.android.common.app.intent_flags_option import IntentFlagsOption +from appium.options.android.common.app.optional_intent_arguments_option import OptionalIntentArgumentsOption +from appium.options.android.common.app.remote_apps_cache_limit_option import RemoteAppsCacheLimitOption +from appium.options.android.common.app.uninstall_other_packages_option import UninstallOtherPackagesOption +from appium.options.android.common.avd.avd_args_option import AvdArgsOption +from appium.options.android.common.avd.avd_env_option import AvdEnvOption +from appium.options.android.common.avd.avd_launch_timeout_option import AvdLaunchTimeoutOption +from appium.options.android.common.avd.avd_option import AvdOption +from appium.options.android.common.avd.avd_ready_timeout_option import AvdReadyTimeoutOption +from appium.options.android.common.avd.gps_enabled_option import GpsEnabledOption +from appium.options.android.common.avd.network_speed_option import NetworkSpeedOption +from appium.options.android.common.context.auto_webview_timeout_option import AutoWebviewTimeoutOption +from appium.options.android.common.context.chrome_logging_prefs_option import ChromeLoggingPrefsOption +from appium.options.android.common.context.chrome_options_option import ChromeOptionsOption +from appium.options.android.common.context.chromedriver_args_option import ChromedriverArgsOption +from appium.options.android.common.context.chromedriver_chrome_mapping_file_option import ( + ChromedriverChromeMappingFileOption, +) +from appium.options.android.common.context.chromedriver_disable_build_check_option import ( + ChromedriverDisableBuildCheckOption, +) +from appium.options.android.common.context.chromedriver_executable_dir_option import ChromedriverExecutableDirOption +from appium.options.android.common.context.chromedriver_executable_option import ChromedriverExecutableOption +from appium.options.android.common.context.chromedriver_port_option import ChromedriverPortOption +from appium.options.android.common.context.chromedriver_ports_option import ChromedriverPortsOption +from appium.options.android.common.context.chromedriver_use_system_executable_option import ( + ChromedriverUseSystemExecutableOption, +) +from appium.options.android.common.context.ensure_webviews_have_pages_option import EnsureWebviewsHavePagesOption +from appium.options.android.common.context.extract_chrome_android_package_from_context_name_option import ( + ExtractChromeAndroidPackageFromContextNameOption, +) +from appium.options.android.common.context.native_web_screenshot_option import NativeWebScreenshotOption +from appium.options.android.common.context.recreate_chrome_driver_sessions_option import ( + RecreateChromeDriverSessionsOption, +) +from appium.options.android.common.context.show_chromedriver_log_option import ShowChromedriverLogOption +from appium.options.android.common.context.webview_devtools_port_option import WebviewDevtoolsPortOption +from appium.options.android.common.localization.locale_script_option import LocaleScriptOption +from appium.options.android.common.locking.skip_unlock_option import SkipUnlockOption +from appium.options.android.common.locking.unlock_key_option import UnlockKeyOption +from appium.options.android.common.locking.unlock_strategy_option import UnlockStrategyOption +from appium.options.android.common.locking.unlock_success_timeout_option import UnlockSuccessTimeoutOption +from appium.options.android.common.locking.unlock_type_option import UnlockTypeOption +from appium.options.android.common.mjpeg.mjpeg_screenshot_url_option import MjpegScreenshotUrlOption +from appium.options.android.common.other.disable_suppress_accessibility_service_option import ( + DisableSuppressAccessibilityServiceOption, +) +from appium.options.android.common.other.user_profile_option import UserProfileOption +from appium.options.android.common.signing.key_alias_option import KeyAliasOption +from appium.options.android.common.signing.key_password_option import KeyPasswordOption +from appium.options.android.common.signing.keystore_password_option import KeystorePasswordOption +from appium.options.android.common.signing.keystore_path_option import KeystorePathOption +from appium.options.android.common.signing.no_sign_option import NoSignOption +from appium.options.android.common.signing.use_keystore_option import UseKeystoreOption +from appium.options.common.app_option import AppOption +from appium.options.common.auto_web_view_option import AutoWebViewOption +from appium.options.common.automation_name_option import AUTOMATION_NAME +from appium.options.common.base import PLATFORM_NAME, AppiumOptions +from appium.options.common.clear_system_files_option import ClearSystemFilesOption +from appium.options.common.device_name_option import DeviceNameOption +from appium.options.common.enable_performance_logging_option import EnablePerformanceLoggingOption +from appium.options.common.is_headless_option import IsHeadlessOption +from appium.options.common.language_option import LanguageOption +from appium.options.common.locale_option import LocaleOption +from appium.options.common.orientation_option import OrientationOption +from appium.options.common.other_apps_option import OtherAppsOption +from appium.options.common.platform_version_option import PlatformVersionOption +from appium.options.common.skip_log_capture_option import SkipLogCaptureOption +from appium.options.common.system_port_option import SystemPortOption +from appium.options.common.udid_option import UdidOption + +from .activity_options_option import ActivityOptionsOption +from .app_locale_option import AppLocaleOption +from .espresso_build_config_option import EspressoBuildConfigOption +from .espresso_server_launch_timeout_option import EspressoServerLaunchTimeoutOption +from .force_espresso_rebuild_option import ForceEspressoRebuildOption +from .intent_options_option import IntentOptionsOption +from .show_gradle_log_option import ShowGradleLogOption + + +class EspressoOptions( + AppiumOptions, + AppOption, + OrientationOption, + UdidOption, + LanguageOption, + LocaleOption, + IsHeadlessOption, + SkipLogCaptureOption, + AutoWebViewOption, + DeviceNameOption, + ClearSystemFilesOption, + EnablePerformanceLoggingOption, + OtherAppsOption, + SystemPortOption, + AppPackageOption, + AppActivityOption, + AppWaitActivityOption, + AppWaitPackageOption, + AppWaitDurationOption, + AndroidInstallTimeoutOption, + AppWaitForLaunchOption, + IntentCategoryOption, + IntentActionOption, + IntentFlagsOption, + OptionalIntentArgumentsOption, + AutoGrantPermissionsOption, + UninstallOtherPackagesOption, + AllowTestPackagesOption, + RemoteAppsCacheLimitOption, + EnforceAppInstallOption, + LocaleScriptOption, + AdbPortOption, + PlatformVersionOption, + RemoteAdbHostOption, + AdbExecTimeoutOption, + ClearDeviceLogsOnStartOption, + BuildToolsVersionOption, + SkipLogcatCaptureOption, + SuppressKillServerOption, + IgnoreHiddenApiPolicyErrorOption, + MockLocationAppOption, + LogcatFormatOption, + LogcatFilterSpecsOption, + AllowDelayAdbOption, + AvdOption, + AvdLaunchTimeoutOption, + AvdReadyTimeoutOption, + AvdArgsOption, + AvdEnvOption, + NetworkSpeedOption, + GpsEnabledOption, + UseKeystoreOption, + KeystorePathOption, + KeystorePasswordOption, + KeyAliasOption, + KeyPasswordOption, + NoSignOption, + SkipUnlockOption, + UnlockKeyOption, + UnlockStrategyOption, + UnlockSuccessTimeoutOption, + UnlockTypeOption, + MjpegScreenshotUrlOption, + AutoWebviewTimeoutOption, + ChromeLoggingPrefsOption, + ChromeOptionsOption, + ChromedriverArgsOption, + ChromedriverChromeMappingFileOption, + ChromedriverDisableBuildCheckOption, + ChromedriverExecutableDirOption, + ChromedriverExecutableOption, + ChromedriverPortOption, + ChromedriverPortsOption, + ChromedriverUseSystemExecutableOption, + EnsureWebviewsHavePagesOption, + ExtractChromeAndroidPackageFromContextNameOption, + NativeWebScreenshotOption, + RecreateChromeDriverSessionsOption, + ShowChromedriverLogOption, + WebviewDevtoolsPortOption, + DisableSuppressAccessibilityServiceOption, + UserProfileOption, + EspressoBuildConfigOption, + EspressoServerLaunchTimeoutOption, + ForceEspressoRebuildOption, + ShowGradleLogOption, + IntentOptionsOption, + ActivityOptionsOption, + AppLocaleOption, +): + @property + def default_capabilities(self) -> Dict: + return { + AUTOMATION_NAME: 'Espresso', + PLATFORM_NAME: 'Android', + } diff --git a/appium/options/android/espresso/espresso_build_config_option.py b/appium/options/android/espresso/espresso_build_config_option.py new file mode 100644 index 00000000..7a8694f3 --- /dev/null +++ b/appium/options/android/espresso/espresso_build_config_option.py @@ -0,0 +1,46 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +import json +from typing import Any, Dict, Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ESPRESSO_BUILD_CONFIG = 'espressoBuildConfig' + + +class EspressoBuildConfigOption(SupportsCapabilities): + @property + def espresso_build_config(self) -> Optional[Union[Dict[str, Any], str]]: + """ + Espresso build config. + """ + value = self.get_capability(ESPRESSO_BUILD_CONFIG) + try: + return json.loads(value) + except Exception: + return value + + @espresso_build_config.setter + def espresso_build_config(self, value: Union[Dict[str, Any], str]) -> None: + """ + This config allows to customize several important properties of + Espresso server. Refer to + https://github.com/appium/appium-espresso-driver#espresso-build-config + for more information on how to properly construct such config. + """ + self.set_capability(ESPRESSO_BUILD_CONFIG, value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)) diff --git a/appium/options/android/espresso/espresso_server_launch_timeout_option.py b/appium/options/android/espresso/espresso_server_launch_timeout_option.py new file mode 100644 index 00000000..9335358d --- /dev/null +++ b/appium/options/android/espresso/espresso_server_launch_timeout_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ESPRESSO_SERVER_LAUNCH_TIMEOUT = 'espressoServerLaunchTimeout' + + +class EspressoServerLaunchTimeoutOption(SupportsCapabilities): + @property + def espresso_server_launch_timeout(self) -> Optional[timedelta]: + """ + Maximum timeout to wait until Espresso server is listening on the device. + """ + value = self.get_capability(ESPRESSO_SERVER_LAUNCH_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @espresso_server_launch_timeout.setter + def espresso_server_launch_timeout(self, value: Union[timedelta, int]) -> None: + """ + Set the maximum timeout to wait util Espresso is listening on the device. + 45000 ms by default + """ + self.set_capability( + ESPRESSO_SERVER_LAUNCH_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value + ) diff --git a/appium/options/android/espresso/force_espresso_rebuild_option.py b/appium/options/android/espresso/force_espresso_rebuild_option.py new file mode 100644 index 00000000..8971db79 --- /dev/null +++ b/appium/options/android/espresso/force_espresso_rebuild_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +FORCE_ESPRESSO_REBUILD = 'forceEspressoRebuild' + + +class ForceEspressoRebuildOption(SupportsCapabilities): + @property + def force_espresso_rebuild(self) -> Optional[bool]: + """ + Whether to force Espresso server rebuild on a new session startup. + """ + return self.get_capability(FORCE_ESPRESSO_REBUILD) + + @force_espresso_rebuild.setter + def force_espresso_rebuild(self, value: bool) -> None: + """ + Whether to always enforce Espresso server rebuild (true). + By default, Espresso caches the already built server apk and only rebuilds + it when it is necessary, because rebuilding process needs extra time. + false by default. + """ + self.set_capability(FORCE_ESPRESSO_REBUILD, value) diff --git a/appium/options/android/espresso/intent_options_option.py b/appium/options/android/espresso/intent_options_option.py new file mode 100644 index 00000000..59625bd9 --- /dev/null +++ b/appium/options/android/espresso/intent_options_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Any, Dict, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +INTENT_OPTIONS = 'intentOptions' + + +class IntentOptionsOption(SupportsCapabilities): + @property + def intent_options(self) -> Optional[Dict[str, Any]]: + """ + Intent options. + """ + return self.get_capability(INTENT_OPTIONS) + + @intent_options.setter + def intent_options(self, value: Dict[str, Any]) -> None: + """ + The mapping of custom options for the intent that is going to be passed + to the main app activity. Check + https://github.com/appium/appium-espresso-driver#intent-options + for more details. + """ + self.set_capability(INTENT_OPTIONS, value) diff --git a/appium/options/android/espresso/show_gradle_log_option.py b/appium/options/android/espresso/show_gradle_log_option.py new file mode 100644 index 00000000..48d233db --- /dev/null +++ b/appium/options/android/espresso/show_gradle_log_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SHOW_GRADLE_LOG = 'showGradleLog' + + +class ShowGradleLogOption(SupportsCapabilities): + @property + def show_gradle_log(self) -> Optional[bool]: + """ + Whether to include Gradle log to the regular server log. + """ + return self.get_capability(SHOW_GRADLE_LOG) + + @show_gradle_log.setter + def show_gradle_log(self, value: bool) -> None: + """ + Whether to include Gradle log to the regular server logs while + building Espresso server. false by default. + """ + self.set_capability(SHOW_GRADLE_LOG, value) diff --git a/appium/options/android/uiautomator2/__init__.py b/appium/options/android/uiautomator2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/android/uiautomator2/base.py b/appium/options/android/uiautomator2/base.py new file mode 100644 index 00000000..7efcb903 --- /dev/null +++ b/appium/options/android/uiautomator2/base.py @@ -0,0 +1,221 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict + +from appium.options.android.common.adb.adb_exec_timeout_option import AdbExecTimeoutOption +from appium.options.android.common.adb.adb_port_option import AdbPortOption +from appium.options.android.common.adb.allow_delay_adb_option import AllowDelayAdbOption +from appium.options.android.common.adb.build_tools_version_option import BuildToolsVersionOption +from appium.options.android.common.adb.clear_device_logs_on_start_option import ClearDeviceLogsOnStartOption +from appium.options.android.common.adb.ignore_hidden_api_policy_error_option import IgnoreHiddenApiPolicyErrorOption +from appium.options.android.common.adb.logcat_filter_specs_option import LogcatFilterSpecsOption +from appium.options.android.common.adb.logcat_format_option import LogcatFormatOption +from appium.options.android.common.adb.mock_location_app_option import MockLocationAppOption +from appium.options.android.common.adb.remote_adb_host_option import RemoteAdbHostOption +from appium.options.android.common.adb.skip_logcat_capture_option import SkipLogcatCaptureOption +from appium.options.android.common.adb.suppress_kill_server_option import SuppressKillServerOption +from appium.options.android.common.app.allow_test_packages_option import AllowTestPackagesOption +from appium.options.android.common.app.android_install_timeout_option import AndroidInstallTimeoutOption +from appium.options.android.common.app.app_activity_option import AppActivityOption +from appium.options.android.common.app.app_package_option import AppPackageOption +from appium.options.android.common.app.app_wait_activity_option import AppWaitActivityOption +from appium.options.android.common.app.app_wait_duration_option import AppWaitDurationOption +from appium.options.android.common.app.app_wait_for_launch_option import AppWaitForLaunchOption +from appium.options.android.common.app.app_wait_package_option import AppWaitPackageOption +from appium.options.android.common.app.auto_grant_premissions_option import AutoGrantPermissionsOption +from appium.options.android.common.app.enforce_app_install_option import EnforceAppInstallOption +from appium.options.android.common.app.intent_action_option import IntentActionOption +from appium.options.android.common.app.intent_category_option import IntentCategoryOption +from appium.options.android.common.app.intent_flags_option import IntentFlagsOption +from appium.options.android.common.app.optional_intent_arguments_option import OptionalIntentArgumentsOption +from appium.options.android.common.app.remote_apps_cache_limit_option import RemoteAppsCacheLimitOption +from appium.options.android.common.app.uninstall_other_packages_option import UninstallOtherPackagesOption +from appium.options.android.common.avd.avd_args_option import AvdArgsOption +from appium.options.android.common.avd.avd_env_option import AvdEnvOption +from appium.options.android.common.avd.avd_launch_timeout_option import AvdLaunchTimeoutOption +from appium.options.android.common.avd.avd_option import AvdOption +from appium.options.android.common.avd.avd_ready_timeout_option import AvdReadyTimeoutOption +from appium.options.android.common.avd.gps_enabled_option import GpsEnabledOption +from appium.options.android.common.avd.network_speed_option import NetworkSpeedOption +from appium.options.android.common.context.auto_webview_timeout_option import AutoWebviewTimeoutOption +from appium.options.android.common.context.chrome_logging_prefs_option import ChromeLoggingPrefsOption +from appium.options.android.common.context.chrome_options_option import ChromeOptionsOption +from appium.options.android.common.context.chromedriver_args_option import ChromedriverArgsOption +from appium.options.android.common.context.chromedriver_chrome_mapping_file_option import ( + ChromedriverChromeMappingFileOption, +) +from appium.options.android.common.context.chromedriver_disable_build_check_option import ( + ChromedriverDisableBuildCheckOption, +) +from appium.options.android.common.context.chromedriver_executable_dir_option import ChromedriverExecutableDirOption +from appium.options.android.common.context.chromedriver_executable_option import ChromedriverExecutableOption +from appium.options.android.common.context.chromedriver_port_option import ChromedriverPortOption +from appium.options.android.common.context.chromedriver_ports_option import ChromedriverPortsOption +from appium.options.android.common.context.chromedriver_use_system_executable_option import ( + ChromedriverUseSystemExecutableOption, +) +from appium.options.android.common.context.ensure_webviews_have_pages_option import EnsureWebviewsHavePagesOption +from appium.options.android.common.context.extract_chrome_android_package_from_context_name_option import ( + ExtractChromeAndroidPackageFromContextNameOption, +) +from appium.options.android.common.context.native_web_screenshot_option import NativeWebScreenshotOption +from appium.options.android.common.context.recreate_chrome_driver_sessions_option import ( + RecreateChromeDriverSessionsOption, +) +from appium.options.android.common.context.show_chromedriver_log_option import ShowChromedriverLogOption +from appium.options.android.common.context.webview_devtools_port_option import WebviewDevtoolsPortOption +from appium.options.android.common.localization.locale_script_option import LocaleScriptOption +from appium.options.android.common.locking.skip_unlock_option import SkipUnlockOption +from appium.options.android.common.locking.unlock_key_option import UnlockKeyOption +from appium.options.android.common.locking.unlock_strategy_option import UnlockStrategyOption +from appium.options.android.common.locking.unlock_success_timeout_option import UnlockSuccessTimeoutOption +from appium.options.android.common.locking.unlock_type_option import UnlockTypeOption +from appium.options.android.common.mjpeg.mjpeg_screenshot_url_option import MjpegScreenshotUrlOption +from appium.options.android.common.other.disable_suppress_accessibility_service_option import ( + DisableSuppressAccessibilityServiceOption, +) +from appium.options.android.common.other.user_profile_option import UserProfileOption +from appium.options.android.common.signing.key_alias_option import KeyAliasOption +from appium.options.android.common.signing.key_password_option import KeyPasswordOption +from appium.options.android.common.signing.keystore_password_option import KeystorePasswordOption +from appium.options.android.common.signing.keystore_path_option import KeystorePathOption +from appium.options.android.common.signing.no_sign_option import NoSignOption +from appium.options.android.common.signing.use_keystore_option import UseKeystoreOption +from appium.options.common.app_option import AppOption +from appium.options.common.auto_web_view_option import AutoWebViewOption +from appium.options.common.automation_name_option import AUTOMATION_NAME +from appium.options.common.base import PLATFORM_NAME, AppiumOptions +from appium.options.common.clear_system_files_option import ClearSystemFilesOption +from appium.options.common.device_name_option import DeviceNameOption +from appium.options.common.enable_performance_logging_option import EnablePerformanceLoggingOption +from appium.options.common.is_headless_option import IsHeadlessOption +from appium.options.common.language_option import LanguageOption +from appium.options.common.locale_option import LocaleOption +from appium.options.common.orientation_option import OrientationOption +from appium.options.common.other_apps_option import OtherAppsOption +from appium.options.common.platform_version_option import PlatformVersionOption +from appium.options.common.skip_log_capture_option import SkipLogCaptureOption +from appium.options.common.system_port_option import SystemPortOption +from appium.options.common.udid_option import UdidOption + +from .disable_window_animation_option import DisableWindowAnimationOption +from .mjpeg_server_port_option import MjpegServerPortOption +from .skip_device_initialization_option import SkipDeviceInitializationOption +from .skip_server_installation_option import SkipServerInstallationOption +from .uiautomator2_server_install_timeout_option import Uiautomator2ServerInstallTimeoutOption +from .uiautomator2_server_launch_timeout_option import Uiautomator2ServerLaunchTimeoutOption +from .uiautomator2_server_read_timeout_option import Uiautomator2ServerReadTimeoutOption + + +class UiAutomator2Options( + AppiumOptions, + AppOption, + ClearSystemFilesOption, + OrientationOption, + UdidOption, + LanguageOption, + LocaleOption, + IsHeadlessOption, + SkipLogCaptureOption, + AutoWebViewOption, + EnablePerformanceLoggingOption, + OtherAppsOption, + DeviceNameOption, + SystemPortOption, + SkipServerInstallationOption, + Uiautomator2ServerInstallTimeoutOption, + Uiautomator2ServerLaunchTimeoutOption, + Uiautomator2ServerReadTimeoutOption, + DisableWindowAnimationOption, + SkipDeviceInitializationOption, + AppPackageOption, + AppActivityOption, + PlatformVersionOption, + AppWaitActivityOption, + AppWaitPackageOption, + AppWaitDurationOption, + AndroidInstallTimeoutOption, + AppWaitForLaunchOption, + IntentCategoryOption, + IntentActionOption, + IntentFlagsOption, + OptionalIntentArgumentsOption, + AutoGrantPermissionsOption, + UninstallOtherPackagesOption, + AllowTestPackagesOption, + RemoteAppsCacheLimitOption, + EnforceAppInstallOption, + LocaleScriptOption, + AdbPortOption, + RemoteAdbHostOption, + AdbExecTimeoutOption, + ClearDeviceLogsOnStartOption, + SkipLogcatCaptureOption, + BuildToolsVersionOption, + SuppressKillServerOption, + IgnoreHiddenApiPolicyErrorOption, + MockLocationAppOption, + LogcatFormatOption, + LogcatFilterSpecsOption, + AllowDelayAdbOption, + AvdOption, + AvdLaunchTimeoutOption, + AvdReadyTimeoutOption, + AvdArgsOption, + AvdEnvOption, + NetworkSpeedOption, + GpsEnabledOption, + UseKeystoreOption, + KeystorePathOption, + KeystorePasswordOption, + KeyAliasOption, + KeyPasswordOption, + NoSignOption, + SkipUnlockOption, + UnlockKeyOption, + UnlockStrategyOption, + UnlockSuccessTimeoutOption, + UnlockTypeOption, + MjpegServerPortOption, + MjpegScreenshotUrlOption, + AutoWebviewTimeoutOption, + ChromeLoggingPrefsOption, + ChromeOptionsOption, + ChromedriverArgsOption, + ChromedriverChromeMappingFileOption, + ChromedriverDisableBuildCheckOption, + ChromedriverExecutableDirOption, + ChromedriverExecutableOption, + ChromedriverPortOption, + ChromedriverPortsOption, + ChromedriverUseSystemExecutableOption, + EnsureWebviewsHavePagesOption, + ExtractChromeAndroidPackageFromContextNameOption, + NativeWebScreenshotOption, + RecreateChromeDriverSessionsOption, + ShowChromedriverLogOption, + WebviewDevtoolsPortOption, + DisableSuppressAccessibilityServiceOption, + UserProfileOption, +): + @property + def default_capabilities(self) -> Dict: + return { + AUTOMATION_NAME: 'UIAutomator2', + PLATFORM_NAME: 'Android', + } diff --git a/appium/options/android/uiautomator2/disable_window_animation_option.py b/appium/options/android/uiautomator2/disable_window_animation_option.py new file mode 100644 index 00000000..5ff614b4 --- /dev/null +++ b/appium/options/android/uiautomator2/disable_window_animation_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +DISABLE_WINDOWS_ANIMATION = 'disableWindowAnimation' + + +class DisableWindowAnimationOption(SupportsCapabilities): + @property + def disable_window_animation(self) -> Optional[bool]: + """ + Whether window animations when starting the instrumentation process + are disabled. + """ + return self.get_capability(DISABLE_WINDOWS_ANIMATION) + + @disable_window_animation.setter + def disable_window_animation(self, value: bool) -> None: + """ + Set whether to disable window animations when starting the instrumentation process. + false by default + """ + self.set_capability(DISABLE_WINDOWS_ANIMATION, value) diff --git a/appium/options/android/uiautomator2/mjpeg_server_port_option.py b/appium/options/android/uiautomator2/mjpeg_server_port_option.py new file mode 100644 index 00000000..6c702646 --- /dev/null +++ b/appium/options/android/uiautomator2/mjpeg_server_port_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +MJPEG_SERVER_PORT = 'mjpegServerPort' + + +class MjpegServerPortOption(SupportsCapabilities): + @property + def mjpeg_server_port(self) -> Optional[int]: + """ + Number of the port UiAutomator2 server starts the MJPEG server on. + """ + return self.get_capability(MJPEG_SERVER_PORT) + + @mjpeg_server_port.setter + def mjpeg_server_port(self, value: int) -> None: + """ + The number of the port UiAutomator2 server starts the MJPEG server on. + If not provided then the screenshots broadcasting service on the remote + device does not get exposed to a local port (e.g. no adb port forwarding + is happening). + """ + self.set_capability(MJPEG_SERVER_PORT, value) diff --git a/appium/options/android/uiautomator2/skip_device_initialization_option.py b/appium/options/android/uiautomator2/skip_device_initialization_option.py new file mode 100644 index 00000000..eda37be3 --- /dev/null +++ b/appium/options/android/uiautomator2/skip_device_initialization_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SKIP_DEVICE_INITIALIZATION = 'skipDeviceInitialization' + + +class SkipDeviceInitializationOption(SupportsCapabilities): + @property + def skip_device_initialization(self) -> Optional[bool]: + """ + Whether initial device startup checks by the server are disabled. + """ + return self.get_capability(SKIP_DEVICE_INITIALIZATION) + + @skip_device_initialization.setter + def skip_device_initialization(self, value: bool) -> None: + """ + If set to true then device startup checks (whether it is ready and whether + Settings app is installed) will be canceled on session creation. + Could speed up the session creation if you know what you are doing. false by default + """ + self.set_capability(SKIP_DEVICE_INITIALIZATION, value) diff --git a/appium/options/android/uiautomator2/skip_server_installation_option.py b/appium/options/android/uiautomator2/skip_server_installation_option.py new file mode 100644 index 00000000..c343f78f --- /dev/null +++ b/appium/options/android/uiautomator2/skip_server_installation_option.py @@ -0,0 +1,44 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SKIP_SERVER_INSTALLATION = 'skipServerInstallation' + + +class SkipServerInstallationOption(SupportsCapabilities): + @property + def skip_server_installation(self) -> Optional[bool]: + """ + Whether to skip the server components installation + on the device under test and all the related checks. + """ + return self.get_capability(SKIP_SERVER_INSTALLATION) + + @skip_server_installation.setter + def skip_server_installation(self, value: bool) -> None: + """ + Set whether to skip the server components installation + on the device under test and all the related checks. + This could help to speed up the session startup if you know for sure the + correct server version is installed on the device. + In case the server is not installed or an incorrect version of it is installed + then you may get an unexpected error later. + """ + self.set_capability(SKIP_SERVER_INSTALLATION, value) diff --git a/appium/options/android/uiautomator2/uiautomator2_server_install_timeout_option.py b/appium/options/android/uiautomator2/uiautomator2_server_install_timeout_option.py new file mode 100644 index 00000000..4b25638f --- /dev/null +++ b/appium/options/android/uiautomator2/uiautomator2_server_install_timeout_option.py @@ -0,0 +1,44 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +UIAUTOMATOR2_SERVER_INSTALL_TIMEOUT = 'uiautomator2ServerInstallTimeout' + + +class Uiautomator2ServerInstallTimeoutOption(SupportsCapabilities): + @property + def uiautomator2_server_install_timeout(self) -> Optional[timedelta]: + """ + Maximum timeout to wait until UiAutomator2 server is installed on the device. + """ + value = self.get_capability(UIAUTOMATOR2_SERVER_INSTALL_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @uiautomator2_server_install_timeout.setter + def uiautomator2_server_install_timeout(self, value: Union[timedelta, int]) -> None: + """ + Set the maximum timeout to wait util UiAutomator2 server is installed on the device. + 20000 ms by default + """ + self.set_capability( + UIAUTOMATOR2_SERVER_INSTALL_TIMEOUT, + int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value, + ) diff --git a/appium/options/android/uiautomator2/uiautomator2_server_launch_timeout_option.py b/appium/options/android/uiautomator2/uiautomator2_server_launch_timeout_option.py new file mode 100644 index 00000000..ce74c447 --- /dev/null +++ b/appium/options/android/uiautomator2/uiautomator2_server_launch_timeout_option.py @@ -0,0 +1,44 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +UIAUTOMATOR2_SERVER_LAUNCH_TIMEOUT = 'uiautomator2ServerLaunchTimeout' + + +class Uiautomator2ServerLaunchTimeoutOption(SupportsCapabilities): + @property + def uiautomator2_server_launch_timeout(self) -> Optional[timedelta]: + """ + Maximum timeout to wait until UiAutomator2 server is listening on the device. + """ + value = self.get_capability(UIAUTOMATOR2_SERVER_LAUNCH_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @uiautomator2_server_launch_timeout.setter + def uiautomator2_server_launch_timeout(self, value: Union[timedelta, int]) -> None: + """ + Set the maximum timeout to wait util UiAutomator2Server is listening on + the device. 30000 ms by default + """ + self.set_capability( + UIAUTOMATOR2_SERVER_LAUNCH_TIMEOUT, + int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value, + ) diff --git a/appium/options/android/uiautomator2/uiautomator2_server_read_timeout_option.py b/appium/options/android/uiautomator2/uiautomator2_server_read_timeout_option.py new file mode 100644 index 00000000..cb8a39c2 --- /dev/null +++ b/appium/options/android/uiautomator2/uiautomator2_server_read_timeout_option.py @@ -0,0 +1,46 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +UIAUTOMATOR2_SERVER_READ_TIMEOUT = 'uiautomator2ServerReadTimeout' + + +class Uiautomator2ServerReadTimeoutOption(SupportsCapabilities): + @property + def uiautomator2_server_read_timeout(self) -> Optional[timedelta]: + """ + Maximum timeout to wait for an HTTP response from UiAutomator2Server. + """ + value = self.get_capability(UIAUTOMATOR2_SERVER_READ_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @uiautomator2_server_read_timeout.setter + def uiautomator2_server_read_timeout(self, value: Union[timedelta, int]) -> None: + """ + Set the maximum timeout to wait for a HTTP response from UiAutomator2Server. + Only values greater than zero are accepted. If the given value is too low + then expect driver commands to fail with timeout of Xms exceeded error. + 240000 ms by default + """ + self.set_capability( + UIAUTOMATOR2_SERVER_READ_TIMEOUT, + int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value, + ) diff --git a/appium/options/common/__init__.py b/appium/options/common/__init__.py new file mode 100644 index 00000000..a7a37ea6 --- /dev/null +++ b/appium/options/common/__init__.py @@ -0,0 +1 @@ +from .base import AppiumOptions diff --git a/appium/options/common/app_option.py b/appium/options/common/app_option.py new file mode 100644 index 00000000..4a8fff1b --- /dev/null +++ b/appium/options/common/app_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +APP = 'app' + + +class AppOption(SupportsCapabilities): + @property + def app(self) -> Optional[str]: + """ + String representing app location. + """ + return self.get_capability(APP) + + @app.setter + def app(self, value: str) -> None: + """ + Set the absolute local path for the location of the App. + The app must be located on the same machine where Appium + server is running. + Could also be a valid URL. + """ + self.set_capability(APP, value) diff --git a/appium/options/common/auto_web_view_option.py b/appium/options/common/auto_web_view_option.py new file mode 100644 index 00000000..6983aef4 --- /dev/null +++ b/appium/options/common/auto_web_view_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +AUTO_WEB_VIEW = 'autoWebView' + + +class AutoWebViewOption(SupportsCapabilities): + @property + def auto_web_view(self) -> Optional[bool]: + """ + Whether the driver should try to automatically switch + to a web view context after the session is started. + """ + return self.get_capability(AUTO_WEB_VIEW) + + @auto_web_view.setter + def auto_web_view(self, value: bool) -> None: + """ + Set whether the driver should try to automatically switch + a web view context after the session is started. + """ + self.set_capability(AUTO_WEB_VIEW, value) diff --git a/appium/options/common/automation_name_option.py b/appium/options/common/automation_name_option.py new file mode 100644 index 00000000..c18e0a15 --- /dev/null +++ b/appium/options/common/automation_name_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +AUTOMATION_NAME = 'automationName' + + +class AutomationNameOption(SupportsCapabilities): + @property + def automation_name(self) -> Optional[str]: + """ + String representing the name of the automation engine name. + """ + return self.get_capability(AUTOMATION_NAME) + + @automation_name.setter + def automation_name(self, value: str) -> None: + """ + Set the automation driver name to use for the given platform. + """ + self.set_capability(AUTOMATION_NAME, value) diff --git a/appium/options/common/base.py b/appium/options/common/base.py new file mode 100644 index 00000000..5ac7b2e3 --- /dev/null +++ b/appium/options/common/base.py @@ -0,0 +1,128 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +import copy +from typing import Any, Dict, TypeVar + +from selenium.webdriver.common.options import BaseOptions + +from .automation_name_option import AutomationNameOption +from .browser_name_option import BROWSER_NAME, BrowserNameOption +from .event_timings_option import EventTimingsOption +from .full_reset_option import FullResetOption +from .new_command_timeout_option import NewCommandTimeoutOption +from .no_reset_option import NoResetOption +from .print_page_source_on_find_failure_option import PrintPageSourceOnFindFailureOption + +APPIUM_PREFIX = 'appium:' +T = TypeVar('T', bound='AppiumOptions') +PLATFORM_NAME = 'platformName' + + +class AppiumOptions( + BaseOptions, + BrowserNameOption, + AutomationNameOption, + EventTimingsOption, + PrintPageSourceOnFindFailureOption, + NoResetOption, + FullResetOption, + NewCommandTimeoutOption, +): + _caps: Dict + W3C_CAPABILITY_NAMES = frozenset( + [ + 'acceptInsecureCerts', + BROWSER_NAME, + 'browserVersion', + PLATFORM_NAME, + 'pageLoadStrategy', + 'proxy', + 'setWindowRect', + 'timeouts', + 'unhandledPromptBehavior', + 'strictFileInteractability', # WebDriver spec v2 https://www.w3.org/TR/webdriver2/ + 'userAgent', # WebDriver spec v2 https://www.w3.org/TR/webdriver2/ + 'webSocketUrl', # WebDriver BiDi + ] + ) + _OSS_W3C_CONVERSION = { + 'acceptSslCerts': 'acceptInsecureCerts', + 'version': 'browserVersion', + 'platform': PLATFORM_NAME, + } + + # noinspection PyMissingConstructor + def __init__(self) -> None: + self._caps = self.default_capabilities + # FIXME: https://github.com/SeleniumHQ/selenium/issues/10755 + self._ignore_local_proxy = False + + def set_capability(self: T, name: str, value: Any) -> T: # type: ignore[override] + w3c_name = name if name in self.W3C_CAPABILITY_NAMES or ':' in name else f'{APPIUM_PREFIX}{name}' + if value is None: + if w3c_name in self._caps: + del self._caps[w3c_name] + else: + self._caps[w3c_name] = value + return self + + def get_capability(self, name: str) -> Any: + """Fetches capability value or None if the capability is not set""" + return self._caps[name] if name in self._caps else self._caps.get(f'{APPIUM_PREFIX}{name}') + + def load_capabilities(self: T, caps: Dict[str, Any]) -> T: + """Sets multiple capabilities""" + for name, value in caps.items(): + self.set_capability(name, value) + return self + + @staticmethod + def as_w3c(capabilities: Dict) -> Dict: + """ + Formats given capabilities to a valid W3C session request object + + Args: + capabilities: Capabilities mapping + + Returns: + W3C session request object + """ + + def process_key(k: str) -> str: + key = AppiumOptions._OSS_W3C_CONVERSION.get(k, k) + if key in AppiumOptions.W3C_CAPABILITY_NAMES: + return key + return key if ':' in key else f'{APPIUM_PREFIX}{key}' + + processed_caps = {process_key(k): v for k, v in copy.deepcopy(capabilities).items()} + return {'capabilities': {'firstMatch': [{}], 'alwaysMatch': processed_caps}} + + def to_w3c(self) -> Dict: + """ + Formats the instance to a valid W3C session request object + + :return: W3C session request object + """ + return self.as_w3c(self.to_capabilities()) + + def to_capabilities(self) -> Dict: + return copy.copy(self._caps) + + @property + def default_capabilities(self) -> Dict: + return {} diff --git a/appium/options/common/browser_name_option.py b/appium/options/common/browser_name_option.py new file mode 100644 index 00000000..2b107972 --- /dev/null +++ b/appium/options/common/browser_name_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +BROWSER_NAME = 'browserName' + + +class BrowserNameOption(SupportsCapabilities): + @property + def browser_name(self) -> Optional[str]: + """ + The name of the browser to run the test on. + """ + return self.get_capability(BROWSER_NAME) + + @browser_name.setter + def browser_name(self, value: str) -> None: + """ + Set the name of the browser to run the test on. + """ + self.set_capability(BROWSER_NAME, value) diff --git a/appium/options/common/bundle_id_option.py b/appium/options/common/bundle_id_option.py new file mode 100644 index 00000000..0546571a --- /dev/null +++ b/appium/options/common/bundle_id_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +BUNDLE_ID = 'bundleId' + + +class BundleIdOption(SupportsCapabilities): + @property + def bundle_id(self) -> Optional[str]: + """ + The bundle identifier of the application to automate. + """ + return self.get_capability(BUNDLE_ID) + + @bundle_id.setter + def bundle_id(self, value: str) -> None: + """ + Set the bundle identifier of the application to automate. + """ + self.set_capability(BUNDLE_ID, value) diff --git a/appium/options/common/clear_system_files_option.py b/appium/options/common/clear_system_files_option.py new file mode 100644 index 00000000..208c4bb9 --- /dev/null +++ b/appium/options/common/clear_system_files_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +CLEAR_SYSTEM_FILES = 'clearSystemFiles' + + +class ClearSystemFilesOption(SupportsCapabilities): + @property + def clear_system_files(self) -> Optional[bool]: + """ + Whether the driver should delete generated files at the end of a session. + """ + return self.get_capability(CLEAR_SYSTEM_FILES) + + @clear_system_files.setter + def clear_system_files(self, value: bool) -> None: + """ + Set whether the driver should delete generated files at the end of a session. + """ + self.set_capability(CLEAR_SYSTEM_FILES, value) diff --git a/appium/options/common/device_name_option.py b/appium/options/common/device_name_option.py new file mode 100644 index 00000000..8e33cce3 --- /dev/null +++ b/appium/options/common/device_name_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +DEVICE_NAME = 'deviceName' + + +class DeviceNameOption(SupportsCapabilities): + @property + def device_name(self) -> Optional[str]: + """ + The name of the device. + """ + return self.get_capability(DEVICE_NAME) + + @device_name.setter + def device_name(self, value: str) -> None: + """ + Set the name of the device to be used in the test. + """ + self.set_capability(DEVICE_NAME, value) diff --git a/appium/options/common/enable_performance_logging_option.py b/appium/options/common/enable_performance_logging_option.py new file mode 100644 index 00000000..22a4bdb3 --- /dev/null +++ b/appium/options/common/enable_performance_logging_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +ENABLE_PERFORMANCE_LOGGING = 'enablePerformanceLogging' + + +class EnablePerformanceLoggingOption(SupportsCapabilities): + @property + def enable_performance_logging(self) -> Optional[bool]: + """ + Whether to enable additional performance logging. + """ + return self.get_capability(ENABLE_PERFORMANCE_LOGGING) + + @enable_performance_logging.setter + def enable_performance_logging(self, value: bool) -> None: + """ + Set whether to enable additional performance logging. + """ + self.set_capability(ENABLE_PERFORMANCE_LOGGING, value) diff --git a/appium/options/common/event_timings_option.py b/appium/options/common/event_timings_option.py new file mode 100644 index 00000000..b9c0a124 --- /dev/null +++ b/appium/options/common/event_timings_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +EVENT_TIMINGS = 'eventTimings' + + +class EventTimingsOption(SupportsCapabilities): + @property + def event_timings(self) -> Optional[bool]: + """ + Whether the driver should to report the timings + for various Appium-internal events. + """ + return self.get_capability(EVENT_TIMINGS) + + @event_timings.setter + def event_timings(self, value: bool) -> None: + """ + Set whether the driver should to report the timings + for various Appium-internal events. + """ + self.set_capability(EVENT_TIMINGS, value) diff --git a/appium/options/common/full_reset_option.py b/appium/options/common/full_reset_option.py new file mode 100644 index 00000000..aee86d7d --- /dev/null +++ b/appium/options/common/full_reset_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +FULL_RESET = 'fullReset' + + +class FullResetOption(SupportsCapabilities): + @property + def full_reset(self) -> Optional[bool]: + """ + Whether the driver should perform a full reset. + """ + return self.get_capability(FULL_RESET) + + @full_reset.setter + def full_reset(self, value: bool) -> None: + """ + Set whether the driver should perform a full reset. + """ + self.set_capability(FULL_RESET, value) diff --git a/appium/options/common/is_headless_option.py b/appium/options/common/is_headless_option.py new file mode 100644 index 00000000..bc0c2b11 --- /dev/null +++ b/appium/options/common/is_headless_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +IS_HEADLESS = 'isHeadless' + + +class IsHeadlessOption(SupportsCapabilities): + @property + def is_headless(self) -> Optional[bool]: + """ + Whether the driver should start emulator/simulator in headless mode. + """ + return self.get_capability(IS_HEADLESS) + + @is_headless.setter + def is_headless(self, value: bool) -> None: + """ + Set emulator/simulator to start in headless mode (e.g. no UI is shown). + It is only applied if the emulator is not running before the test starts. + """ + self.set_capability(IS_HEADLESS, value) diff --git a/appium/options/common/language_option.py b/appium/options/common/language_option.py new file mode 100644 index 00000000..f82de63d --- /dev/null +++ b/appium/options/common/language_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +LANGUAGE = 'language' + + +class LanguageOption(SupportsCapabilities): + @property + def language(self) -> Optional[str]: + """ + Language abbreviation to use in a test session. + """ + return self.get_capability(LANGUAGE) + + @language.setter + def language(self, value: str) -> None: + """ + Set language abbreviation to use in a test session. + """ + self.set_capability(LANGUAGE, value) diff --git a/appium/options/common/locale_option.py b/appium/options/common/locale_option.py new file mode 100644 index 00000000..58503a89 --- /dev/null +++ b/appium/options/common/locale_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +LOCALE = 'locale' + + +class LocaleOption(SupportsCapabilities): + @property + def locale(self) -> Optional[str]: + """ + Locale abbreviation to use in a test session. + """ + return self.get_capability(LOCALE) + + @locale.setter + def locale(self, value: str) -> None: + """ + Set locale abbreviation to use in a test session. + """ + self.set_capability(LOCALE, value) diff --git a/appium/options/common/new_command_timeout_option.py b/appium/options/common/new_command_timeout_option.py new file mode 100644 index 00000000..377ef719 --- /dev/null +++ b/appium/options/common/new_command_timeout_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from .supports_capabilities import SupportsCapabilities + +NEW_COMMAND_TIMEOUT = 'newCommandTimeout' + + +class NewCommandTimeoutOption(SupportsCapabilities): + @property + def new_command_timeout(self) -> Optional[timedelta]: + """ + The allowed time before seeing a new server command. + """ + value = self.get_capability(NEW_COMMAND_TIMEOUT) + return None if value is None else timedelta(seconds=value) + + @new_command_timeout.setter + def new_command_timeout(self, value: Union[timedelta, int]) -> None: + """ + Set the allowed time before seeing a new server command. + The value could either be provided as timedelta instance or an integer number of seconds. + """ + self.set_capability(NEW_COMMAND_TIMEOUT, value.total_seconds() if isinstance(value, timedelta) else value) diff --git a/appium/options/common/no_reset_option.py b/appium/options/common/no_reset_option.py new file mode 100644 index 00000000..ee2dd998 --- /dev/null +++ b/appium/options/common/no_reset_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +NO_RESET = 'noReset' + + +class NoResetOption(SupportsCapabilities): + @property + def no_reset(self) -> Optional[bool]: + """ + Whether the driver should not perform a reset. + """ + return self.get_capability(NO_RESET) + + @no_reset.setter + def no_reset(self, value: bool) -> None: + """ + Set whether the driver should not perform a reset. + """ + self.set_capability(NO_RESET, value) diff --git a/appium/options/common/orientation_option.py b/appium/options/common/orientation_option.py new file mode 100644 index 00000000..4141a4d9 --- /dev/null +++ b/appium/options/common/orientation_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +ORIENTATION = 'orientation' + + +class OrientationOption(SupportsCapabilities): + @property + def orientation(self) -> Optional[str]: + """ + The orientation of the device's screen. + Usually this is either 'PORTRAIT' or 'LANDSCAPE'. + """ + return self.get_capability(ORIENTATION) + + @orientation.setter + def orientation(self, value: str) -> None: + """ + Set the orientation of the device's screen. + Usually this is either 'PORTRAIT' or 'LANDSCAPE'. + """ + self.set_capability(ORIENTATION, value) diff --git a/appium/options/common/other_apps_option.py b/appium/options/common/other_apps_option.py new file mode 100644 index 00000000..6efbbbc4 --- /dev/null +++ b/appium/options/common/other_apps_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +OTHER_APPS = 'otherApps' + + +class OtherAppsOption(SupportsCapabilities): + @property + def other_apps(self) -> Optional[str]: + """ + Locations of apps to install before running a test. + """ + return self.get_capability(OTHER_APPS) + + @other_apps.setter + def other_apps(self, value: str) -> None: + """ + Set locations of apps to install before running a test. + Each item could be separated with a single comma. + """ + self.set_capability(OTHER_APPS, value) diff --git a/appium/options/common/platform_version_option.py b/appium/options/common/platform_version_option.py new file mode 100644 index 00000000..d237cc28 --- /dev/null +++ b/appium/options/common/platform_version_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +PLATFORM_VERSION = 'platformVersion' + + +class PlatformVersionOption(SupportsCapabilities): + @property + def platform_version(self) -> Optional[str]: + """ + The platform version of an emulator or a real device. + This capability is used for device autodetection if udid is not provided. + """ + return self.get_capability(PLATFORM_VERSION) + + @platform_version.setter + def platform_version(self, value: str) -> None: + """ + Set the platform version of an emulator or a real device. + This capability is used for device autodetection if udid is not provided. + """ + self.set_capability(PLATFORM_VERSION, value) diff --git a/appium/options/common/postrun_option.py b/appium/options/common/postrun_option.py new file mode 100644 index 00000000..f940cd6b --- /dev/null +++ b/appium/options/common/postrun_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict, Optional + +from .supports_capabilities import SupportsCapabilities + +POSTRUN = 'postrun' + + +class PostrunOption(SupportsCapabilities): + @property + def postrun(self) -> Optional[Dict[str, str]]: + """ + System script which is supposed to be executed upon + driver session quit. + """ + return self.get_capability(POSTRUN) + + @postrun.setter + def postrun(self, value: Dict[str, str]) -> None: + """ + Set a system script to execute upon driver session quit. + """ + self.set_capability(POSTRUN, value) diff --git a/appium/options/common/prerun_option.py b/appium/options/common/prerun_option.py new file mode 100644 index 00000000..139874c7 --- /dev/null +++ b/appium/options/common/prerun_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict, Optional + +from .supports_capabilities import SupportsCapabilities + +PRERUN = 'prerun' + + +class PrerunOption(SupportsCapabilities): + @property + def prerun(self) -> Optional[Dict[str, str]]: + """ + System script which is supposed to be executed before + a driver session is initialised. + """ + return self.get_capability(PRERUN) + + @prerun.setter + def prerun(self, value: Dict[str, str]) -> None: + """ + Set a system script which is supposed to be executed before + a driver session is initialised. + """ + self.set_capability(PRERUN, value) diff --git a/appium/options/common/print_page_source_on_find_failure_option.py b/appium/options/common/print_page_source_on_find_failure_option.py new file mode 100644 index 00000000..b93f07cf --- /dev/null +++ b/appium/options/common/print_page_source_on_find_failure_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +PRINT_PAGE_SOURCE_ON_FIND_FAILURE = 'printPageSourceOnFindFailure' + + +class PrintPageSourceOnFindFailureOption(SupportsCapabilities): + @property + def print_page_source_on_find_failure(self) -> Optional[bool]: + """ + Whether the driver should print the page source to the log + if a find failure occurs. + """ + return self.get_capability(PRINT_PAGE_SOURCE_ON_FIND_FAILURE) + + @print_page_source_on_find_failure.setter + def print_page_source_on_find_failure(self, value: bool) -> None: + """ + Set whether the driver should print the page source to the log + if a find failure occurs. + """ + self.set_capability(PRINT_PAGE_SOURCE_ON_FIND_FAILURE, value) diff --git a/appium/options/common/skip_log_capture_option.py b/appium/options/common/skip_log_capture_option.py new file mode 100644 index 00000000..668704e1 --- /dev/null +++ b/appium/options/common/skip_log_capture_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +SKIP_LOG_CAPTURE = 'skipLogCapture' + + +class SkipLogCaptureOption(SupportsCapabilities): + @property + def skip_log_capture(self) -> Optional[bool]: + """ + Whether the driver should not record device logs. + """ + return self.get_capability(SKIP_LOG_CAPTURE) + + @skip_log_capture.setter + def skip_log_capture(self, value: bool) -> None: + """ + Set whether the driver should not record device logs. + """ + self.set_capability(SKIP_LOG_CAPTURE, value) diff --git a/appium/options/common/supports_capabilities.py b/appium/options/common/supports_capabilities.py new file mode 100644 index 00000000..a8e7fb29 --- /dev/null +++ b/appium/options/common/supports_capabilities.py @@ -0,0 +1,26 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Any, Protocol, TypeVar + +T = TypeVar('T') + + +class SupportsCapabilities(Protocol): + def set_capability(self: T, name: str, value: Any) -> T: ... + + def get_capability(self: T, name: str) -> Any: ... diff --git a/appium/options/common/system_host_option.py b/appium/options/common/system_host_option.py new file mode 100644 index 00000000..0544de5c --- /dev/null +++ b/appium/options/common/system_host_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SYSTEM_HOST = 'systemHost' + + +class SystemHostOption(SupportsCapabilities): + @property + def system_host(self) -> Optional[str]: + """ + The name of the host for the internal server to listen on. + """ + return self.get_capability(SYSTEM_HOST) + + @system_host.setter + def system_host(self, value: str) -> None: + """ + Set the name of the host for the internal server to listen on. + """ + self.set_capability(SYSTEM_HOST, value) diff --git a/appium/options/common/system_port_option.py b/appium/options/common/system_port_option.py new file mode 100644 index 00000000..e11f3b59 --- /dev/null +++ b/appium/options/common/system_port_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SYSTEM_PORT = 'systemPort' + + +class SystemPortOption(SupportsCapabilities): + @property + def system_port(self) -> Optional[int]: + """ + The number of the port for the internal server to listen on. + """ + return self.get_capability(SYSTEM_PORT) + + @system_port.setter + def system_port(self, value: int) -> None: + """ + Set the number of the port for the internal server to listen on. + """ + self.set_capability(SYSTEM_PORT, value) diff --git a/appium/options/common/udid_option.py b/appium/options/common/udid_option.py new file mode 100644 index 00000000..f760a1d2 --- /dev/null +++ b/appium/options/common/udid_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from .supports_capabilities import SupportsCapabilities + +UDID = 'udid' + + +class UdidOption(SupportsCapabilities): + @property + def udid(self) -> Optional[str]: + """ + The unique identifier of the device under test. + """ + return self.get_capability(UDID) + + @udid.setter + def udid(self, value: str) -> None: + """ + Set the unique identifier of the device under test. + """ + self.set_capability(UDID, value) diff --git a/appium/options/flutter_integration/__init__.py b/appium/options/flutter_integration/__init__.py new file mode 100644 index 00000000..ba044a8a --- /dev/null +++ b/appium/options/flutter_integration/__init__.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +# 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. + +from .base import FlutterOptions diff --git a/appium/options/flutter_integration/base.py b/appium/options/flutter_integration/base.py new file mode 100644 index 00000000..f67b9a93 --- /dev/null +++ b/appium/options/flutter_integration/base.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict + +from appium.options.common.automation_name_option import AUTOMATION_NAME +from appium.options.common.base import AppiumOptions +from appium.options.flutter_integration.flutter_element_wait_timeout_option import FlutterElementWaitTimeOutOption +from appium.options.flutter_integration.flutter_enable_mock_camera_option import FlutterEnableMockCameraOption +from appium.options.flutter_integration.flutter_server_launch_timeout_option import FlutterServerLaunchTimeOutOption +from appium.options.flutter_integration.flutter_system_port_option import FlutterSystemPortOption + + +class FlutterOptions( + AppiumOptions, + FlutterElementWaitTimeOutOption, + FlutterEnableMockCameraOption, + FlutterServerLaunchTimeOutOption, + FlutterSystemPortOption, +): + @property + def default_capabilities(self) -> Dict: + return { + AUTOMATION_NAME: 'FlutterIntegration', + } diff --git a/appium/options/flutter_integration/flutter_element_wait_timeout_option.py b/appium/options/flutter_integration/flutter_element_wait_timeout_option.py new file mode 100644 index 00000000..bd427977 --- /dev/null +++ b/appium/options/flutter_integration/flutter_element_wait_timeout_option.py @@ -0,0 +1,50 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +FLUTTER_ELEMENT_WAIT_TIMEOUT = 'flutterElementWaitTimeout' + + +class FlutterElementWaitTimeOutOption(SupportsCapabilities): + @property + def flutter_element_wait_timeout(self) -> Optional[timedelta]: + """ + Maximum timeout to wait for element for Flutter integration test + + Returns: + Optional[timedelta]: The timeout value as a `timedelta` object if set, or `None` if the timeout is not defined. + """ + return self.get_capability(FLUTTER_ELEMENT_WAIT_TIMEOUT) + + @flutter_element_wait_timeout.setter + def flutter_element_wait_timeout(self, value: Union[timedelta, int]) -> None: + """ + Sets the maximum timeout to wait for a Flutter element in an integration test. + Default timeout is 5000ms + + Args: + value (Union[timedelta, int]): The timeout value, either as a `timedelta` object or an integer in milliseconds. + If provided as a `timedelta`, it will be converted to milliseconds. + """ + self.set_capability( + FLUTTER_ELEMENT_WAIT_TIMEOUT, + (int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value), + ) diff --git a/appium/options/flutter_integration/flutter_enable_mock_camera_option.py b/appium/options/flutter_integration/flutter_enable_mock_camera_option.py new file mode 100644 index 00000000..b90cb001 --- /dev/null +++ b/appium/options/flutter_integration/flutter_enable_mock_camera_option.py @@ -0,0 +1,44 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + + +from appium.options.common.supports_capabilities import SupportsCapabilities + +FLUTTER_ENABLE_MOCK_CAMERA = 'flutterEnableMockCamera' + + +class FlutterEnableMockCameraOption(SupportsCapabilities): + @property + def flutter_enable_mock_camera(self) -> bool: + """ + Get state of the mock camera for Flutter integration test + + Returns: + bool: A boolean indicating whether the mock camera is enabled (True) or disabled (False). + """ + return self.get_capability(FLUTTER_ENABLE_MOCK_CAMERA) + + @flutter_enable_mock_camera.setter + def flutter_enable_mock_camera(self, value: bool) -> None: + """ + Setter method enable or disable the mock camera for Flutter integration test + Default state is `False` + + Args: + value (bool): A boolean value indicating whether to enable (True) or disable (False) the mock camera. + """ + self.set_capability(FLUTTER_ENABLE_MOCK_CAMERA, value) diff --git a/appium/options/flutter_integration/flutter_server_launch_timeout_option.py b/appium/options/flutter_integration/flutter_server_launch_timeout_option.py new file mode 100644 index 00000000..32cea2f9 --- /dev/null +++ b/appium/options/flutter_integration/flutter_server_launch_timeout_option.py @@ -0,0 +1,51 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +FLUTTER_SERVER_LAUNCH_TIMEOUT = 'flutterServerLaunchTimeout' + + +class FlutterServerLaunchTimeOutOption(SupportsCapabilities): + @property + def flutter_server_launch_timeout(self) -> Optional[timedelta]: + """ + Gets the current timeout for launching the Flutter server in a Flutter application. + + Returns: + Optional[timedelta]: The timeout value as a `timedelta` object if set, or `None` if the timeout is not defined. + + """ + return self.get_capability(FLUTTER_SERVER_LAUNCH_TIMEOUT) + + @flutter_server_launch_timeout.setter + def flutter_server_launch_timeout(self, value: Union[timedelta, int]) -> None: + """ + Sets the timeout for launching the Flutter server in Flutter application. + Default timeout is 5000ms + + Args: + value (Union[timedelta, int]): The timeout value, either as a `timedelta` object or an integer in milliseconds. + If provided as a `timedelta`, it will be converted to milliseconds. + """ + self.set_capability( + FLUTTER_SERVER_LAUNCH_TIMEOUT, + (int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value), + ) diff --git a/appium/options/flutter_integration/flutter_system_port_option.py b/appium/options/flutter_integration/flutter_system_port_option.py new file mode 100644 index 00000000..db7ab7f5 --- /dev/null +++ b/appium/options/flutter_integration/flutter_system_port_option.py @@ -0,0 +1,45 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +FLUTTER_SYSTEM_PORT = 'flutterSystemPort' + + +class FlutterSystemPortOption(SupportsCapabilities): + @property + def flutter_system_port(self) -> Optional[int]: + """ + Get flutter system port for Flutter integration tests. + + Returns: + int: returns the port number + """ + return self.get_capability(FLUTTER_SYSTEM_PORT) + + @flutter_system_port.setter + def flutter_system_port(self, value: int) -> None: + """ + Sets the system port for Flutter integration tests. + By default the first free port from 10000..11000 range is selected + + Args: + value (int): The port number to be used for the Flutter server. + """ + self.set_capability(FLUTTER_SYSTEM_PORT, value) diff --git a/appium/options/gecko/__init__.py b/appium/options/gecko/__init__.py new file mode 100644 index 00000000..1dfd3287 --- /dev/null +++ b/appium/options/gecko/__init__.py @@ -0,0 +1 @@ +from .base import GeckoOptions diff --git a/appium/options/gecko/android_storage_option.py b/appium/options/gecko/android_storage_option.py new file mode 100644 index 00000000..1209ac50 --- /dev/null +++ b/appium/options/gecko/android_storage_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ANDROID_STORAGE = 'androidStorage' + + +class AndroidStorageOption(SupportsCapabilities): + @property + def android_storage(self) -> Optional[str]: + """ + The currently set storage type. + """ + return self.get_capability(ANDROID_STORAGE) + + @android_storage.setter + def android_storage(self, value: str) -> None: + """ + See https://firefox-source-docs.mozilla.org/testing/geckodriver + /Flags.html#code-android-storage-var-android-storage-var-code + """ + self.set_capability(ANDROID_STORAGE, value) diff --git a/appium/options/gecko/base.py b/appium/options/gecko/base.py new file mode 100644 index 00000000..873e5646 --- /dev/null +++ b/appium/options/gecko/base.py @@ -0,0 +1,51 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict + +from appium.options.common.automation_name_option import AUTOMATION_NAME +from appium.options.common.base import AppiumOptions +from appium.options.common.system_port_option import SystemPortOption + +from .android_storage_option import AndroidStorageOption +from .firefox_options_option import FirefoxOptionsOption +from .marionette_port_option import MarionettePortOption +from .verbosity_option import VerbosityOption + + +class GeckoOptions( + AppiumOptions, + AndroidStorageOption, + FirefoxOptionsOption, + MarionettePortOption, + SystemPortOption, + VerbosityOption, +): + @SystemPortOption.system_port.setter # type: ignore + def system_port(self, value: int) -> None: + """ + The number of the port for the driver to listen on. Must be unique + for each session. If not provided then the driver will try to detect + it automatically. + """ + SystemPortOption.system_port.fset(self, value) # type: ignore + + @property + def default_capabilities(self) -> Dict: + return { + AUTOMATION_NAME: 'Gecko', + } diff --git a/appium/options/gecko/firefox_options_option.py b/appium/options/gecko/firefox_options_option.py new file mode 100644 index 00000000..87906b4b --- /dev/null +++ b/appium/options/gecko/firefox_options_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Any, Dict, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +FIREFOX_OPTIONS = 'moz:firefoxOptions' + + +class FirefoxOptionsOption(SupportsCapabilities): + @property + def firefox_options(self) -> Optional[Dict[str, Any]]: + """ + Firefox options mapping. + """ + return self.get_capability(FIREFOX_OPTIONS) + + @firefox_options.setter + def firefox_options(self, value: Dict[str, Any]) -> None: + """ + See https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions + """ + self.set_capability(FIREFOX_OPTIONS, value) diff --git a/appium/options/gecko/marionette_port_option.py b/appium/options/gecko/marionette_port_option.py new file mode 100644 index 00000000..24846fd7 --- /dev/null +++ b/appium/options/gecko/marionette_port_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +MARIONETTE_PORT = 'marionettePort' + + +class MarionettePortOption(SupportsCapabilities): + @property + def marionette_port(self) -> Optional[int]: + """ + The number of the port for the Marionette server to listen on. + """ + return self.get_capability(MARIONETTE_PORT) + + @marionette_port.setter + def marionette_port(self, value: int) -> None: + """ + Selects the port for Geckodriver’s connection to the Marionette + remote protocol. The existing Firefox instance must have Marionette + enabled. To enable the remote protocol in Firefox, you can pass the + -marionette flag. Unless the marionette.port preference has been + user-set, Marionette will listen on port 2828, which is the default + value for this capability. + """ + self.set_capability(MARIONETTE_PORT, value) diff --git a/appium/options/gecko/verbosity_option.py b/appium/options/gecko/verbosity_option.py new file mode 100644 index 00000000..9888ddf2 --- /dev/null +++ b/appium/options/gecko/verbosity_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +VERBOSITY = 'verbosity' + + +class VerbosityOption(SupportsCapabilities): + @property + def verbosity(self) -> Optional[str]: + """ + The verbosity level of driver logging. + """ + return self.get_capability(VERBOSITY) + + @verbosity.setter + def verbosity(self, value: str) -> None: + """ + The verbosity level of driver logging. + By default, minimum verbosity is applied. + Either 'debug' or 'trace'. + """ + self.set_capability(VERBOSITY, value) diff --git a/appium/options/ios/__init__.py b/appium/options/ios/__init__.py new file mode 100644 index 00000000..21e82f75 --- /dev/null +++ b/appium/options/ios/__init__.py @@ -0,0 +1,2 @@ +from .safari.base import SafariOptions +from .xcuitest.base import XCUITestOptions diff --git a/appium/options/ios/safari/__init__.py b/appium/options/ios/safari/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/ios/safari/automatic_inspection_option.py b/appium/options/ios/safari/automatic_inspection_option.py new file mode 100644 index 00000000..3ab38e82 --- /dev/null +++ b/appium/options/ios/safari/automatic_inspection_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +AUTOMATIC_INSPECTION = 'safari:automaticInspection' + + +class AutomaticInspectionOption(SupportsCapabilities): + @property + def automatic_inspection(self) -> Optional[bool]: + """ + Whether to use automatic inspection. + """ + return self.get_capability(AUTOMATIC_INSPECTION) + + @automatic_inspection.setter + def automatic_inspection(self, value: bool) -> None: + """ + This capability instructs Safari to preload the Web Inspector and JavaScript + debugger in the background prior to returning a newly-created window. + To pause the test's execution in JavaScript and bring up Web Inspector's + Debugger tab, you can simply evaluate a debugger statement in the test page. + """ + self.set_capability(AUTOMATIC_INSPECTION, value) diff --git a/appium/options/ios/safari/automatic_profiling_option.py b/appium/options/ios/safari/automatic_profiling_option.py new file mode 100644 index 00000000..76eae620 --- /dev/null +++ b/appium/options/ios/safari/automatic_profiling_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +AUTOMATIC_PROFILING = 'safari:automaticProfiling' + + +class AutomaticProfilingOption(SupportsCapabilities): + @property + def automatic_profiling(self) -> Optional[bool]: + """ + Whether to use automatic profiling. + """ + return self.get_capability(AUTOMATIC_PROFILING) + + @automatic_profiling.setter + def automatic_profiling(self, value: bool) -> None: + """ + This capability instructs Safari to preload the Web Inspector and start + a Timeline recording in the background prior to returning a newly-created + window. To view the recording, open the Web Inspector through Safari's + Develop menu. + """ + self.set_capability(AUTOMATIC_PROFILING, value) diff --git a/appium/options/ios/safari/base.py b/appium/options/ios/safari/base.py new file mode 100644 index 00000000..8c6cfa68 --- /dev/null +++ b/appium/options/ios/safari/base.py @@ -0,0 +1,51 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict + +from appium.options.common.automation_name_option import AUTOMATION_NAME +from appium.options.common.base import PLATFORM_NAME, AppiumOptions + +from .automatic_inspection_option import AutomaticInspectionOption +from .automatic_profiling_option import AutomaticProfilingOption +from .device_name_option import DeviceNameOption +from .device_type_option import DeviceTypeOption +from .device_udid_option import DeviceUdidOption +from .platform_build_version_option import PlatformBuildVersionOption +from .platform_version_option import PlatformVersionOption +from .use_simulator_option import UseSimulatorOption +from .webkit_webrtc_option import WebkitWebrtcOption + + +class SafariOptions( + AppiumOptions, + AutomaticInspectionOption, + AutomaticProfilingOption, + DeviceNameOption, + DeviceTypeOption, + DeviceUdidOption, + PlatformBuildVersionOption, + PlatformVersionOption, + UseSimulatorOption, + WebkitWebrtcOption, +): + @property + def default_capabilities(self) -> Dict: + return { + PLATFORM_NAME: 'iOS', + AUTOMATION_NAME: 'Safari', + } diff --git a/appium/options/ios/safari/device_name_option.py b/appium/options/ios/safari/device_name_option.py new file mode 100644 index 00000000..ebc949ad --- /dev/null +++ b/appium/options/ios/safari/device_name_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +DEVICE_NAME = 'safari:deviceName' + + +class DeviceNameOption(SupportsCapabilities): + @property + def device_name(self) -> Optional[str]: + """ + String representing the name of the device. + """ + return self.get_capability(DEVICE_NAME) + + @device_name.setter + def device_name(self, value: str) -> None: + """ + safaridriver will only create a session using hosts whose device name + matches the value of safari:deviceName. Device names are compared + case-insensitively. NOTE: Device names for connected devices are shown in + iTunes. If Xcode is installed, device names for connected devices are available + via the output of instruments(1) and in the Devices and Simulators window + (accessed in Xcode via "Window -> Devices and Simulators"). + """ + self.set_capability(DEVICE_NAME, value) diff --git a/appium/options/ios/safari/device_type_option.py b/appium/options/ios/safari/device_type_option.py new file mode 100644 index 00000000..329974e3 --- /dev/null +++ b/appium/options/ios/safari/device_type_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +DEVICE_TYPE = 'safari:deviceType' + + +class DeviceTypeOption(SupportsCapabilities): + @property + def device_type(self) -> Optional[str]: + """ + String representing the type of the device. + """ + return self.get_capability(DEVICE_TYPE) + + @device_type.setter + def device_type(self, value: str) -> None: + """ + If the value of safari:deviceType is 'iPhone', safaridriver will only create a session + using an iPhone device or iPhone simulator. If the value of safari:deviceType is 'iPad', + safaridriver will only create a session using an iPad device or iPad simulator. + Values of safari:deviceType are compared case-insensitively. + """ + self.set_capability(DEVICE_TYPE, value) diff --git a/appium/options/ios/safari/device_udid_option.py b/appium/options/ios/safari/device_udid_option.py new file mode 100644 index 00000000..12850acf --- /dev/null +++ b/appium/options/ios/safari/device_udid_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +DEVICE_UDID = 'safari:deviceUDID' + + +class DeviceUdidOption(SupportsCapabilities): + @property + def device_udid(self) -> Optional[str]: + """ + String representing the UDID of the device. + """ + return self.get_capability(DEVICE_UDID) + + @device_udid.setter + def device_udid(self, value: str) -> None: + """ + safaridriver will only create a session using hosts whose device UDID + matches the value of safari:deviceUDID. Device UDIDs are compared + case-insensitively. NOTE: If Xcode is installed, UDIDs for connected + devices are available via the output of instruments(1) and in the + Devices and Simulators window (accessed in Xcode via + "Window -> Devices and Simulators"). + """ + self.set_capability(DEVICE_UDID, value) diff --git a/appium/options/ios/safari/platform_build_version_option.py b/appium/options/ios/safari/platform_build_version_option.py new file mode 100644 index 00000000..f411ec22 --- /dev/null +++ b/appium/options/ios/safari/platform_build_version_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +PLATFORM_BUILD_VERSION = 'safari:platformBuildVersion' + + +class PlatformBuildVersionOption(SupportsCapabilities): + @property + def platform_build_version(self) -> Optional[str]: + """ + String representing the platform build version. + """ + return self.get_capability(PLATFORM_BUILD_VERSION) + + @platform_build_version.setter + def platform_build_version(self, value: str) -> None: + """ + safaridriver will only create a session using hosts whose OS build + version matches the value of safari:platformBuildVersion. Example + of a macOS build version is '18E193'. On macOS, the OS build version + can be determined by running the sw_vers(1) utility. + """ + self.set_capability(PLATFORM_BUILD_VERSION, value) diff --git a/appium/options/ios/safari/platform_version_option.py b/appium/options/ios/safari/platform_version_option.py new file mode 100644 index 00000000..fd3d4b60 --- /dev/null +++ b/appium/options/ios/safari/platform_version_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +PLATFORM_VERSION = 'safari:platformVersion' + + +class PlatformVersionOption(SupportsCapabilities): + @property + def platform_version(self) -> Optional[str]: + """ + String representing the platform version. + """ + return self.get_capability(PLATFORM_VERSION) + + @platform_version.setter + def platform_version(self, value: str) -> None: + """ + safaridriver will only create a session using hosts whose OS + version matches the value of safari:platformVersion. OS version + numbers are prefix-matched. For example, if the value of safari:platformVersion + is '12', this will allow hosts with an OS version of '12.0' or '12.1' but not '10.12'. + """ + self.set_capability(PLATFORM_VERSION, value) diff --git a/appium/options/ios/safari/use_simulator_option.py b/appium/options/ios/safari/use_simulator_option.py new file mode 100644 index 00000000..25a09a6b --- /dev/null +++ b/appium/options/ios/safari/use_simulator_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +USE_SIMULATOR = 'safari:useSimulator' + + +class UseSimulatorOption(SupportsCapabilities): + @property + def use_simulator(self) -> Optional[bool]: + """ + Whether to use iOS Simulator. + """ + return self.get_capability(USE_SIMULATOR) + + @use_simulator.setter + def use_simulator(self, value: bool) -> None: + """ + If the value of safari:useSimulator is true, safaridriver will only use + iOS Simulator hosts. If the value of safari:useSimulator is false, safaridriver + will not use iOS Simulator hosts. NOTE: An Xcode installation is required + in order to run WebDriver tests on iOS Simulator hosts. + """ + self.set_capability(USE_SIMULATOR, value) diff --git a/appium/options/ios/safari/webkit_webrtc_option.py b/appium/options/ios/safari/webkit_webrtc_option.py new file mode 100644 index 00000000..7980df7a --- /dev/null +++ b/appium/options/ios/safari/webkit_webrtc_option.py @@ -0,0 +1,52 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Any, Dict, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WEBKIT_WEBRTC = 'webkit:WebRTC' + + +class WebkitWebrtcOption(SupportsCapabilities): + @property + def webkit_webrtc(self) -> Optional[Dict[str, Any]]: + """ + WebRTC policies. + """ + return self.get_capability(WEBKIT_WEBRTC) + + @webkit_webrtc.setter + def webkit_webrtc(self, value: Dict[str, Any]) -> None: + """ + This option allows a test to temporarily change Safari's policies + for WebRTC and Media Capture. + The following dictionary values are supported: + - DisableInsecureMediaCapture: Boolean value. + Normally, Safari refuses to allow media capture over insecure connections. + This restriction is relaxed by default for WebDriver sessions for testing + purposes (for example, a test web server not configured for HTTPS). When + this capability is specified, Safari will revert to the normal behavior of + preventing media capture over insecure connections. + - DisableICECandidateFiltering: Boolean value. + To protect a user's privacy, Safari normally filters out WebRTC + ICE candidates that correspond to internal network addresses when + capture devices are not in use. This capability suppresses ICE candidate + filtering so that both internal and external network addresses are + always sent as ICE candidates. + """ + self.set_capability(WEBKIT_WEBRTC, value) diff --git a/appium/options/ios/xcuitest/__init__.py b/appium/options/ios/xcuitest/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/ios/xcuitest/app/__init__.py b/appium/options/ios/xcuitest/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/ios/xcuitest/app/app_install_strategy_option.py b/appium/options/ios/xcuitest/app/app_install_strategy_option.py new file mode 100644 index 00000000..ff812fe8 --- /dev/null +++ b/appium/options/ios/xcuitest/app/app_install_strategy_option.py @@ -0,0 +1,46 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_INSTALL_STRATEGY = 'appInstallStrategy' + + +class AppInstallStrategyOption(SupportsCapabilities): + @property + def app_install_strategy(self) -> Optional[str]: + """ + App install strategy. + """ + return self.get_capability(APP_INSTALL_STRATEGY) + + @app_install_strategy.setter + def app_install_strategy(self, value: str) -> None: + """ + Select application installation strategy for real devices. The following + strategies are supported: + * serial (default) - pushes app files to the device in a sequential order; + this is the least performant strategy, although the most reliable; + * parallel - pushes app files simultaneously; this is usually the + most performant strategy, but sometimes could not be very stable; + * ios-deploy - tells the driver to use a third-party tool ios-deploy to + install the app; obviously the tool must be installed separately + first and must be present in PATH before it could be used. + """ + self.set_capability(APP_INSTALL_STRATEGY, value) diff --git a/appium/options/ios/xcuitest/app/app_push_timeout_option.py b/appium/options/ios/xcuitest/app/app_push_timeout_option.py new file mode 100644 index 00000000..3f49bde9 --- /dev/null +++ b/appium/options/ios/xcuitest/app/app_push_timeout_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_PUSH_TIMEOUT = 'appPushTimeout' + + +class AppPushTimeoutOption(SupportsCapabilities): + @property + def app_push_timeout(self) -> Optional[timedelta]: + """ + Maximum timeout for application upload. + """ + value = self.get_capability(APP_PUSH_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @app_push_timeout.setter + def app_push_timeout(self, value: Union[timedelta, int]) -> None: + """ + The timeout for application upload. + Works for real devices only. + The default value is 30000ms. + """ + self.set_capability(APP_PUSH_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/options/ios/xcuitest/app/localizable_strings_dir_option.py b/appium/options/ios/xcuitest/app/localizable_strings_dir_option.py new file mode 100644 index 00000000..e78155a6 --- /dev/null +++ b/appium/options/ios/xcuitest/app/localizable_strings_dir_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +LOCALIZABLE_STRINGS_DIR = 'localizableStringsDir' + + +class LocalizableStringsDirOption(SupportsCapabilities): + @property + def localizable_strings_dir(self) -> Optional[str]: + """ + Resource folder name where the main locale strings are stored. + """ + return self.get_capability(LOCALIZABLE_STRINGS_DIR) + + @localizable_strings_dir.setter + def localizable_strings_dir(self, value: str) -> None: + """ + Where to look for localizable strings in the application bundle. + Defaults to en.lproj. + """ + self.set_capability(LOCALIZABLE_STRINGS_DIR, value) diff --git a/appium/options/ios/xcuitest/base.py b/appium/options/ios/xcuitest/base.py new file mode 100644 index 00000000..0d349934 --- /dev/null +++ b/appium/options/ios/xcuitest/base.py @@ -0,0 +1,227 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict + +from appium.options.common.app_option import AppOption +from appium.options.common.auto_web_view_option import AutoWebViewOption +from appium.options.common.automation_name_option import AUTOMATION_NAME +from appium.options.common.base import PLATFORM_NAME, AppiumOptions +from appium.options.common.bundle_id_option import BundleIdOption +from appium.options.common.clear_system_files_option import ClearSystemFilesOption +from appium.options.common.device_name_option import DeviceNameOption +from appium.options.common.enable_performance_logging_option import EnablePerformanceLoggingOption +from appium.options.common.is_headless_option import IsHeadlessOption +from appium.options.common.language_option import LanguageOption +from appium.options.common.locale_option import LocaleOption +from appium.options.common.orientation_option import OrientationOption +from appium.options.common.other_apps_option import OtherAppsOption +from appium.options.common.platform_version_option import PlatformVersionOption +from appium.options.common.skip_log_capture_option import SkipLogCaptureOption +from appium.options.common.udid_option import UdidOption + +from .app.app_install_strategy_option import AppInstallStrategyOption +from .app.app_push_timeout_option import AppPushTimeoutOption +from .app.localizable_strings_dir_option import LocalizableStringsDirOption +from .general.include_device_caps_to_session_info_option import IncludeDeviceCapsToSessionInfoOption +from .general.reset_location_service_option import ResetLocationServiceOption +from .other.command_timeouts_option import CommandTimeoutsOption +from .other.launch_with_idb_option import LaunchWithIdbOption +from .other.show_ios_log_option import ShowIosLogOption +from .other.use_json_source_option import UseJsonSourceOption +from .simulator.calendar_access_authorized_option import CalendarAccessAuthorizedOption +from .simulator.calendar_format_option import CalendarFormatOption +from .simulator.connect_hardware_keyboard_option import ConnectHardwareKeyboardOption +from .simulator.custom_ssl_cert_option import CustomSslCertOption +from .simulator.enforce_fresh_simulator_creation_option import EnforceFreshSimulatorCreationOption +from .simulator.force_simulator_software_keyboard_presence_option import ForceSimulatorSoftwareKeyboardPresenceOption +from .simulator.ios_simulator_logs_predicate_option import IosSimulatorLogsPredicateOption +from .simulator.keep_key_chains_option import KeepKeyChainsOption +from .simulator.keychains_exclude_patterns_option import KeychainsExcludePatternsOption +from .simulator.permissions_option import PermissionsOption +from .simulator.reduce_motion_option import ReduceMotionOption +from .simulator.reset_on_session_start_only_option import ResetOnSessionStartOnlyOption +from .simulator.scale_factor_option import ScaleFactorOption +from .simulator.shutdown_other_simulators_option import ShutdownOtherSimulatorsOption +from .simulator.simulator_devices_set_path_option import SimulatorDevicesSetPathOption +from .simulator.simulator_pasteboard_automatic_sync_option import SimulatorPasteboardAutomaticSyncOption +from .simulator.simulator_startup_timeout_option import SimulatorStartupTimeoutOption +from .simulator.simulator_trace_pointer_option import SimulatorTracePointerOption +from .simulator.simulator_window_center_option import SimulatorWindowCenterOption +from .wda.allow_provisioning_device_regitration_option import AllowProvisioningDeviceRegistrationOption +from .wda.auto_accept_alerts_option import AutoAcceptAlertsOption +from .wda.auto_disimiss_alerts_option import AutoDismissAlertsOption +from .wda.derived_data_path_option import DerivedDataPathOption +from .wda.disable_automatic_screenshots_option import DisableAutomaticScreenshotsOption +from .wda.force_app_launch_option import ForceAppLaunchOption +from .wda.keychain_password_option import KeychainPasswordOption +from .wda.keychain_path_option import KeychainPathOption +from .wda.max_typing_frequency_option import MaxTypingFrequencyOption +from .wda.mjpeg_server_port_option import MjpegServerPortOption +from .wda.prebuilt_wda_path_option import PrebuiltWdaPathOption +from .wda.process_arguments_option import ProcessArgumentsOption +from .wda.result_bundle_path_option import ResultBundlePathOption +from .wda.screenshot_quality_option import ScreenshotQualityOption +from .wda.should_terminate_app_option import ShouldTerminateAppOption +from .wda.should_use_singleton_test_manager_option import ShouldUseSingletonTestManagerOption +from .wda.show_xcode_log_option import ShowXcodeLogOption +from .wda.simple_is_visible_check_option import SimpleIsVisibleCheckOption +from .wda.updated_wda_bundle_id_option import UpdatedWdaBundleIdOption +from .wda.use_native_caching_strategy_option import UseNativeCachingStrategyOption +from .wda.use_new_wda_option import UseNewWdaOption +from .wda.use_prebuilt_wda_option import UsePrebuiltWdaOption +from .wda.use_preinstalled_wda_option import UsePreinstalledWdaOption +from .wda.use_simple_build_test_option import UseSimpleBuildTestOption +from .wda.use_xctestrun_file_option import UseXctestrunFileOption +from .wda.wait_for_idle_timeout_option import WaitForIdleTimeoutOption +from .wda.wait_for_quiescence_option import WaitForQuiescenceOption +from .wda.wda_base_url_option import WdaBaseUrlOption +from .wda.wda_connection_timeout_option import WdaConnectionTimeoutOption +from .wda.wda_eventloop_idle_delay_option import WdaEventloopIdleDelayOption +from .wda.wda_launch_timeout_option import WdaLaunchTimeoutOption +from .wda.wda_local_port_option import WdaLocalPortOption +from .wda.wda_startup_retries_option import WdaStartupRetriesOption +from .wda.wda_startup_retry_interval_option import WdaStartupRetryIntervalOption +from .wda.web_driver_agent_url_option import WebDriverAgentUrlOption +from .wda.xcode_org_id_option import XcodeOrgIdOption +from .wda.xcode_signing_id_option import XcodeSigningIdOption +from .webview.absolute_web_locations_option import AbsoluteWebLocationsOption +from .webview.additional_webview_bundle_ids_option import AdditionalWebviewBundleIdsOption +from .webview.enable_async_execute_from_https_option import EnableAsyncExecuteFromHttpsOption +from .webview.full_context_list_option import FullContextListOption +from .webview.include_safari_in_webviews_option import IncludeSafariInWebviewsOption +from .webview.native_web_tap_option import NativeWebTapOption +from .webview.safari_garbage_collect_option import SafariGarbageCollectOption +from .webview.safari_ignore_fraud_warning_option import SafariIgnoreFraudWarningOption +from .webview.safari_ignore_web_hostnames_option import SafariIgnoreWebHostnamesOption +from .webview.safari_initial_url_option import SafariInitialUrlOption +from .webview.safari_log_all_communication_hex_dump_option import SafariLogAllCommunicationHexDumpOption +from .webview.safari_log_all_communication_option import SafariLogAllCommunicationOption +from .webview.safari_open_links_in_background_option import SafariOpenLinksInBackgroundOption +from .webview.safari_socket_chunk_size_option import SafariSocketChunkSizeOption +from .webview.safari_web_inspector_max_frame_length_option import SafariWebInspectorMaxFrameLengthOption +from .webview.webkit_response_timeout_option import WebkitResponseTimeoutOption +from .webview.webview_connect_retries_option import WebviewConnectRetriesOption +from .webview.webview_connect_timeout_option import WebviewConnectTimeoutOption + + +class XCUITestOptions( + AppiumOptions, + AppOption, + BundleIdOption, + PlatformVersionOption, + ClearSystemFilesOption, + OrientationOption, + UdidOption, + LanguageOption, + LocaleOption, + IsHeadlessOption, + SkipLogCaptureOption, + AutoWebViewOption, + EnablePerformanceLoggingOption, + OtherAppsOption, + DeviceNameOption, + IncludeDeviceCapsToSessionInfoOption, + ResetLocationServiceOption, + AppInstallStrategyOption, + AppPushTimeoutOption, + LocalizableStringsDirOption, + CommandTimeoutsOption, + LaunchWithIdbOption, + ShowIosLogOption, + UseJsonSourceOption, + CalendarAccessAuthorizedOption, + CalendarFormatOption, + ConnectHardwareKeyboardOption, + CustomSslCertOption, + EnforceFreshSimulatorCreationOption, + ForceSimulatorSoftwareKeyboardPresenceOption, + IosSimulatorLogsPredicateOption, + KeepKeyChainsOption, + KeychainsExcludePatternsOption, + PermissionsOption, + ReduceMotionOption, + ResetOnSessionStartOnlyOption, + ScaleFactorOption, + ShutdownOtherSimulatorsOption, + SimulatorDevicesSetPathOption, + SimulatorPasteboardAutomaticSyncOption, + SimulatorStartupTimeoutOption, + SimulatorTracePointerOption, + SimulatorWindowCenterOption, + AllowProvisioningDeviceRegistrationOption, + AutoAcceptAlertsOption, + AutoDismissAlertsOption, + DerivedDataPathOption, + DisableAutomaticScreenshotsOption, + ForceAppLaunchOption, + KeychainPasswordOption, + KeychainPathOption, + MaxTypingFrequencyOption, + MjpegServerPortOption, + PrebuiltWdaPathOption, + ProcessArgumentsOption, + ResultBundlePathOption, + ScreenshotQualityOption, + ShouldTerminateAppOption, + ShouldUseSingletonTestManagerOption, + ShowXcodeLogOption, + SimpleIsVisibleCheckOption, + UpdatedWdaBundleIdOption, + UseNativeCachingStrategyOption, + UseNewWdaOption, + UsePrebuiltWdaOption, + UsePreinstalledWdaOption, + UseSimpleBuildTestOption, + UseXctestrunFileOption, + WaitForIdleTimeoutOption, + WaitForQuiescenceOption, + WdaBaseUrlOption, + WdaConnectionTimeoutOption, + WdaEventloopIdleDelayOption, + WdaLaunchTimeoutOption, + WdaLocalPortOption, + WdaStartupRetriesOption, + WdaStartupRetryIntervalOption, + WebDriverAgentUrlOption, + XcodeOrgIdOption, + XcodeSigningIdOption, + AbsoluteWebLocationsOption, + AdditionalWebviewBundleIdsOption, + EnableAsyncExecuteFromHttpsOption, + FullContextListOption, + IncludeSafariInWebviewsOption, + NativeWebTapOption, + SafariGarbageCollectOption, + SafariIgnoreFraudWarningOption, + SafariIgnoreWebHostnamesOption, + SafariInitialUrlOption, + SafariLogAllCommunicationHexDumpOption, + SafariLogAllCommunicationOption, + SafariOpenLinksInBackgroundOption, + SafariSocketChunkSizeOption, + SafariWebInspectorMaxFrameLengthOption, + WebkitResponseTimeoutOption, + WebviewConnectRetriesOption, + WebviewConnectTimeoutOption, +): + @property + def default_capabilities(self) -> Dict: + return { + AUTOMATION_NAME: 'XCUITest', + PLATFORM_NAME: 'iOS', + } diff --git a/appium/options/ios/xcuitest/general/__init__.py b/appium/options/ios/xcuitest/general/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/ios/xcuitest/general/include_device_caps_to_session_info_option.py b/appium/options/ios/xcuitest/general/include_device_caps_to_session_info_option.py new file mode 100644 index 00000000..b5df6d28 --- /dev/null +++ b/appium/options/ios/xcuitest/general/include_device_caps_to_session_info_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +INCLUDE_DEVICE_CAPS_TO_SESSION_INFO = 'includeDeviceCapsToSessionInfo' + + +class IncludeDeviceCapsToSessionInfoOption(SupportsCapabilities): + @property + def include_device_caps_to_session_info(self) -> Optional[bool]: + """ + Whether to include screen information as the result of Get Session Capabilities. + """ + return self.get_capability(INCLUDE_DEVICE_CAPS_TO_SESSION_INFO) + + @include_device_caps_to_session_info.setter + def include_device_caps_to_session_info(self, value: bool) -> None: + """ + Whether to include screen information as the result of Get Session Capabilities. + It includes pixelRatio, statBarHeight and viewportRect, but + it causes an extra API call to WDA which may increase the response time. + Defaults to true. + """ + self.set_capability(INCLUDE_DEVICE_CAPS_TO_SESSION_INFO, value) diff --git a/appium/options/ios/xcuitest/general/reset_location_service_option.py b/appium/options/ios/xcuitest/general/reset_location_service_option.py new file mode 100644 index 00000000..c9e242cc --- /dev/null +++ b/appium/options/ios/xcuitest/general/reset_location_service_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +RESET_LOCATION_SERVICE = 'resetLocationService' + + +class ResetLocationServiceOption(SupportsCapabilities): + @property + def reset_location_service(self) -> Optional[bool]: + """ + Whether to reset the location service in the session deletion on real devices. + """ + return self.get_capability(RESET_LOCATION_SERVICE) + + @reset_location_service.setter + def reset_location_service(self, value: bool) -> None: + """ + Whether reset the location service in the session deletion on real devices. + Defaults to false. + """ + self.set_capability(RESET_LOCATION_SERVICE, value) diff --git a/appium/options/ios/xcuitest/other/__init__.py b/appium/options/ios/xcuitest/other/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/ios/xcuitest/other/command_timeouts_option.py b/appium/options/ios/xcuitest/other/command_timeouts_option.py new file mode 100644 index 00000000..420b3503 --- /dev/null +++ b/appium/options/ios/xcuitest/other/command_timeouts_option.py @@ -0,0 +1,57 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Dict, Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +COMMAND_TIMEOUTS = 'commandTimeouts' + + +class CommandTimeoutsOption(SupportsCapabilities): + @property + def command_timeouts(self) -> Optional[Union[Dict[str, timedelta], timedelta]]: + """ + Custom timeout(s) for WDA backend commands execution. + """ + value = self.get_capability(COMMAND_TIMEOUTS) + if value is None: + return None + if isinstance(value, dict): + return {k: timedelta(milliseconds=v) for k, v in value.items()} + return timedelta(milliseconds=int(value)) + + @command_timeouts.setter + def command_timeouts(self, value: Union[Dict[str, timedelta], timedelta, int]) -> None: + """ + Custom timeout for all WDA backend commands execution. + This might be useful if WDA backend freezes unexpectedly or requires too + much time to fail and blocks automated test execution. + Dictionary keys are command names which you can find in server logs. Look for + "Executing command 'command_name'" records. + Timeout value is expected to contain max duration to wait for + the given WDA command to be executed before terminating the session forcefully. + The magic 'default' key allows to provide the timeout for all other commands that + were not explicitly mentioned as dictionary keys + """ + if isinstance(value, dict): + self.set_capability(COMMAND_TIMEOUTS, {k: int(v.total_seconds() * 1000) for k, v in value.items()}) + elif isinstance(value, timedelta): + self.set_capability(COMMAND_TIMEOUTS, f'{int(value.total_seconds() * 1000)}') + else: + self.set_capability(COMMAND_TIMEOUTS, value) diff --git a/appium/options/ios/xcuitest/other/launch_with_idb_option.py b/appium/options/ios/xcuitest/other/launch_with_idb_option.py new file mode 100644 index 00000000..c8701c62 --- /dev/null +++ b/appium/options/ios/xcuitest/other/launch_with_idb_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +LAUNCH_WITH_IDB = 'launchWithIDB' + + +class LaunchWithIdbOption(SupportsCapabilities): + @property + def launch_with_idb(self) -> Optional[bool]: + """ + Whether to launch WebDriverAgentRunner with idb instead of xcodebuild. + """ + return self.get_capability(LAUNCH_WITH_IDB) + + @launch_with_idb.setter + def launch_with_idb(self, value: bool) -> None: + """ + Launch WebDriverAgentRunner with idb instead of xcodebuild. This could save + a significant amount of time by skipping the xcodebuild process, although the + idb might not be very reliable, especially with fresh Xcode SDKs. Check + the idb repository for more details on possible compatibility issues. + Defaults to false. + """ + self.set_capability(LAUNCH_WITH_IDB, value) diff --git a/appium/options/ios/xcuitest/other/show_ios_log_option.py b/appium/options/ios/xcuitest/other/show_ios_log_option.py new file mode 100644 index 00000000..b4bf75d3 --- /dev/null +++ b/appium/options/ios/xcuitest/other/show_ios_log_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SHOW_IOS_LOG = 'showIOSLog' + + +class ShowIosLogOption(SupportsCapabilities): + @property + def show_ios_log(self) -> Optional[bool]: + """ + Whether to show any logs captured from a device in the appium logs. + """ + return self.get_capability(SHOW_IOS_LOG) + + @show_ios_log.setter + def show_ios_log(self, value: bool) -> None: + """ + Whether to show any logs captured from a device in the appium logs. + Default false. + """ + self.set_capability(SHOW_IOS_LOG, value) diff --git a/appium/options/ios/xcuitest/other/use_json_source_option.py b/appium/options/ios/xcuitest/other/use_json_source_option.py new file mode 100644 index 00000000..1eac50bb --- /dev/null +++ b/appium/options/ios/xcuitest/other/use_json_source_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +USE_JSON_SOURCE = 'useJSONSource' + + +class UseJsonSourceOption(SupportsCapabilities): + @property + def use_json_source(self) -> Optional[bool]: + """ + Whether to get JSON source from WDA and transform it to XML on the driver side. + """ + return self.get_capability(USE_JSON_SOURCE) + + @use_json_source.setter + def use_json_source(self, value: bool) -> None: + """ + Whether to get JSON source from WDA and transform it to XML on the driver side. + Defaults to false. + """ + self.set_capability(USE_JSON_SOURCE, value) diff --git a/appium/options/ios/xcuitest/simulator/__init__.py b/appium/options/ios/xcuitest/simulator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/ios/xcuitest/simulator/calendar_access_authorized_option.py b/appium/options/ios/xcuitest/simulator/calendar_access_authorized_option.py new file mode 100644 index 00000000..a0e1afe9 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/calendar_access_authorized_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CALENDAR_ACCESS_AUTHORIZED = 'calendarAccessAuthorized' + + +class CalendarAccessAuthorizedOption(SupportsCapabilities): + @property + def calendar_access_authorized(self) -> Optional[bool]: + """ + Whether to enable calendar access on IOS Simulator. + """ + return self.get_capability(CALENDAR_ACCESS_AUTHORIZED) + + @calendar_access_authorized.setter + def calendar_access_authorized(self, value: bool) -> None: + """ + Set this to true if you want to enable calendar access on IOS Simulator + with given bundleId. Set to false, if you want to disable calendar access + on IOS Simulator with given bundleId. If not set, the calendar + authorization status will not be set. + """ + self.set_capability(CALENDAR_ACCESS_AUTHORIZED, value) diff --git a/appium/options/ios/xcuitest/simulator/calendar_format_option.py b/appium/options/ios/xcuitest/simulator/calendar_format_option.py new file mode 100644 index 00000000..4e756598 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/calendar_format_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CALENDAR_FORMAT = 'calendarFormat' + + +class CalendarFormatOption(SupportsCapabilities): + @property + def calendar_format(self) -> Optional[str]: + """ + Calendar format for the iOS Simulator. + """ + return self.get_capability(CALENDAR_FORMAT) + + @calendar_format.setter + def calendar_format(self, value: str) -> None: + """ + Set calendar format for the iOS Simulator. + """ + self.set_capability(CALENDAR_FORMAT, value) diff --git a/appium/options/ios/xcuitest/simulator/connect_hardware_keyboard_option.py b/appium/options/ios/xcuitest/simulator/connect_hardware_keyboard_option.py new file mode 100644 index 00000000..f7118d6d --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/connect_hardware_keyboard_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CONNECT_HARDWARE_KEYBOARD = 'connectHardwareKeyboard' + + +class ConnectHardwareKeyboardOption(SupportsCapabilities): + @property + def connect_hardware_keyboard(self) -> Optional[bool]: + """ + Whether to connect hardware keyboard to Simulator. + """ + return self.get_capability(CONNECT_HARDWARE_KEYBOARD) + + @connect_hardware_keyboard.setter + def connect_hardware_keyboard(self, value: bool) -> None: + """ + Set this option to true in order to enable hardware keyboard in Simulator. + The preference works only when Appium launches a simulator instance with + this value. It is set to false by default, because this helps to workaround + some XCTest bugs. connectHardwareKeyboard: true makes + forceSimulatorSoftwareKeyboardPresence: false if no explicit value is set + for forceSimulatorSoftwareKeyboardPresence capability since Appium 1.22.0. + """ + self.set_capability(CONNECT_HARDWARE_KEYBOARD, value) diff --git a/appium/options/ios/xcuitest/simulator/custom_ssl_cert_option.py b/appium/options/ios/xcuitest/simulator/custom_ssl_cert_option.py new file mode 100644 index 00000000..a3914b93 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/custom_ssl_cert_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CUSTOM_SSL_CERT = 'customSSLCert' + + +class CustomSslCertOption(SupportsCapabilities): + @property + def custom_ssl_cert(self) -> Optional[str]: + """ + SSL certificate content. + """ + return self.get_capability(CUSTOM_SSL_CERT) + + @custom_ssl_cert.setter + def custom_ssl_cert(self, value: str) -> None: + """ + Adds a root SSL certificate to IOS Simulator. + The certificate content must be provided in PEM format. + """ + self.set_capability(CUSTOM_SSL_CERT, value) diff --git a/appium/options/ios/xcuitest/simulator/enforce_fresh_simulator_creation_option.py b/appium/options/ios/xcuitest/simulator/enforce_fresh_simulator_creation_option.py new file mode 100644 index 00000000..cea87eb0 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/enforce_fresh_simulator_creation_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ENFORCE_FRESH_SIMULATOR_CREATION = 'enforceFreshSimulatorCreation' + + +class EnforceFreshSimulatorCreationOption(SupportsCapabilities): + @property + def enforce_fresh_simulator_creation(self) -> Optional[bool]: + """ + Whether to create a new simulator for each new test session. + """ + return self.get_capability(ENFORCE_FRESH_SIMULATOR_CREATION) + + @enforce_fresh_simulator_creation.setter + def enforce_fresh_simulator_creation(self, value: bool) -> None: + """ + Creates a new simulator in session creation and deletes it in session deletion. + Defaults to false. + """ + self.set_capability(ENFORCE_FRESH_SIMULATOR_CREATION, value) diff --git a/appium/options/ios/xcuitest/simulator/force_simulator_software_keyboard_presence_option.py b/appium/options/ios/xcuitest/simulator/force_simulator_software_keyboard_presence_option.py new file mode 100644 index 00000000..1e1a697a --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/force_simulator_software_keyboard_presence_option.py @@ -0,0 +1,45 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE = 'forceSimulatorSoftwareKeyboardPresence' + + +class ForceSimulatorSoftwareKeyboardPresenceOption(SupportsCapabilities): + @property + def force_simulator_software_keyboard_presence(self) -> Optional[bool]: + """ + Whether to enforce software keyboard presence. + """ + return self.get_capability(FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE) + + @force_simulator_software_keyboard_presence.setter + def force_simulator_software_keyboard_presence(self, value: bool) -> None: + """ + Set this option to true in order to turn software keyboard on and turn + hardware keyboard off in Simulator since Appium 1.22.0. This option helps + to avoid Keyboard is not present error. It is set to true by default. + Appium respects preset simulator software/hardware keyboard preference + when this value is false, so connectHardwareKeyboard: false and + forceSimulatorSoftwareKeyboardPresence: false means for Appium to keep + the current Simulator keyboard preferences. This option has priority + over connectHardwareKeyboard. + """ + self.set_capability(FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE, value) diff --git a/appium/options/ios/xcuitest/simulator/ios_simulator_logs_predicate_option.py b/appium/options/ios/xcuitest/simulator/ios_simulator_logs_predicate_option.py new file mode 100644 index 00000000..c7d48647 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/ios_simulator_logs_predicate_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +IOS_SIMULATOR_LOGS_PREDICATE = 'iosSimulatorLogsPredicate' + + +class IosSimulatorLogsPredicateOption(SupportsCapabilities): + @property + def ios_simulator_logs_predicate(self) -> Optional[bool]: + """ + Get Simulator log filtering predicate. + """ + return self.get_capability(IOS_SIMULATOR_LOGS_PREDICATE) + + @ios_simulator_logs_predicate.setter + def ios_simulator_logs_predicate(self, value: bool) -> None: + """ + Set the --predicate flag in the ios simulator logs. + """ + self.set_capability(IOS_SIMULATOR_LOGS_PREDICATE, value) diff --git a/appium/options/ios/xcuitest/simulator/keep_key_chains_option.py b/appium/options/ios/xcuitest/simulator/keep_key_chains_option.py new file mode 100644 index 00000000..a304c935 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/keep_key_chains_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +KEEP_KEY_CHAINS = 'keepKeyChains' + + +class KeepKeyChainsOption(SupportsCapabilities): + @property + def keep_key_chains(self) -> Optional[bool]: + """ + Whether to preserve Simulator keychains after full reset. + """ + return self.get_capability(KEEP_KEY_CHAINS) + + @keep_key_chains.setter + def keep_key_chains(self, value: bool) -> None: + """ + Set the capability to true in order to preserve Simulator keychains folder after + full reset. This feature has no effect on real devices. Defaults to false. + """ + self.set_capability(KEEP_KEY_CHAINS, value) diff --git a/appium/options/ios/xcuitest/simulator/keychains_exclude_patterns_option.py b/appium/options/ios/xcuitest/simulator/keychains_exclude_patterns_option.py new file mode 100644 index 00000000..d971f5c5 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/keychains_exclude_patterns_option.py @@ -0,0 +1,44 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +KEYCHAINS_EXCLUDE_PATTERNS = 'keychainsExcludePatterns' + + +class KeychainsExcludePatternsOption(SupportsCapabilities): + @property + def keychains_exclude_patterns(self) -> Optional[str]: + """ + Keychains exclude patterns. + """ + return self.get_capability(KEYCHAINS_EXCLUDE_PATTERNS) + + @keychains_exclude_patterns.setter + def keychains_exclude_patterns(self, value: str) -> None: + """ + This capability accepts comma-separated path patterns, + which are going to be excluded from keychains restore while + full reset is being performed on Simulator. It might be + useful if you want to exclude only particular keychain types + from being restored, like the applications keychain. This + feature has no effect on real devices. E.g. "*keychain*.db*" + to exclude applications keychain from being restored + """ + self.set_capability(KEYCHAINS_EXCLUDE_PATTERNS, value) diff --git a/appium/options/ios/xcuitest/simulator/permissions_option.py b/appium/options/ios/xcuitest/simulator/permissions_option.py new file mode 100644 index 00000000..6146e382 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/permissions_option.py @@ -0,0 +1,50 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +import json +from typing import Dict, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +PERMISSIONS = 'permissions' + + +class PermissionsOption(SupportsCapabilities): + @property + def permissions(self) -> Optional[Dict[str, Dict[str, str]]]: + """ + Get Simulator permissions. + """ + value = self.get_capability(PERMISSIONS) + return None if value is None else json.loads(value) + + @permissions.setter + def permissions(self, value: Dict[str, Dict[str, str]]) -> None: + """ + Allows setting of permissions for the specified application bundle on + Simulator only. + + Since Xcode SDK 11.4 Apple provides native APIs to interact with + application settings. Check the output of `xcrun simctl privacy booted` + command to get the list of available permission names. Use yes, no + and unset as values in order to grant, revoke or reset the corresponding + permission. Below Xcode SDK 11.4 it is required that applesimutils package + is installed and available in PATH. The list of available service names + and statuses can be found at https://github.com/wix/AppleSimulatorUtils. + For example: {"com.apple.mobilecal": {"calendar": "YES"}} + """ + self.set_capability(PERMISSIONS, json.dumps(value, ensure_ascii=False)) diff --git a/appium/options/ios/xcuitest/simulator/reduce_motion_option.py b/appium/options/ios/xcuitest/simulator/reduce_motion_option.py new file mode 100644 index 00000000..89dbe877 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/reduce_motion_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +REDUCE_MOTION = 'reduceMotion' + + +class ReduceMotionOption(SupportsCapabilities): + @property + def reduce_motion(self) -> Optional[bool]: + """ + Whether to reduce motion accessibility preference. + """ + return self.get_capability(REDUCE_MOTION) + + @reduce_motion.setter + def reduce_motion(self, value: bool) -> None: + """ + Allows to turn on/off reduce motion accessibility preference. + Setting reduceMotion on helps to reduce flakiness during tests. + Only on simulators. + """ + self.set_capability(REDUCE_MOTION, value) diff --git a/appium/options/ios/xcuitest/simulator/reset_on_session_start_only_option.py b/appium/options/ios/xcuitest/simulator/reset_on_session_start_only_option.py new file mode 100644 index 00000000..294a7211 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/reset_on_session_start_only_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +RESET_ON_SESSION_START_ONLY = 'resetOnSessionStartOnly' + + +class ResetOnSessionStartOnlyOption(SupportsCapabilities): + @property + def reset_on_session_start_only(self) -> Optional[bool]: + """ + Whether to perform Simulator reset on test session finish (false) or not (true). + """ + return self.get_capability(RESET_ON_SESSION_START_ONLY) + + @reset_on_session_start_only.setter + def reset_on_session_start_only(self, value: bool) -> None: + """ + Whether to perform reset on test session finish (false) or not (true). + Keeping this variable set to true and Simulator running (the default + behaviour since version 1.6.4) may significantly shorten the duration of + test session initialization. + """ + self.set_capability(RESET_ON_SESSION_START_ONLY, value) diff --git a/appium/options/ios/xcuitest/simulator/scale_factor_option.py b/appium/options/ios/xcuitest/simulator/scale_factor_option.py new file mode 100644 index 00000000..8b7e6e03 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/scale_factor_option.py @@ -0,0 +1,44 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SCALE_FACTOR = 'scaleFactor' + + +class ScaleFactorOption(SupportsCapabilities): + @property + def scale_factor(self) -> Optional[str]: + """ + Simulator scale factor. + """ + return self.get_capability(SCALE_FACTOR) + + @scale_factor.setter + def scale_factor(self, value: str) -> None: + """ + Simulator scale factor. This is useful to have if the default resolution + of simulated device is greater than the actual display resolution. + So you can scale the simulator to see the whole device screen without scrolling. + Acceptable values for simulators running Xcode SDK 8 and older are: '1.0', + '0.75', '0.5', '0.33' and '0.25', where '1.0' means 100% scale. + For simulators running Xcode SDK 9 and above the value could be any valid + positive float number. + """ + self.set_capability(SCALE_FACTOR, value) diff --git a/appium/options/ios/xcuitest/simulator/shutdown_other_simulators_option.py b/appium/options/ios/xcuitest/simulator/shutdown_other_simulators_option.py new file mode 100644 index 00000000..a7c5edc5 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/shutdown_other_simulators_option.py @@ -0,0 +1,44 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SHUTDOWN_OTHER_SIMULATORS = 'shutdownOtherSimulators' + + +class ShutdownOtherSimulatorsOption(SupportsCapabilities): + @property + def shutdown_other_simulators(self) -> Optional[bool]: + """ + Whether to shut down of other booted simulators except of the current one. + """ + return self.get_capability(SHUTDOWN_OTHER_SIMULATORS) + + @shutdown_other_simulators.setter + def shutdown_other_simulators(self, value: bool) -> None: + """ + If this capability set to true and the current device under test is an iOS + Simulator then Appium will try to shut down all the other running Simulators + before to start a new session. This might be useful while executing webview + tests on different devices, since only one device can be debugged remotely + at once due to an Apple bug. The capability only has an effect if + --relaxed-security command line argument is provided to the server. + Defaults to false. + """ + self.set_capability(SHUTDOWN_OTHER_SIMULATORS, value) diff --git a/appium/options/ios/xcuitest/simulator/simulator_devices_set_path_option.py b/appium/options/ios/xcuitest/simulator/simulator_devices_set_path_option.py new file mode 100644 index 00000000..84557535 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/simulator_devices_set_path_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SIMULATOR_DEVICES_SET_PATH = 'simulatorDevicesSetPath' + + +class SimulatorDevicesSetPathOption(SupportsCapabilities): + @property + def simulator_devices_set_path(self) -> Optional[str]: + """ + Alternative path to the simulator devices set. + """ + return self.get_capability(SIMULATOR_DEVICES_SET_PATH) + + @simulator_devices_set_path.setter + def simulator_devices_set_path(self, value: str) -> None: + """ + This capability allows to set an alternative path to the simulator devices + set in case you have multiple sets deployed on your local system. Such + feature could be useful if you, for example, would like to save disk space + on the main system volume. + """ + self.set_capability(SIMULATOR_DEVICES_SET_PATH, value) diff --git a/appium/options/ios/xcuitest/simulator/simulator_pasteboard_automatic_sync_option.py b/appium/options/ios/xcuitest/simulator/simulator_pasteboard_automatic_sync_option.py new file mode 100644 index 00000000..a1258df4 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/simulator_pasteboard_automatic_sync_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SIMULATOR_PASTEBOARD_AUTOMATIC_SYNC = 'simulatorPasteboardAutomaticSync' + + +class SimulatorPasteboardAutomaticSyncOption(SupportsCapabilities): + @property + def simulator_pasteboard_automatic_sync(self) -> Optional[bool]: + """ + Pasteboard automation sync state. + """ + return self.get_capability(SIMULATOR_PASTEBOARD_AUTOMATIC_SYNC) + + @simulator_pasteboard_automatic_sync.setter + def simulator_pasteboard_automatic_sync(self, value: bool) -> None: + """ + Handle the -PasteboardAutomaticSync flag when simulator process launches. + It could improve launching simulator performance not to sync pasteboard with + the system when this value is off. on forces the flag enabled. system does + not provide the flag to the launching command. on, off, or system is available. + They are case-insensitive. Defaults to off. + """ + self.set_capability(SIMULATOR_PASTEBOARD_AUTOMATIC_SYNC, value) diff --git a/appium/options/ios/xcuitest/simulator/simulator_startup_timeout_option.py b/appium/options/ios/xcuitest/simulator/simulator_startup_timeout_option.py new file mode 100644 index 00000000..29353498 --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/simulator_startup_timeout_option.py @@ -0,0 +1,46 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SIMULATOR_STARTUP_TIMEOUT = 'simulatorStartupTimeout' + + +class SimulatorStartupTimeoutOption(SupportsCapabilities): + @property + def simulator_startup_timeout(self) -> Optional[timedelta]: + """ + Simulator startup timeout. + """ + value = self.get_capability(SIMULATOR_STARTUP_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @simulator_startup_timeout.setter + def simulator_startup_timeout(self, value: Union[timedelta, int]) -> None: + """ + Allows to change the default timeout for Simulator startup. + By default, this value is set to 120000ms (2 minutes), + although the startup could take longer on a weak hardware + or if other concurrent processes use much system resources + during the boot up procedure. + """ + self.set_capability( + SIMULATOR_STARTUP_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value + ) diff --git a/appium/options/ios/xcuitest/simulator/simulator_trace_pointer_option.py b/appium/options/ios/xcuitest/simulator/simulator_trace_pointer_option.py new file mode 100644 index 00000000..d5f5d1fd --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/simulator_trace_pointer_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SIMULATOR_TRACE_POINTER = 'simulatorTracePointer' + + +class SimulatorTracePointerOption(SupportsCapabilities): + @property + def simulator_trace_pointer(self) -> Optional[bool]: + """ + Whether to highlight pointer moves in the Simulator window. + """ + return self.get_capability(SIMULATOR_TRACE_POINTER) + + @simulator_trace_pointer.setter + def simulator_trace_pointer(self, value: bool) -> None: + """ + Set whether to highlight pointer moves in the Simulator window. + The Simulator UI client must be shut down before the session + startup in order for this capability to be applied properly. + false by default. + """ + self.set_capability(SIMULATOR_TRACE_POINTER, value) diff --git a/appium/options/ios/xcuitest/simulator/simulator_window_center_option.py b/appium/options/ios/xcuitest/simulator/simulator_window_center_option.py new file mode 100644 index 00000000..1f8b2d8b --- /dev/null +++ b/appium/options/ios/xcuitest/simulator/simulator_window_center_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SIMULATOR_WINDOW_CENTER = 'simulatorWindowCenter' + + +class SimulatorWindowCenterOption(SupportsCapabilities): + @property + def simulator_window_center(self) -> Optional[str]: + """ + Simulator window center coordinates. + """ + return self.get_capability(SIMULATOR_WINDOW_CENTER) + + @simulator_window_center.setter + def simulator_window_center(self, value: str) -> None: + """ + Allows to explicitly set the coordinates of Simulator window center + for Xcode9+ SDK. This capability only has an effect if Simulator + window has not been opened yet for the current session before it started. + e.g. "{-100.0,100.0}" or "{500,500}", spaces are not allowed + """ + self.set_capability(SIMULATOR_WINDOW_CENTER, value) diff --git a/appium/options/ios/xcuitest/wda/__init__.py b/appium/options/ios/xcuitest/wda/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/ios/xcuitest/wda/allow_provisioning_device_regitration_option.py b/appium/options/ios/xcuitest/wda/allow_provisioning_device_regitration_option.py new file mode 100644 index 00000000..c88124b4 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/allow_provisioning_device_regitration_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ALLOW_PROVISIONING_DEVICE_REGISTRATION = 'allowProvisioningDeviceRegistration' + + +class AllowProvisioningDeviceRegistrationOption(SupportsCapabilities): + @property + def allow_provisioning_device_registration(self) -> Optional[bool]: + """ + Whether to allow xcodebuild to register your destination device on the developer portal. + """ + return self.get_capability(ALLOW_PROVISIONING_DEVICE_REGISTRATION) + + @allow_provisioning_device_registration.setter + def allow_provisioning_device_registration(self, value: bool) -> None: + """ + Allow xcodebuild to register your destination device on the developer portal + if necessary. Requires a developer account to have been added in Xcode's Accounts + preference pane. Defaults to false. + """ + self.set_capability(ALLOW_PROVISIONING_DEVICE_REGISTRATION, value) diff --git a/appium/options/ios/xcuitest/wda/auto_accept_alerts_option.py b/appium/options/ios/xcuitest/wda/auto_accept_alerts_option.py new file mode 100644 index 00000000..05c55943 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/auto_accept_alerts_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +AUTO_ACCEPT_ALERTS = 'autoAcceptAlerts' + + +class AutoAcceptAlertsOption(SupportsCapabilities): + @property + def auto_accept_alerts(self) -> Optional[bool]: + """ + Whether to accept all alerts automatically. + """ + return self.get_capability(AUTO_ACCEPT_ALERTS) + + @auto_accept_alerts.setter + def auto_accept_alerts(self, value: bool) -> None: + """ + Accept all iOS alerts automatically if they pop up. This includes privacy + access permission alerts (e.g., location, contacts, photos). Default is false. + """ + self.set_capability(AUTO_ACCEPT_ALERTS, value) diff --git a/appium/options/ios/xcuitest/wda/auto_disimiss_alerts_option.py b/appium/options/ios/xcuitest/wda/auto_disimiss_alerts_option.py new file mode 100644 index 00000000..61a28414 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/auto_disimiss_alerts_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +AUTO_DISMISS_ALERTS = 'autoDismissAlerts' + + +class AutoDismissAlertsOption(SupportsCapabilities): + @property + def auto_dismiss_alerts(self) -> Optional[bool]: + """ + Whether to dismiss all alerts automatically. + """ + return self.get_capability(AUTO_DISMISS_ALERTS) + + @auto_dismiss_alerts.setter + def auto_dismiss_alerts(self, value: bool) -> None: + """ + Dismiss all iOS alerts automatically if they pop up. This includes privacy + access permission alerts (e.g., location, contacts, photos). Default is false. + """ + self.set_capability(AUTO_DISMISS_ALERTS, value) diff --git a/appium/options/ios/xcuitest/wda/derived_data_path_option.py b/appium/options/ios/xcuitest/wda/derived_data_path_option.py new file mode 100644 index 00000000..ffa40a68 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/derived_data_path_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +DERIVED_DATA_PATH = 'derivedDataPath' + + +class DerivedDataPathOption(SupportsCapabilities): + @property + def derived_data_path(self) -> Optional[str]: + """ + Path to the derived data WDA folder. + """ + return self.get_capability(DERIVED_DATA_PATH) + + @derived_data_path.setter + def derived_data_path(self, value: str) -> None: + """ + Use along with usePrebuiltWDA capability and choose where to search for the existing WDA app. + If the capability is not set then Xcode will store the derived data in the default root + taken from preferences. + It also makes sense to choose different folders for parallel WDA sessions. + """ + self.set_capability(DERIVED_DATA_PATH, value) diff --git a/appium/options/ios/xcuitest/wda/disable_automatic_screenshots_option.py b/appium/options/ios/xcuitest/wda/disable_automatic_screenshots_option.py new file mode 100644 index 00000000..ac76af33 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/disable_automatic_screenshots_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +DISABLE_AUTOMATIC_SCREENSHOTS = 'disableAutomaticScreenshots' + + +class DisableAutomaticScreenshotsOption(SupportsCapabilities): + @property + def disable_automatic_screenshots(self) -> Optional[bool]: + """ + Whether to disable automatic XCTest screenshots. + """ + return self.get_capability(DISABLE_AUTOMATIC_SCREENSHOTS) + + @disable_automatic_screenshots.setter + def disable_automatic_screenshots(self, value: bool) -> None: + """ + Disable automatic screenshots taken by XCTest at every interaction. + Default is up to WebDriverAgent's config to decide, which currently + defaults to true. + """ + self.set_capability(DISABLE_AUTOMATIC_SCREENSHOTS, value) diff --git a/appium/options/ios/xcuitest/wda/force_app_launch_option.py b/appium/options/ios/xcuitest/wda/force_app_launch_option.py new file mode 100644 index 00000000..364133d0 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/force_app_launch_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +FORCE_APP_LAUNCH = 'forceAppLaunch' + + +class ForceAppLaunchOption(SupportsCapabilities): + @property + def force_app_launch(self) -> Optional[bool]: + """ + Whether to enforce app restart on session startup. + """ + return self.get_capability(FORCE_APP_LAUNCH) + + @force_app_launch.setter + def force_app_launch(self, value: bool) -> None: + """ + Specify if the app should be forcefully restarted if it is already + running on session startup. This capability only has an effect if an + application identifier has been passed to the test session (either + explicitly, by setting bundleId, or implicitly, by providing app). + Default is true unless noReset capability is set to true. + """ + self.set_capability(FORCE_APP_LAUNCH, value) diff --git a/appium/options/ios/xcuitest/wda/keychain_password_option.py b/appium/options/ios/xcuitest/wda/keychain_password_option.py new file mode 100644 index 00000000..5514519f --- /dev/null +++ b/appium/options/ios/xcuitest/wda/keychain_password_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +KEYCHAIN_PASSWORD = 'keychainPassword' + + +class KeychainPasswordOption(SupportsCapabilities): + @property + def keychain_password(self) -> Optional[str]: + """ + Custom keychain password. + """ + return self.get_capability(KEYCHAIN_PASSWORD) + + @keychain_password.setter + def keychain_password(self, value: str) -> None: + """ + Custom keychain password. The keychain is expected to + contain the private development key. + """ + self.set_capability(KEYCHAIN_PASSWORD, value) diff --git a/appium/options/ios/xcuitest/wda/keychain_path_option.py b/appium/options/ios/xcuitest/wda/keychain_path_option.py new file mode 100644 index 00000000..5cf39742 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/keychain_path_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +KEYCHAIN_PATH = 'keychainPath' + + +class KeychainPathOption(SupportsCapabilities): + @property + def keychain_path(self) -> Optional[str]: + """ + Path to a custom keychain. + """ + return self.get_capability(KEYCHAIN_PATH) + + @keychain_path.setter + def keychain_path(self, value: str) -> None: + """ + Path to a custom keychain, which + contains the private development key. + """ + self.set_capability(KEYCHAIN_PATH, value) diff --git a/appium/options/ios/xcuitest/wda/max_typing_frequency_option.py b/appium/options/ios/xcuitest/wda/max_typing_frequency_option.py new file mode 100644 index 00000000..d5ea8dea --- /dev/null +++ b/appium/options/ios/xcuitest/wda/max_typing_frequency_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +MAX_TYPING_FREQUENCY = 'maxTypingFrequency' + + +class MaxTypingFrequencyOption(SupportsCapabilities): + @property + def max_typing_frequency(self) -> Optional[int]: + """ + The number of keystrokes per minute. + """ + return self.get_capability(MAX_TYPING_FREQUENCY) + + @max_typing_frequency.setter + def max_typing_frequency(self, value: int) -> None: + """ + Maximum frequency of keystrokes for typing and clear. If your tests + are failing because of typing errors, you may want to adjust this. + Defaults to 60 keystrokes per minute. + """ + self.set_capability(MAX_TYPING_FREQUENCY, value) diff --git a/appium/options/ios/xcuitest/wda/mjpeg_server_port_option.py b/appium/options/ios/xcuitest/wda/mjpeg_server_port_option.py new file mode 100644 index 00000000..679c1af4 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/mjpeg_server_port_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +MJPEG_SERVER_PORT = 'mjpegServerPort' + + +class MjpegServerPortOption(SupportsCapabilities): + @property + def mjpeg_server_port(self) -> Optional[int]: + """ + Port number on which WDA broadcasts screenshots stream encoded into MJPEG + format from the device under test. + """ + return self.get_capability(MJPEG_SERVER_PORT) + + @mjpeg_server_port.setter + def mjpeg_server_port(self, value: int) -> None: + """ + Port number on which WDA broadcasts screenshots stream encoded into MJPEG + format from the device under test. It might be necessary to change this value + if the default port is busy because of other tests running in parallel. + Default value: 9100. + """ + self.set_capability(MJPEG_SERVER_PORT, value) diff --git a/appium/options/ios/xcuitest/wda/prebuilt_wda_path_option.py b/appium/options/ios/xcuitest/wda/prebuilt_wda_path_option.py new file mode 100644 index 00000000..f26d12f0 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/prebuilt_wda_path_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +PREBUILT_WDA_PATH = 'prebuiltWDAPath' + + +class PrebuiltWdaPathOption(SupportsCapabilities): + @property + def prebuilt_wda_path(self) -> Optional[str]: + """ + The path to the prebuilt WebDriverAgent. + """ + return self.get_capability(PREBUILT_WDA_PATH) + + @prebuilt_wda_path.setter + def prebuilt_wda_path(self, value: str) -> None: + """ + The path to the prebuilt WebDriverAgent. This should be the path to the + WebDriverAgent.xcarchive file or the WebDriverAgent.app bundle. + """ + self.set_capability(PREBUILT_WDA_PATH, value) diff --git a/appium/options/ios/xcuitest/wda/process_arguments_option.py b/appium/options/ios/xcuitest/wda/process_arguments_option.py new file mode 100644 index 00000000..5b169a94 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/process_arguments_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict, List, Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +PROCESS_ARGUMENTS = 'processArguments' + + +class ProcessArgumentsOption(SupportsCapabilities): + @property + def process_arguments(self) -> Optional[Dict[str, Union[List[str], Dict[str, str]]]]: + """ + Command line arguments and/or environment variables of the application under test. + """ + return self.get_capability(PROCESS_ARGUMENTS) + + @process_arguments.setter + def process_arguments(self, value: Dict[str, Union[List[str], Dict[str, str]]]) -> None: + """ + Provides process arguments and environment which will be sent + to the WebDriverAgent server. Acceptable dictionary keys are 'env' + and 'args'. The value of 'args' should be a list of app command line + arguments represented as strings and the value of 'env' is expected to + be a dictionary of environment variable names and their values (also strings). + """ + self.set_capability(PROCESS_ARGUMENTS, value) diff --git a/appium/options/ios/xcuitest/wda/result_bundle_path_option.py b/appium/options/ios/xcuitest/wda/result_bundle_path_option.py new file mode 100644 index 00000000..5cf7ec09 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/result_bundle_path_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +RESULT_BUNDLE_PATH = 'resultBundlePath' + + +class ResultBundlePathOption(SupportsCapabilities): + @property + def result_bundle_path(self) -> Optional[str]: + """ + Path where the resulting XCTest bundle should be stored. + """ + return self.get_capability(RESULT_BUNDLE_PATH) + + @result_bundle_path.setter + def result_bundle_path(self, value: str) -> None: + """ + Specify the path to the result bundle path as xcodebuild argument for + WebDriverAgent build under a security flag. WebDriverAgent process must + start/stop every time to pick up changed value of this property. + Specifying useNewWDA to true may help there. Please read 'man xcodebuild' + for more details. + """ + self.set_capability(RESULT_BUNDLE_PATH, value) diff --git a/appium/options/ios/xcuitest/wda/screenshot_quality_option.py b/appium/options/ios/xcuitest/wda/screenshot_quality_option.py new file mode 100644 index 00000000..d808db9d --- /dev/null +++ b/appium/options/ios/xcuitest/wda/screenshot_quality_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SCREENSHOT_QUALITY = 'screenshotQuality' + + +class ScreenshotQualityOption(SupportsCapabilities): + @property + def screenshot_quality(self) -> Optional[int]: + """ + Screenshot quality value. + """ + return self.get_capability(SCREENSHOT_QUALITY) + + @screenshot_quality.setter + def screenshot_quality(self, value: int) -> None: + """ + Changes the quality of phone display screenshots following + xctest/xctimagequality Default value is 1. 0 is the highest and + 2 is the lowest quality. You can also change it via settings + command. 0 might cause OutOfMemory crash on high-resolution + devices like iPad Pro. + """ + self.set_capability(SCREENSHOT_QUALITY, value) diff --git a/appium/options/ios/xcuitest/wda/should_terminate_app_option.py b/appium/options/ios/xcuitest/wda/should_terminate_app_option.py new file mode 100644 index 00000000..8550627c --- /dev/null +++ b/appium/options/ios/xcuitest/wda/should_terminate_app_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SHOULD_TERMINATE_APP = 'shouldTerminateApp' + + +class ShouldTerminateAppOption(SupportsCapabilities): + @property + def should_terminate_app(self) -> Optional[bool]: + """ + Whether to enforce app termination on session quit. + """ + return self.get_capability(SHOULD_TERMINATE_APP) + + @should_terminate_app.setter + def should_terminate_app(self, value: bool) -> None: + """ + Specify if the app should be terminated on session end. + This capability only has an effect if an application identifier + has been passed to the test session (either explicitly, + by setting bundleId, or implicitly, by providing app). + Default is true unless noReset capability is set to true. + """ + self.set_capability(SHOULD_TERMINATE_APP, value) diff --git a/appium/options/ios/xcuitest/wda/should_use_singleton_test_manager_option.py b/appium/options/ios/xcuitest/wda/should_use_singleton_test_manager_option.py new file mode 100644 index 00000000..56923681 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/should_use_singleton_test_manager_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SHOULD_USE_SINGLETON_TEST_MANAGER = 'shouldUseSingletonTestManager' + + +class ShouldUseSingletonTestManagerOption(SupportsCapabilities): + @property + def should_use_singleton_test_manager(self) -> Optional[bool]: + """ + Whether to use the default proxy for test management within WebDriverAgent. + """ + return self.get_capability(SHOULD_USE_SINGLETON_TEST_MANAGER) + + @should_use_singleton_test_manager.setter + def should_use_singleton_test_manager(self, value: bool) -> None: + """ + Use default proxy for test management within WebDriverAgent. Setting this to false + sometimes helps with socket hangup problems. Defaults to true. + """ + self.set_capability(SHOULD_USE_SINGLETON_TEST_MANAGER, value) diff --git a/appium/options/ios/xcuitest/wda/show_xcode_log_option.py b/appium/options/ios/xcuitest/wda/show_xcode_log_option.py new file mode 100644 index 00000000..0e435b71 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/show_xcode_log_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SHOW_XCODE_LOG = 'showXcodeLog' + + +class ShowXcodeLogOption(SupportsCapabilities): + @property + def show_xcode_log(self) -> Optional[bool]: + """ + Whether to display the output of the Xcode command used to run the tests. + """ + return self.get_capability(SHOW_XCODE_LOG) + + @show_xcode_log.setter + def show_xcode_log(self, value: bool) -> None: + """ + Whether to display the output of the Xcode command used to run the tests in + server logs. If this is true, there will be lots of extra logging at startup. + Defaults to false. + """ + self.set_capability(SHOW_XCODE_LOG, value) diff --git a/appium/options/ios/xcuitest/wda/simple_is_visible_check_option.py b/appium/options/ios/xcuitest/wda/simple_is_visible_check_option.py new file mode 100644 index 00000000..694ed574 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/simple_is_visible_check_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SIMPLE_IS_VISIBLE_CHECK = 'simpleIsVisibleCheck' + + +class SimpleIsVisibleCheckOption(SupportsCapabilities): + @property + def simple_is_visible_check(self) -> Optional[bool]: + """ + Whether to use native methods for determining visibility of elements. + """ + return self.get_capability(SIMPLE_IS_VISIBLE_CHECK) + + @simple_is_visible_check.setter + def simple_is_visible_check(self, value: bool) -> None: + """ + Use native methods for determining visibility of elements. + In some cases this takes a long time. Setting this capability to false will + cause the system to use the position and size of elements to make sure they + are visible on the screen. This can, however, lead to false results in some + situations. Defaults to false, except iOS 9.3, where it defaults to true. + """ + self.set_capability(SIMPLE_IS_VISIBLE_CHECK, value) diff --git a/appium/options/ios/xcuitest/wda/updated_wda_bundle_id_option.py b/appium/options/ios/xcuitest/wda/updated_wda_bundle_id_option.py new file mode 100644 index 00000000..b799c949 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/updated_wda_bundle_id_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +UPDATED_WDA_BUNDLE_ID = 'updatedWDABundleId' + + +class UpdatedWdaBundleIdOption(SupportsCapabilities): + @property + def updated_wda_bundle_id(self) -> Optional[str]: + """ + WDA bundle identifier. + """ + return self.get_capability(UPDATED_WDA_BUNDLE_ID) + + @updated_wda_bundle_id.setter + def updated_wda_bundle_id(self, value: str) -> None: + """ + Bundle id to update WDA to before building and launching on real devices. + This bundle id must be associated with a valid provisioning profile. + """ + self.set_capability(UPDATED_WDA_BUNDLE_ID, value) diff --git a/appium/options/ios/xcuitest/wda/use_native_caching_strategy_option.py b/appium/options/ios/xcuitest/wda/use_native_caching_strategy_option.py new file mode 100644 index 00000000..a3e30de1 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/use_native_caching_strategy_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +USE_NATIVE_CACHING_STRATEGY = 'useNativeCachingStrategy' + + +class UseNativeCachingStrategyOption(SupportsCapabilities): + @property + def use_native_caching_strategy(self) -> Optional[bool]: + """ + Whether to use the native caching strategy. + """ + return self.get_capability(USE_NATIVE_CACHING_STRATEGY) + + @use_native_caching_strategy.setter + def use_native_caching_strategy(self, value: bool) -> None: + """ + Set this capability to false in order to use the custom elements caching + strategy. This might help to avoid stale element exception on property + change. By default, the native XCTest cache resolution is used (true) + for all native locators (e.g. all, but xpath). + """ + self.set_capability(USE_NATIVE_CACHING_STRATEGY, value) diff --git a/appium/options/ios/xcuitest/wda/use_new_wda_option.py b/appium/options/ios/xcuitest/wda/use_new_wda_option.py new file mode 100644 index 00000000..f52684a6 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/use_new_wda_option.py @@ -0,0 +1,51 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +USE_NEW_WDA = 'useNewWDA' + + +class UseNewWdaOption(SupportsCapabilities): + @property + def use_new_wda(self) -> Optional[bool]: + """ + Whether whether to uninstall of any existing WebDriverAgent app + on the device under test. + """ + return self.get_capability(USE_NEW_WDA) + + @use_new_wda.setter + def use_new_wda(self, value: bool) -> None: + """ + If true, forces uninstall of any existing WebDriverAgent app on device. + Set it to true if you want to apply different startup options for WebDriverAgent + for each session. Although, it is only guaranteed to work stable on Simulator. + Real devices require WebDriverAgent client to run for as long as possible without + reinstall/restart to avoid issues like + https://github.com/facebook/WebDriverAgent/issues/507. The false value + (the default behaviour since driver version 2.35.0) will try to + detect currently running WDA listener executed by previous testing session(s) + and reuse it if possible, which is highly recommended for real device testing + and to speed up suites of multiple tests in general. A new WDA session will be + triggered at the default URL (http://localhost:8100) if WDA is not listening and + webDriverAgentUrl capability is not set. The negative/unset value of useNewWDA + capability has no effect prior to xcuitest driver version 2.35.0. + """ + self.set_capability(USE_NEW_WDA, value) diff --git a/appium/options/ios/xcuitest/wda/use_prebuilt_wda_option.py b/appium/options/ios/xcuitest/wda/use_prebuilt_wda_option.py new file mode 100644 index 00000000..76fd5163 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/use_prebuilt_wda_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +USE_PREBUILT_WDA = 'usePrebuiltWDA' + + +class UsePrebuiltWdaOption(SupportsCapabilities): + @property + def use_prebuilt_wda(self) -> Optional[bool]: + """ + Whether to skip the build phase of running the WDA app. + """ + return self.get_capability(USE_PREBUILT_WDA) + + @use_prebuilt_wda.setter + def use_prebuilt_wda(self, value: bool) -> None: + """ + Skips the build phase of running the WDA app. Building is then the responsibility + of the user. Only works for Xcode 8+. Defaults to false. + """ + self.set_capability(USE_PREBUILT_WDA, value) diff --git a/appium/options/ios/xcuitest/wda/use_preinstalled_wda_option.py b/appium/options/ios/xcuitest/wda/use_preinstalled_wda_option.py new file mode 100644 index 00000000..547d87f9 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/use_preinstalled_wda_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +USE_PREINSTALLED_WDA = 'usePreinstalledWDA' + + +class UsePreinstalledWdaOption(SupportsCapabilities): + @property + def use_preinstalled_wda(self) -> Optional[bool]: + """ + Whether to use a preinstalled WebDriverAgent. + """ + return self.get_capability(USE_PREINSTALLED_WDA) + + @use_preinstalled_wda.setter + def use_preinstalled_wda(self, value: bool) -> None: + """ + Whether to use a preinstalled WebDriverAgent. If true, Appium will not + build and install the WebDriverAgent, but will use an existing one. + Defaults to false. + """ + self.set_capability(USE_PREINSTALLED_WDA, value) diff --git a/appium/options/ios/xcuitest/wda/use_simple_build_test_option.py b/appium/options/ios/xcuitest/wda/use_simple_build_test_option.py new file mode 100644 index 00000000..516e08cb --- /dev/null +++ b/appium/options/ios/xcuitest/wda/use_simple_build_test_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +USE_SIMPLE_BUILD_TEST = 'useSimpleBuildTest' + + +class UseSimpleBuildTestOption(SupportsCapabilities): + @property + def use_simple_build_test(self) -> Optional[bool]: + """ + Whether to enforce app termination on session quit. + """ + return self.get_capability(USE_SIMPLE_BUILD_TEST) + + @use_simple_build_test.setter + def use_simple_build_test(self, value: bool) -> None: + """ + Build with 'build' and run test with 'test' in xcodebuild for all Xcode versions if + this is true, or build with 'build-for-testing' and run tests with + 'test-without-building' for over Xcode 8 if this is false. Defaults to false. + """ + self.set_capability(USE_SIMPLE_BUILD_TEST, value) diff --git a/appium/options/ios/xcuitest/wda/use_xctestrun_file_option.py b/appium/options/ios/xcuitest/wda/use_xctestrun_file_option.py new file mode 100644 index 00000000..0d1a3c64 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/use_xctestrun_file_option.py @@ -0,0 +1,49 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +USE_XCTESTRUN_FILE = 'useXctestrunFile' + + +class UseXctestrunFileOption(SupportsCapabilities): + @property + def use_xctestrun_file(self) -> Optional[bool]: + """ + Whether to use of .xctestrun file to launch WDA. + """ + return self.get_capability(USE_XCTESTRUN_FILE) + + @use_xctestrun_file.setter + def use_xctestrun_file(self, value: bool) -> None: + """ + Use Xctestrun file to launch WDA. It will search for such file in bootstrapPath. + Expected name of file is WebDriverAgentRunner_iphoneos<sdkVersion>-arm64.xctestrun for + real device and WebDriverAgentRunner_iphonesimulator<sdkVersion>-x86_64.xctestrun for + simulator. One can do build-for-testing for WebDriverAgent project for simulator and + real device and then you will see Product Folder like this and you need to copy content + of this folder at bootstrapPath location. Since this capability expects that you have + already built WDA project, it neither checks whether you have necessary dependencies to + build WDA nor will it try to build project. Defaults to false. Tips: Xcodebuild builds for the + target platform version. We'd recommend you to build with minimal OS version which you'd + like to run as the original WDA module. e.g. If you build WDA for 12.2, the module cannot + run on iOS 11.4 because of loading some module error on simulator. A module built with 11.4 + can work on iOS 12.2. (This is xcodebuild's expected behaviour.) + """ + self.set_capability(USE_XCTESTRUN_FILE, value) diff --git a/appium/options/ios/xcuitest/wda/wait_for_idle_timeout_option.py b/appium/options/ios/xcuitest/wda/wait_for_idle_timeout_option.py new file mode 100644 index 00000000..ff994d43 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/wait_for_idle_timeout_option.py @@ -0,0 +1,45 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WAIT_FOR_IDLE_TIMEOUT = 'waitForIdleTimeout' + + +class WaitForIdleTimeoutOption(SupportsCapabilities): + @property + def wait_for_idle_timeout(self) -> Optional[timedelta]: + """ + Maximum timeout to wait until WDA responds to HTTP requests. + """ + value = self.get_capability(WAIT_FOR_IDLE_TIMEOUT) + return None if value is None else timedelta(seconds=value) + + @wait_for_idle_timeout.setter + def wait_for_idle_timeout(self, value: Union[timedelta, float]) -> None: + """ + The time to wait until the application under test is idling. + XCTest requires the app's main thread to be idling in order to execute any action on it, + so WDA might not even start/freeze if the app under test is constantly hogging the main + thread. The default value is 10 (seconds). Setting it to zero disables idling checks completely + (not recommended) and has the same effect as setting waitForQuiescence to false. + Available since Appium 1.20.0. + """ + self.set_capability(WAIT_FOR_IDLE_TIMEOUT, value.total_seconds() if isinstance(value, timedelta) else value) diff --git a/appium/options/ios/xcuitest/wda/wait_for_quiescence_option.py b/appium/options/ios/xcuitest/wda/wait_for_quiescence_option.py new file mode 100644 index 00000000..b8be7f32 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/wait_for_quiescence_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WAIT_FOR_QUIESCENCE = 'waitForQuiescence' + + +class WaitForQuiescenceOption(SupportsCapabilities): + @property + def wait_for_quiescence(self) -> Optional[bool]: + """ + Whether to wait for application quiescence. + """ + return self.get_capability(WAIT_FOR_QUIESCENCE) + + @wait_for_quiescence.setter + def wait_for_quiescence(self, value: Union[timedelta, float]) -> None: + """ + It allows to turn on/off waiting for application quiescence in WebDriverAgent, + while performing queries. The default value is true. You can avoid this kind + of issues if you turn it off. Consider using waitForIdleTimeout capability + instead for this purpose since Appium 1.20.0. + """ + self.set_capability(WAIT_FOR_QUIESCENCE, value) diff --git a/appium/options/ios/xcuitest/wda/wda_base_url_option.py b/appium/options/ios/xcuitest/wda/wda_base_url_option.py new file mode 100644 index 00000000..c2ca5cd8 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/wda_base_url_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WDA_BASE_URL = 'wdaBaseUrl' + + +class WdaBaseUrlOption(SupportsCapabilities): + @property + def wda_base_url(self) -> Optional[str]: + """ + Prefix to build a custom WebDriverAgent URL. + """ + return self.get_capability(WDA_BASE_URL) + + @wda_base_url.setter + def wda_base_url(self, value: str) -> None: + """ + This value, if specified, will be used as a prefix to build a custom + WebDriverAgent url. It is different from webDriverAgentUrl, because + if the latter is set then it expects WebDriverAgent to be already + listening and skips the building phase. Defaults to http://localhost. + """ + self.set_capability(WDA_BASE_URL, value) diff --git a/appium/options/ios/xcuitest/wda/wda_connection_timeout_option.py b/appium/options/ios/xcuitest/wda/wda_connection_timeout_option.py new file mode 100644 index 00000000..e21bb183 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/wda_connection_timeout_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WDA_CONNECTION_TIMEOUT = 'wdaConnectionTimeout' + + +class WdaConnectionTimeoutOption(SupportsCapabilities): + @property + def wda_connection_timeout(self) -> Optional[timedelta]: + """ + Maximum timeout to wait until WDA responds to HTTP requests. + """ + value = self.get_capability(WDA_CONNECTION_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @wda_connection_timeout.setter + def wda_connection_timeout(self, value: Union[timedelta, int]) -> None: + """ + Connection timeout to wait for a response from WebDriverAgent. + Defaults to 240000ms. + """ + self.set_capability( + WDA_CONNECTION_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value + ) diff --git a/appium/options/ios/xcuitest/wda/wda_eventloop_idle_delay_option.py b/appium/options/ios/xcuitest/wda/wda_eventloop_idle_delay_option.py new file mode 100644 index 00000000..e957f269 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/wda_eventloop_idle_delay_option.py @@ -0,0 +1,46 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WDA_EVENTLOOP_IDLE_DELAY = 'wdaEventloopIdleDelay' + + +class WdaEventloopIdleDelayOption(SupportsCapabilities): + @property + def wda_eventloop_idle_delay(self) -> Optional[timedelta]: + """ + Event loop idle delay. + """ + value = self.get_capability(WDA_EVENTLOOP_IDLE_DELAY) + return None if value is None else timedelta(seconds=value) + + @wda_eventloop_idle_delay.setter + def wda_eventloop_idle_delay(self, value: Union[timedelta, float]) -> None: + """ + Delays the invocation of -[XCUIApplicationProcess setEventLoopHasIdled:] by the + duration specified with this capability. This can help quiescence apps + that fail to do so for no obvious reason (and creating a session fails for + that reason). This increases the time for session creation + because -[XCUIApplicationProcess setEventLoopHasIdled:] is called multiple times. + If you enable this capability start with at least 3 seconds and try increasing it, + if creating the session still fails. Defaults to 0. + """ + self.set_capability(WDA_EVENTLOOP_IDLE_DELAY, value.total_seconds() if isinstance(value, timedelta) else value) diff --git a/appium/options/ios/xcuitest/wda/wda_launch_timeout_option.py b/appium/options/ios/xcuitest/wda/wda_launch_timeout_option.py new file mode 100644 index 00000000..325b7e40 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/wda_launch_timeout_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WDA_LAUNCH_TIMEOUT = 'wdaLaunchTimeout' + + +class WdaLaunchTimeoutOption(SupportsCapabilities): + @property + def wda_launch_timeout(self) -> Optional[timedelta]: + """ + Maximum timeout to wait until WDA is listening. + """ + value = self.get_capability(WDA_LAUNCH_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @wda_launch_timeout.setter + def wda_launch_timeout(self, value: Union[timedelta, int]) -> None: + """ + Timeout to wait for WebDriverAgent to be pingable, + after its building is finished. Defaults to 60000ms. + """ + self.set_capability(WDA_LAUNCH_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/options/ios/xcuitest/wda/wda_local_port_option.py b/appium/options/ios/xcuitest/wda/wda_local_port_option.py new file mode 100644 index 00000000..1af241a3 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/wda_local_port_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WDA_LOCAL_PORT = 'wdaLocalPort' + + +class WdaLocalPortOption(SupportsCapabilities): + @property + def wda_local_port(self) -> Optional[int]: + """ + Local port number where the WDA traffic is being forwarded. + """ + return self.get_capability(WDA_LOCAL_PORT) + + @wda_local_port.setter + def wda_local_port(self, value: int) -> None: + """ + This value, if specified, will be used to forward traffic from + Mac host to real ios devices over USB. + Default value is the same as the port number used by WDA on + the device under test (8100). + """ + self.set_capability(WDA_LOCAL_PORT, value) diff --git a/appium/options/ios/xcuitest/wda/wda_startup_retries_option.py b/appium/options/ios/xcuitest/wda/wda_startup_retries_option.py new file mode 100644 index 00000000..a7099962 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/wda_startup_retries_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WDA_STARTUP_RETRIES = 'wdaStartupRetries' + + +class WdaStartupRetriesOption(SupportsCapabilities): + @property + def wda_startup_retries(self) -> Optional[int]: + """ + Number of retries before to fail WDA deployment. + """ + return self.get_capability(WDA_STARTUP_RETRIES) + + @wda_startup_retries.setter + def wda_startup_retries(self, value: int) -> None: + """ + Number of times to try to build and launch WebDriverAgent onto the device. + Defaults to 2. + """ + self.set_capability(WDA_STARTUP_RETRIES, value) diff --git a/appium/options/ios/xcuitest/wda/wda_startup_retry_interval_option.py b/appium/options/ios/xcuitest/wda/wda_startup_retry_interval_option.py new file mode 100644 index 00000000..99d89daa --- /dev/null +++ b/appium/options/ios/xcuitest/wda/wda_startup_retry_interval_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WDA_STARTUP_RETRY_INTERVAL = 'wdaStartupRetryInterval' + + +class WdaStartupRetryIntervalOption(SupportsCapabilities): + @property + def wda_startup_retry_interval(self) -> Optional[timedelta]: + """ + Interval to wait between tries to build and launch WebDriverAgent. + """ + value = self.get_capability(WDA_STARTUP_RETRY_INTERVAL) + return None if value is None else timedelta(milliseconds=value) + + @wda_startup_retry_interval.setter + def wda_startup_retry_interval(self, value: Union[timedelta, int]) -> None: + """ + Time interval to wait between tries to build and launch WebDriverAgent. + Defaults to 10000ms. + """ + self.set_capability( + WDA_STARTUP_RETRY_INTERVAL, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value + ) diff --git a/appium/options/ios/xcuitest/wda/web_driver_agent_url_option.py b/appium/options/ios/xcuitest/wda/web_driver_agent_url_option.py new file mode 100644 index 00000000..5dd89f2c --- /dev/null +++ b/appium/options/ios/xcuitest/wda/web_driver_agent_url_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WEB_DRIVER_AGENT_URL = 'webDriverAgentUrl' + + +class WebDriverAgentUrlOption(SupportsCapabilities): + @property + def web_driver_agent_url(self) -> Optional[str]: + """ + WedDriverAgent URL. + """ + return self.get_capability(WEB_DRIVER_AGENT_URL) + + @web_driver_agent_url.setter + def web_driver_agent_url(self, value: str) -> None: + """ + If provided, Appium will connect to an existing WebDriverAgent + instance at this URL instead of starting a new one. + """ + self.set_capability(WEB_DRIVER_AGENT_URL, value) diff --git a/appium/options/ios/xcuitest/wda/xcode_org_id_option.py b/appium/options/ios/xcuitest/wda/xcode_org_id_option.py new file mode 100644 index 00000000..d5c51793 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/xcode_org_id_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +XCODE_ORG_ID = 'xcodeOrgId' + + +class XcodeOrgIdOption(SupportsCapabilities): + @property + def xcode_org_id(self) -> Optional[str]: + """ + Signing certificate organization id for WebDriverAgent compilation. + """ + return self.get_capability(XCODE_ORG_ID) + + @xcode_org_id.setter + def xcode_org_id(self, value: str) -> None: + """ + Provides a signing certificate organization id for WebDriverAgent compilation. + If signing id is not provided then it defaults to "iPhone Developer" + """ + self.set_capability(XCODE_ORG_ID, value) diff --git a/appium/options/ios/xcuitest/wda/xcode_signing_id_option.py b/appium/options/ios/xcuitest/wda/xcode_signing_id_option.py new file mode 100644 index 00000000..08ba1855 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/xcode_signing_id_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +XCODE_SIGNING_ID = 'xcodeSigningId' + + +class XcodeSigningIdOption(SupportsCapabilities): + @property + def xcode_signing_id(self) -> Optional[str]: + """ + Signing certificate for WebDriverAgent compilation. + """ + return self.get_capability(XCODE_SIGNING_ID) + + @xcode_signing_id.setter + def xcode_signing_id(self, value: str) -> None: + """ + Provides a signing certificate for WebDriverAgent compilation. + If signing id is not provided then it defaults to "iPhone Developer" + """ + self.set_capability(XCODE_SIGNING_ID, value) diff --git a/appium/options/ios/xcuitest/webview/__init__.py b/appium/options/ios/xcuitest/webview/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/ios/xcuitest/webview/absolute_web_locations_option.py b/appium/options/ios/xcuitest/webview/absolute_web_locations_option.py new file mode 100644 index 00000000..114c737d --- /dev/null +++ b/appium/options/ios/xcuitest/webview/absolute_web_locations_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ABSOLUTE_WEB_LOCATIONS = 'absoluteWebLocations' + + +class AbsoluteWebLocationsOption(SupportsCapabilities): + @property + def absolute_web_locations(self) -> Optional[bool]: + """ + Whether Get Element Location returns coordinates + relative to the page origin for web view elements. + """ + return self.get_capability(ABSOLUTE_WEB_LOCATIONS) + + @absolute_web_locations.setter + def absolute_web_locations(self, value: bool) -> None: + """ + This capability will direct the Get Element Location command, when used + within webviews, to return coordinates which are relative to the origin of + the page, rather than relative to the current scroll offset. This capability + has no effect outside of webviews. Defaults to false. + """ + self.set_capability(ABSOLUTE_WEB_LOCATIONS, value) diff --git a/appium/options/ios/xcuitest/webview/additional_webview_bundle_ids_option.py b/appium/options/ios/xcuitest/webview/additional_webview_bundle_ids_option.py new file mode 100644 index 00000000..6546c548 --- /dev/null +++ b/appium/options/ios/xcuitest/webview/additional_webview_bundle_ids_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import List, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ADDITIONAL_WEBVIEW_BUNDLE_IDS = 'additionalWebviewBundleIds' + + +class AdditionalWebviewBundleIdsOption(SupportsCapabilities): + @property + def additional_webview_bundle_ids(self) -> Optional[List[str]]: + """ + Array of possible bundle identifiers for webviews. + """ + return self.get_capability(ADDITIONAL_WEBVIEW_BUNDLE_IDS) + + @additional_webview_bundle_ids.setter + def additional_webview_bundle_ids(self, value: List[str]) -> None: + """ + Array of possible bundle identifiers for webviews. This is sometimes + necessary if the Web Inspector is found to be returning a modified + bundle identifier for the app. Defaults to []. + """ + self.set_capability(ADDITIONAL_WEBVIEW_BUNDLE_IDS, value) diff --git a/appium/options/ios/xcuitest/webview/enable_async_execute_from_https_option.py b/appium/options/ios/xcuitest/webview/enable_async_execute_from_https_option.py new file mode 100644 index 00000000..ae2a3dcc --- /dev/null +++ b/appium/options/ios/xcuitest/webview/enable_async_execute_from_https_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ENABLE_ASYNC_EXECUTE_FROM_HTTPS = 'enableAsyncExecuteFromHttps' + + +class EnableAsyncExecuteFromHttpsOption(SupportsCapabilities): + @property + def enable_async_execute_from_https(self) -> Optional[bool]: + """ + Whether to allow simulators to execute async JavaScript on pages using HTTPS. + """ + return self.get_capability(ENABLE_ASYNC_EXECUTE_FROM_HTTPS) + + @enable_async_execute_from_https.setter + def enable_async_execute_from_https(self, value: bool) -> None: + """ + Capability to allow simulators to execute asynchronous JavaScript + on pages using HTTPS. Defaults to false. + """ + self.set_capability(ENABLE_ASYNC_EXECUTE_FROM_HTTPS, value) diff --git a/appium/options/ios/xcuitest/webview/full_context_list_option.py b/appium/options/ios/xcuitest/webview/full_context_list_option.py new file mode 100644 index 00000000..7bdd3102 --- /dev/null +++ b/appium/options/ios/xcuitest/webview/full_context_list_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +FULL_CONTEXT_LIST = 'fullContextList' + + +class FullContextListOption(SupportsCapabilities): + @property + def full_context_list(self) -> Optional[bool]: + """ + Whether to return the detailed information on contexts for the get available + context command. + """ + return self.get_capability(FULL_CONTEXT_LIST) + + @full_context_list.setter + def full_context_list(self, value: bool) -> None: + """ + Sets to return the detailed information on contexts for the get available + context command. If this capability is enabled, then each item in the returned + contexts list would additionally include WebView title, full URL and the bundle + identifier. Defaults to false. + """ + self.set_capability(FULL_CONTEXT_LIST, value) diff --git a/appium/options/ios/xcuitest/webview/include_safari_in_webviews_option.py b/appium/options/ios/xcuitest/webview/include_safari_in_webviews_option.py new file mode 100644 index 00000000..c21cb141 --- /dev/null +++ b/appium/options/ios/xcuitest/webview/include_safari_in_webviews_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +INCLUDE_SAFARI_IN_WEBVIEWS = 'includeSafariInWebviews' + + +class IncludeSafariInWebviewsOption(SupportsCapabilities): + @property + def include_safari_in_webviews(self) -> Optional[bool]: + """ + Whether to add Safari web views to the list of contexts available + during a native/webview app test. + """ + return self.get_capability(INCLUDE_SAFARI_IN_WEBVIEWS) + + @include_safari_in_webviews.setter + def include_safari_in_webviews(self, value: bool) -> None: + """ + Add Safari web contexts to the list of contexts available during a + native/webview app test. This is useful if the test opens Safari and + needs to be able to interact with it. Defaults to false. + """ + self.set_capability(INCLUDE_SAFARI_IN_WEBVIEWS, value) diff --git a/appium/options/ios/xcuitest/webview/native_web_tap_option.py b/appium/options/ios/xcuitest/webview/native_web_tap_option.py new file mode 100644 index 00000000..02a40b98 --- /dev/null +++ b/appium/options/ios/xcuitest/webview/native_web_tap_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +NATIVE_WEB_TAP = 'nativeWebTap' + + +class NativeWebTapOption(SupportsCapabilities): + @property + def native_web_tap(self) -> Optional[bool]: + """ + Whether to enable native taps in web view mode. + """ + return self.get_capability(NATIVE_WEB_TAP) + + @native_web_tap.setter + def native_web_tap(self, value: bool) -> None: + """ + Enable native, non-javascript-based taps being in web context mode. Defaults + to false. Warning: sometimes the preciseness of native taps could be broken, + because there is no reliable way to map web element coordinates to native ones. + """ + self.set_capability(NATIVE_WEB_TAP, value) diff --git a/appium/options/ios/xcuitest/webview/safari_garbage_collect_option.py b/appium/options/ios/xcuitest/webview/safari_garbage_collect_option.py new file mode 100644 index 00000000..63aefc4c --- /dev/null +++ b/appium/options/ios/xcuitest/webview/safari_garbage_collect_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SAFARI_GARBAGE_COLLECT = 'safariGarbageCollect' + + +class SafariGarbageCollectOption(SupportsCapabilities): + @property + def safari_garbage_collect(self) -> Optional[bool]: + """ + Whether to turn on garbage collection when executing scripts on Safari. + """ + return self.get_capability(SAFARI_GARBAGE_COLLECT) + + @safari_garbage_collect.setter + def safari_garbage_collect(self, value: bool) -> None: + """ + Turns on/off Web Inspector garbage collection when executing scripts on Safari. + Turning on may improve performance. Defaults to false. + """ + self.set_capability(SAFARI_GARBAGE_COLLECT, value) diff --git a/appium/options/ios/xcuitest/webview/safari_ignore_fraud_warning_option.py b/appium/options/ios/xcuitest/webview/safari_ignore_fraud_warning_option.py new file mode 100644 index 00000000..c600285d --- /dev/null +++ b/appium/options/ios/xcuitest/webview/safari_ignore_fraud_warning_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SAFARI_IGNORE_FRAUD_WARNING = 'safariIgnoreFraudWarning' + + +class SafariIgnoreFraudWarningOption(SupportsCapabilities): + @property + def safari_ignore_fraud_warning(self) -> Optional[bool]: + """ + Whether to prevent Safari from showing a fraudulent website warning. + """ + return self.get_capability(SAFARI_IGNORE_FRAUD_WARNING) + + @safari_ignore_fraud_warning.setter + def safari_ignore_fraud_warning(self, value: bool) -> None: + """ + Prevent Safari from showing a fraudulent website warning. + Default keeps current sim setting.. + """ + self.set_capability(SAFARI_IGNORE_FRAUD_WARNING, value) diff --git a/appium/options/ios/xcuitest/webview/safari_ignore_web_hostnames_option.py b/appium/options/ios/xcuitest/webview/safari_ignore_web_hostnames_option.py new file mode 100644 index 00000000..9f48f8cf --- /dev/null +++ b/appium/options/ios/xcuitest/webview/safari_ignore_web_hostnames_option.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SAFARI_IGNORE_WEB_HOSTNAMES = 'safariIgnoreWebHostnames' + + +class SafariIgnoreWebHostnamesOption(SupportsCapabilities): + @property + def safari_ignore_web_hostnames(self) -> Optional[str]: + """ + Comma-separated list of host names to be ignored. + """ + return self.get_capability(SAFARI_IGNORE_WEB_HOSTNAMES) + + @safari_ignore_web_hostnames.setter + def safari_ignore_web_hostnames(self, value: str) -> None: + """ + Provide a list of hostnames (comma-separated) that the Safari automation + tools should ignore. This is to provide a workaround to prevent a webkit + bug where the web context is unintentionally changed to a 3rd party website + and the test gets stuck. The common culprits are search engines (yahoo, bing, + google) and about:blank. + """ + self.set_capability(SAFARI_IGNORE_WEB_HOSTNAMES, value) diff --git a/appium/options/ios/xcuitest/webview/safari_initial_url_option.py b/appium/options/ios/xcuitest/webview/safari_initial_url_option.py new file mode 100644 index 00000000..79dc14fe --- /dev/null +++ b/appium/options/ios/xcuitest/webview/safari_initial_url_option.py @@ -0,0 +1,38 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SAFARI_INITIAL_URL = 'safariInitialUrl' + + +class SafariInitialUrlOption(SupportsCapabilities): + @property + def safari_initial_url(self) -> Optional[str]: + """ + The initial safari url. + """ + return self.get_capability(SAFARI_INITIAL_URL) + + @safari_initial_url.setter + def safari_initial_url(self, value: str) -> None: + """ + Set initial safari url, default is a local welcome page. + """ + self.set_capability(SAFARI_INITIAL_URL, value) diff --git a/appium/options/ios/xcuitest/webview/safari_log_all_communication_hex_dump_option.py b/appium/options/ios/xcuitest/webview/safari_log_all_communication_hex_dump_option.py new file mode 100644 index 00000000..03b2e28c --- /dev/null +++ b/appium/options/ios/xcuitest/webview/safari_log_all_communication_hex_dump_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SAFARI_LOG_ALL_COMMUNICATION_HEX_DUMP = 'safariLogAllCommunicationHexDump' + + +class SafariLogAllCommunicationHexDumpOption(SupportsCapabilities): + @property + def safari_log_all_communication_hex_dump(self) -> Optional[bool]: + """ + Whether to log of plists sent to and received from the Web Inspector + in hex dump format. + """ + return self.get_capability(SAFARI_LOG_ALL_COMMUNICATION_HEX_DUMP) + + @safari_log_all_communication_hex_dump.setter + def safari_log_all_communication_hex_dump(self, value: bool) -> None: + """ + Log all communication sent to and received from the Web Inspector, as raw + hex dump and printable characters. This logging is done before any data + manipulation, and so can elucidate some communication issues. Like + appium:safariLogAllCommunication, this can produce a lot of data in some cases, + so it is recommended to be used only when necessary. Defaults to false. + """ + self.set_capability(SAFARI_LOG_ALL_COMMUNICATION_HEX_DUMP, value) diff --git a/appium/options/ios/xcuitest/webview/safari_log_all_communication_option.py b/appium/options/ios/xcuitest/webview/safari_log_all_communication_option.py new file mode 100644 index 00000000..b022dba3 --- /dev/null +++ b/appium/options/ios/xcuitest/webview/safari_log_all_communication_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SAFARI_LOG_ALL_COMMUNICATION = 'safariLogAllCommunication' + + +class SafariLogAllCommunicationOption(SupportsCapabilities): + @property + def safari_log_all_communication(self) -> Optional[bool]: + """ + Whether to log of plists sent to and received from the Web Inspector. + """ + return self.get_capability(SAFARI_LOG_ALL_COMMUNICATION) + + @safari_log_all_communication.setter + def safari_log_all_communication(self, value: bool) -> None: + """ + Log all plists sent to and received from the Web Inspector, as plain text. + For some operations this can be a lot of data, so it is recommended to + be used only when necessary. Defaults to false. + """ + self.set_capability(SAFARI_LOG_ALL_COMMUNICATION, value) diff --git a/appium/options/ios/xcuitest/webview/safari_open_links_in_background_option.py b/appium/options/ios/xcuitest/webview/safari_open_links_in_background_option.py new file mode 100644 index 00000000..2ebb4801 --- /dev/null +++ b/appium/options/ios/xcuitest/webview/safari_open_links_in_background_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SAFARI_OPEN_LINKS_IN_BACKGROUND = 'safariOpenLinksInBackground' + + +class SafariOpenLinksInBackgroundOption(SupportsCapabilities): + @property + def safari_open_links_in_background(self) -> Optional[bool]: + """ + Whether Safari should allow links to open in new windows. + """ + return self.get_capability(SAFARI_OPEN_LINKS_IN_BACKGROUND) + + @safari_open_links_in_background.setter + def safari_open_links_in_background(self, value: bool) -> None: + """ + Whether Safari should allow links to open in new windows. + Default keeps current sim setting. + """ + self.set_capability(SAFARI_OPEN_LINKS_IN_BACKGROUND, value) diff --git a/appium/options/ios/xcuitest/webview/safari_socket_chunk_size_option.py b/appium/options/ios/xcuitest/webview/safari_socket_chunk_size_option.py new file mode 100644 index 00000000..c33ea61d --- /dev/null +++ b/appium/options/ios/xcuitest/webview/safari_socket_chunk_size_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SAFARI_SOCKET_CHUNK_SIZE = 'safariSocketChunkSize' + + +class SafariSocketChunkSizeOption(SupportsCapabilities): + @property + def safari_socket_chunk_size(self) -> Optional[int]: + """ + Get the size of a single remote debugger socket chunk. + """ + return self.get_capability(SAFARI_SOCKET_CHUNK_SIZE) + + @safari_socket_chunk_size.setter + def safari_socket_chunk_size(self, value: int) -> None: + """ + The size, in bytes, of the data to be sent to the Web Inspector on + iOS 11+ real devices. Some devices hang when sending large amounts of + data to the Web Inspector, and breaking them into smaller parts can be + helpful in those cases. Defaults to 16384 (also the maximum possible). + """ + self.set_capability(SAFARI_SOCKET_CHUNK_SIZE, value) diff --git a/appium/options/ios/xcuitest/webview/safari_web_inspector_max_frame_length_option.py b/appium/options/ios/xcuitest/webview/safari_web_inspector_max_frame_length_option.py new file mode 100644 index 00000000..d92fb2d4 --- /dev/null +++ b/appium/options/ios/xcuitest/webview/safari_web_inspector_max_frame_length_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SAFARI_WEB_INSPECTOR_MAX_FRAME_LENGTH = 'safariWebInspectorMaxFrameLength' + + +class SafariWebInspectorMaxFrameLengthOption(SupportsCapabilities): + @property + def safari_web_inspector_max_frame_length(self) -> Optional[int]: + """ + Maximum size in bytes of a single data frame for the Web Inspector. + """ + return self.get_capability(SAFARI_WEB_INSPECTOR_MAX_FRAME_LENGTH) + + @safari_web_inspector_max_frame_length.setter + def safari_web_inspector_max_frame_length(self, value: int) -> None: + """ + The maximum size in bytes of a single data frame for the Web Inspector. + Too high values could introduce slowness and/or memory leaks. + Too low values could introduce possible buffer overflow exceptions. + Defaults to 20MiB (20*1024*1024). + """ + self.set_capability(SAFARI_WEB_INSPECTOR_MAX_FRAME_LENGTH, value) diff --git a/appium/options/ios/xcuitest/webview/webkit_response_timeout_option.py b/appium/options/ios/xcuitest/webview/webkit_response_timeout_option.py new file mode 100644 index 00000000..1e274bf0 --- /dev/null +++ b/appium/options/ios/xcuitest/webview/webkit_response_timeout_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WEBKIT_RESPONSE_TIMEOUT = 'webkitResponseTimeout' + + +class WebkitResponseTimeoutOption(SupportsCapabilities): + @property + def webkit_response_timeout(self) -> Optional[timedelta]: + """ + Time to wait for a response from WebKit in a Safari session. + """ + value = self.get_capability(WEBKIT_RESPONSE_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @webkit_response_timeout.setter + def webkit_response_timeout(self, value: Union[timedelta, int]) -> None: + """ + (Real device only) Set the time to wait for a response from + WebKit in a Safari session. Defaults to 5000ms. + """ + self.set_capability( + WEBKIT_RESPONSE_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value + ) diff --git a/appium/options/ios/xcuitest/webview/webview_connect_retries_option.py b/appium/options/ios/xcuitest/webview/webview_connect_retries_option.py new file mode 100644 index 00000000..7792f15b --- /dev/null +++ b/appium/options/ios/xcuitest/webview/webview_connect_retries_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WEBVIEW_CONNECT_RETRIES = 'webviewConnectRetries' + + +class WebviewConnectRetriesOption(SupportsCapabilities): + @property + def webview_connect_retries(self) -> Optional[int]: + """ + Number of retries to send connection message to remote debugger, + to get a webview. + """ + return self.get_capability(WEBVIEW_CONNECT_RETRIES) + + @webview_connect_retries.setter + def webview_connect_retries(self, value: int) -> None: + """ + Number of times to send connection message to remote debugger, + to get a webview. Default: 8. + """ + self.set_capability(WEBVIEW_CONNECT_RETRIES, value) diff --git a/appium/options/ios/xcuitest/webview/webview_connect_timeout_option.py b/appium/options/ios/xcuitest/webview/webview_connect_timeout_option.py new file mode 100644 index 00000000..9fa74202 --- /dev/null +++ b/appium/options/ios/xcuitest/webview/webview_connect_timeout_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WEBVIEW_CONNECT_TIMEOUT = 'webviewConnectTimeout' + + +class WebviewConnectTimeoutOption(SupportsCapabilities): + @property + def webview_connect_timeout(self) -> Optional[timedelta]: + """ + Timeout to wait for the initial presence of webviews. + """ + value = self.get_capability(WEBVIEW_CONNECT_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @webview_connect_timeout.setter + def webview_connect_timeout(self, value: Union[timedelta, int]) -> None: + """ + The time to wait for the initial presence of webviews in + MobileSafari or hybrid apps. Defaults to 0ms. + """ + self.set_capability( + WEBVIEW_CONNECT_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value + ) diff --git a/appium/options/mac/__init__.py b/appium/options/mac/__init__.py new file mode 100644 index 00000000..41a091df --- /dev/null +++ b/appium/options/mac/__init__.py @@ -0,0 +1 @@ +from .mac2.base import Mac2Options diff --git a/appium/options/mac/mac2/__init__.py b/appium/options/mac/mac2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/mac/mac2/app_path_option.py b/appium/options/mac/mac2/app_path_option.py new file mode 100644 index 00000000..71506fd4 --- /dev/null +++ b/appium/options/mac/mac2/app_path_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from os import PathLike, fspath +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_PATH = 'appPath' + + +class AppPathOption(SupportsCapabilities): + @property + def app_path(self) -> Optional[str]: + """ + The path of the application to automate. + """ + return self.get_capability(APP_PATH) + + @app_path.setter + def app_path(self, value: Union[str, PathLike]) -> None: + """ + Set the path of the application to automate. + """ + self.set_capability(APP_PATH, fspath(value)) diff --git a/appium/options/mac/mac2/arguments_option.py b/appium/options/mac/mac2/arguments_option.py new file mode 100644 index 00000000..3c9a5a70 --- /dev/null +++ b/appium/options/mac/mac2/arguments_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import List, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ARGUMENTS = 'arguments' + + +class ArgumentsOption(SupportsCapabilities): + @property + def arguments(self) -> Optional[List[str]]: + """ + Array of application command line arguments. + """ + return self.get_capability(ARGUMENTS) + + @arguments.setter + def arguments(self, value: List[str]) -> None: + """ + Set the array of application command line arguments. This capability is + only going to be applied if the application is not running on session startup. + """ + self.set_capability(ARGUMENTS, value) diff --git a/appium/options/mac/mac2/base.py b/appium/options/mac/mac2/base.py new file mode 100644 index 00000000..d635f2b3 --- /dev/null +++ b/appium/options/mac/mac2/base.py @@ -0,0 +1,113 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict + +from appium.options.common.automation_name_option import AUTOMATION_NAME +from appium.options.common.base import PLATFORM_NAME, AppiumOptions +from appium.options.common.bundle_id_option import BundleIdOption +from appium.options.common.postrun_option import PostrunOption +from appium.options.common.prerun_option import PrerunOption +from appium.options.common.system_host_option import SystemHostOption +from appium.options.common.system_port_option import SystemPortOption + +from .app_path_option import AppPathOption +from .arguments_option import ArgumentsOption +from .bootstrap_root_option import BootstrapRootOption +from .environment_option import EnvironmentOption +from .server_startup_timeout_option import ServerStartupTimeoutOption +from .show_server_logs_option import ShowServerLogsOption +from .skip_app_kill_option import SkipAppKillOption +from .web_driver_agent_mac_url_option import WebDriverAgentMacUrlOption + + +class Mac2Options( + AppiumOptions, + AppPathOption, + PrerunOption, + PostrunOption, + ArgumentsOption, + BootstrapRootOption, + BundleIdOption, + EnvironmentOption, + ServerStartupTimeoutOption, + ShowServerLogsOption, + SkipAppKillOption, + SystemHostOption, + SystemPortOption, + WebDriverAgentMacUrlOption, +): + @PrerunOption.prerun.setter # type: ignore + def prerun(self, value: Dict[str, str]) -> None: + """ + A mapping containing either 'script' or 'command' key. The value of + each key must be a valid AppleScript script or command to be + executed after before Mac2Driver session is started. See + https://github.com/appium/appium-mac2-driver#applescript-commands-execution + for more details. + """ + PrerunOption.prerun.fset(self, value) # type: ignore + + @PostrunOption.postrun.setter # type: ignore + def postrun(self, value: Dict[str, str]) -> None: + """ + A mapping containing either 'script' or 'command' key. The value of + each key must be a valid AppleScript script or command to be + executed after Mac2Driver session is stopped. See + https://github.com/appium/appium-mac2-driver#applescript-commands-execution + for more details. + """ + PostrunOption.postrun.fset(self, value) # type: ignore + + @SystemPortOption.system_port.setter # type: ignore + def system_port(self, value: int) -> None: + """ + Set the number of the port for the internal server to listen on. + If not provided then Mac2Driver will use the default port 10100. + """ + SystemPortOption.system_port.fset(self, value) # type: ignore + + @SystemHostOption.system_host.setter # type: ignore + def system_host(self, value: str) -> None: + """ + Set the number of the port for the internal server to listen on. + If not provided then Mac2Driver will use the default host + address 127.0.0.1. You could set it to 0.0.0.0 to make the + server listening on all available network interfaces. + It is also possible to set the particular interface name, for example en1. + """ + SystemHostOption.system_host.fset(self, value) # type: ignore + + @BundleIdOption.bundle_id.setter # type: ignore + def bundle_id(self, value: str) -> None: + """ + Set the bundle identifier of the application to automate, for example + com.apple.TextEdit. This is an optional capability. If it is not provided + then the session will be started without an application under test + (actually, it will be Finder). If the application with the given + identifier is not installed then an error will be thrown on session + startup. If the application is already running then it will be moved to + the foreground. + """ + BundleIdOption.bundle_id.fset(self, value) # type: ignore + + @property + def default_capabilities(self) -> Dict: + return { + AUTOMATION_NAME: 'Mac2', + PLATFORM_NAME: 'Mac', + } diff --git a/appium/options/mac/mac2/bootstrap_root_option.py b/appium/options/mac/mac2/bootstrap_root_option.py new file mode 100644 index 00000000..25c53d1b --- /dev/null +++ b/appium/options/mac/mac2/bootstrap_root_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +BOOTSTRAP_ROOT = 'bootstrapRoot' + + +class BootstrapRootOption(SupportsCapabilities): + @property + def bootstrap_root(self) -> Optional[str]: + """ + The full path to WebDriverAgentMac root folder where Xcode project + of the server sources lives. + """ + return self.get_capability(BOOTSTRAP_ROOT) + + @bootstrap_root.setter + def bootstrap_root(self, value: str) -> None: + """ + Set the full path to WebDriverAgentMac root folder where Xcode project + of the server sources lives. By default, this project is located in + the same folder where the corresponding driver Node.js module lives. + """ + self.set_capability(BOOTSTRAP_ROOT, value) diff --git a/appium/options/mac/mac2/environment_option.py b/appium/options/mac/mac2/environment_option.py new file mode 100644 index 00000000..b5d72a9f --- /dev/null +++ b/appium/options/mac/mac2/environment_option.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict, Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +ENVIRONMENT = 'environment' + + +class EnvironmentOption(SupportsCapabilities): + @property + def environment(self) -> Optional[Dict[str, str]]: + """ + Application environment variables mapping. + """ + return self.get_capability(ENVIRONMENT) + + @environment.setter + def environment(self, value: Dict[str, str]) -> None: + """ + Set the dictionary of environment variables (name->value) that are going to be passed + to the application under test on top of environment variables inherited from + the parent process. This option is only going to be applied if the application + is not running on session startup. + """ + self.set_capability(ENVIRONMENT, value) diff --git a/appium/options/mac/mac2/server_startup_timeout_option.py b/appium/options/mac/mac2/server_startup_timeout_option.py new file mode 100644 index 00000000..32e13f79 --- /dev/null +++ b/appium/options/mac/mac2/server_startup_timeout_option.py @@ -0,0 +1,44 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SERVER_STARTUP_TIMEOUT = 'serverStartupTimeout' + + +class ServerStartupTimeoutOption(SupportsCapabilities): + @property + def server_startup_timeout(self) -> Optional[timedelta]: + """ + Get the timeout to wait util the WebDriverAgentMac + project is built and started. + """ + value_ms = self.get_capability(SERVER_STARTUP_TIMEOUT) + return None if value_ms is None else timedelta(milliseconds=value_ms) + + @server_startup_timeout.setter + def server_startup_timeout(self, value: Union[int, timedelta]) -> None: + """ + Set the timeout to wait util the WebDriverAgentMac + project is built and started. + """ + self.set_capability( + SERVER_STARTUP_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value + ) diff --git a/appium/options/mac/mac2/show_server_logs_option.py b/appium/options/mac/mac2/show_server_logs_option.py new file mode 100644 index 00000000..63530492 --- /dev/null +++ b/appium/options/mac/mac2/show_server_logs_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SHOW_SERVER_LOGS = 'showServerLogs' + + +class ShowServerLogsOption(SupportsCapabilities): + @property + def show_server_logs(self) -> Optional[bool]: + """ + Whether to show WDA server logs in the Appium log. + """ + return self.get_capability(SHOW_SERVER_LOGS) + + @show_server_logs.setter + def show_server_logs(self, value: bool) -> None: + """ + Set it to true in order to include xcodebuild output to the Appium + server log. false by default. + """ + self.set_capability(SHOW_SERVER_LOGS, value) diff --git a/appium/options/mac/mac2/skip_app_kill_option.py b/appium/options/mac/mac2/skip_app_kill_option.py new file mode 100644 index 00000000..adfcd19a --- /dev/null +++ b/appium/options/mac/mac2/skip_app_kill_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +SKIP_APP_KILL = 'skipAppKill' + + +class SkipAppKillOption(SupportsCapabilities): + @property + def skip_app_kill(self) -> Optional[bool]: + """ + Whether to skip the termination of the application under test. + """ + return self.get_capability(SKIP_APP_KILL) + + @skip_app_kill.setter + def skip_app_kill(self, value: bool) -> None: + """ + Set whether to skip the termination of the application under test + when the testing session quits. false by default. This capability + is only going to be applied if bundleId is set. + """ + self.set_capability(SKIP_APP_KILL, value) diff --git a/appium/options/mac/mac2/web_driver_agent_mac_url_option.py b/appium/options/mac/mac2/web_driver_agent_mac_url_option.py new file mode 100644 index 00000000..8ffab439 --- /dev/null +++ b/appium/options/mac/mac2/web_driver_agent_mac_url_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WEB_DRIVER_ARGENT_MAC_URL = 'webDriverAgentMacUrl' + + +class WebDriverAgentMacUrlOption(SupportsCapabilities): + @property + def web_driver_agent_mac_url(self) -> Optional[str]: + """ + The URL Appium will connect to an existing WebDriverAgentMac instance. + """ + return self.get_capability(WEB_DRIVER_ARGENT_MAC_URL) + + @web_driver_agent_mac_url.setter + def web_driver_agent_mac_url(self, value: str) -> None: + """ + Set the URL Appium will connect to an existing WebDriverAgentMac + instance at this URL instead of starting a new one. + """ + self.set_capability(WEB_DRIVER_ARGENT_MAC_URL, value) diff --git a/appium/options/windows/__init__.py b/appium/options/windows/__init__.py new file mode 100644 index 00000000..12d31fe3 --- /dev/null +++ b/appium/options/windows/__init__.py @@ -0,0 +1 @@ +from .windows.base import WindowsOptions diff --git a/appium/options/windows/windows/__init__.py b/appium/options/windows/windows/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/options/windows/windows/app_arguments_option.py b/appium/options/windows/windows/app_arguments_option.py new file mode 100644 index 00000000..665977d3 --- /dev/null +++ b/appium/options/windows/windows/app_arguments_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_ARGUMENTS = 'appArguments' + + +class AppArgumentsOption(SupportsCapabilities): + @property + def app_arguments(self) -> Optional[str]: + """ + Application arguments string, for example `/?`. + """ + return self.get_capability(APP_ARGUMENTS) + + @app_arguments.setter + def app_arguments(self, value: str) -> None: + """ + Set application arguments string, for example `/argone "/arg two"`. + Make sure arguments are quoted/escaped properly if necessary: + https://ss64.com/nt/syntax-esc.html + """ + self.set_capability(APP_ARGUMENTS, value) diff --git a/appium/options/windows/windows/app_top_level_window_option.py b/appium/options/windows/windows/app_top_level_window_option.py new file mode 100644 index 00000000..f573fb48 --- /dev/null +++ b/appium/options/windows/windows/app_top_level_window_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_TOP_LEVEL_WINDOW = 'appTopLevelWindow' + + +class AppTopLevelWindowOption(SupportsCapabilities): + @property + def app_top_level_window(self) -> Optional[str]: + """ + Hexadecimal handle of an existing application top level window to attach to. + """ + return self.get_capability(APP_TOP_LEVEL_WINDOW) + + @app_top_level_window.setter + def app_top_level_window(self, value: str) -> None: + """ + Set the hexadecimal handle of an existing application top level + window to attach to, for example 0x12345 (should be of string type). + Either this capability or app one must be provided on session startup. + """ + self.set_capability(APP_TOP_LEVEL_WINDOW, value) diff --git a/appium/options/windows/windows/app_working_dir_option.py b/appium/options/windows/windows/app_working_dir_option.py new file mode 100644 index 00000000..0160f89a --- /dev/null +++ b/appium/options/windows/windows/app_working_dir_option.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_WORKING_DIR = 'appWorkingDir' + + +class AppWorkingDirOption(SupportsCapabilities): + @property + def app_working_dir(self) -> Optional[str]: + """ + Full path to the folder, which is going to be set as the working + dir for the application under test. + """ + return self.get_capability(APP_WORKING_DIR) + + @app_working_dir.setter + def app_working_dir(self, value: str) -> None: + """ + Set the full path to the folder, which is going to be set as the working + dir for the application under test. This is only applicable for classic apps. + """ + self.set_capability(APP_WORKING_DIR, value) diff --git a/appium/options/windows/windows/base.py b/appium/options/windows/windows/base.py new file mode 100644 index 00000000..52de731e --- /dev/null +++ b/appium/options/windows/windows/base.py @@ -0,0 +1,97 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Dict + +from appium.options.common.app_option import AppOption +from appium.options.common.automation_name_option import AUTOMATION_NAME +from appium.options.common.base import PLATFORM_NAME, AppiumOptions +from appium.options.common.postrun_option import PostrunOption +from appium.options.common.prerun_option import PrerunOption +from appium.options.common.system_port_option import SystemPortOption + +from .app_arguments_option import AppArgumentsOption +from .app_top_level_window_option import AppTopLevelWindowOption +from .app_working_dir_option import AppWorkingDirOption +from .create_session_timeout_option import CreateSessionTimeoutOption +from .expreimental_web_driver_option import ExperimentalWebDriverOption +from .wait_for_app_launch_option import WaitForAppLaunchOption + + +class WindowsOptions( + AppiumOptions, + PrerunOption, + PostrunOption, + AppOption, + AppTopLevelWindowOption, + AppWorkingDirOption, + CreateSessionTimeoutOption, + ExperimentalWebDriverOption, + SystemPortOption, + WaitForAppLaunchOption, + AppArgumentsOption, +): + @AppOption.app.setter # type: ignore + def app(self, value: str) -> None: + """ + The name of the UWP application to test or full path to a classic app, + for example Microsoft.WindowsCalculator_8wekyb3d8bbwe!App or + C:\\Windows\\System32\\notepad.exe. It is also possible to set app to Root. + In such case the session will be invoked without any explicit target application + (actually, it will be Explorer). Either this capability or appTopLevelWindow must + be provided on session startup. + """ + AppOption.app.fset(self, value) # type: ignore + + @PrerunOption.prerun.setter # type: ignore + def prerun(self, value: Dict[str, str]) -> None: + """ + A mapping containing either 'script' or 'command' key. The value of + each key must be a valid PowerShell script or command to be + executed prior to the WinAppDriver session startup. + See https://github.com/appium/appium-windows-driver#power-shell-commands-execution + for more details. + """ + PrerunOption.prerun.fset(self, value) # type: ignore + + @PostrunOption.postrun.setter # type: ignore + def postrun(self, value: Dict[str, str]) -> None: + """ + A mapping containing either 'script' or 'command' key. The value of + each key must be a valid PowerShell script or command to be + executed after a WinAppDriver session is finished. + See https://github.com/appium/appium-windows-driver#power-shell-commands-execution + for more details. + """ + PostrunOption.postrun.fset(self, value) # type: ignore + + @SystemPortOption.system_port.setter # type: ignore + def system_port(self, value: int) -> None: + """ + The port number to execute Appium Windows Driver server listener on, + for example 5556. The port must not be occupied. The default starting port + number for a new Appium Windows Driver session is 4724. If this port is + already busy then the next free port will be automatically selected. + """ + SystemPortOption.system_port.fset(self, value) # type: ignore + + @property + def default_capabilities(self) -> Dict: + return { + AUTOMATION_NAME: 'Windows', + PLATFORM_NAME: 'Windows', + } diff --git a/appium/options/windows/windows/create_session_timeout_option.py b/appium/options/windows/windows/create_session_timeout_option.py new file mode 100644 index 00000000..acf1289a --- /dev/null +++ b/appium/options/windows/windows/create_session_timeout_option.py @@ -0,0 +1,45 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +CREATE_SESSION_TIMEOUT = 'createSessionTimeout' + + +class CreateSessionTimeoutOption(SupportsCapabilities): + @property + def create_session_timeout(self) -> Optional[timedelta]: + """ + Timeout used to retry Appium Windows Driver session startup. + """ + value = self.get_capability(CREATE_SESSION_TIMEOUT) + return None if value is None else timedelta(milliseconds=value) + + @create_session_timeout.setter + def create_session_timeout(self, value: Union[timedelta, int]) -> None: + """ + Set the timeout used to retry Appium Windows Driver session startup. + This capability could be used as a workaround for the long startup times + of UWP applications (aka Failed to locate opened application window + with appId: TestCompany.my_app4!App, and processId: 8480). + """ + self.set_capability( + CREATE_SESSION_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value + ) diff --git a/appium/options/windows/windows/expreimental_web_driver_option.py b/appium/options/windows/windows/expreimental_web_driver_option.py new file mode 100644 index 00000000..ae065d71 --- /dev/null +++ b/appium/options/windows/windows/expreimental_web_driver_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +EXPERIMENTAL_WEB_DRIVER = 'ms:experimental-webdriver' + + +class ExperimentalWebDriverOption(SupportsCapabilities): + @property + def experimental_webdriver(self) -> Optional[bool]: + """ + Whether to enable experimental features and optimizations. + """ + return self.get_capability(EXPERIMENTAL_WEB_DRIVER) + + @experimental_webdriver.setter + def experimental_webdriver(self, value: bool) -> None: + """ + Enables experimental features and optimizations. See Appium Windows + Driver release notes for more details on this capability. + """ + self.set_capability(EXPERIMENTAL_WEB_DRIVER, value) diff --git a/appium/options/windows/windows/wait_for_app_launch_option.py b/appium/options/windows/windows/wait_for_app_launch_option.py new file mode 100644 index 00000000..9631b923 --- /dev/null +++ b/appium/options/windows/windows/wait_for_app_launch_option.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +WAIT_FOR_APP_LAUNCH = 'ms:waitForAppLaunch' + + +class WaitForAppLaunchOption(SupportsCapabilities): + @property + def wait_for_app_launch(self) -> Optional[timedelta]: + """ + Timeout used to retry Appium Windows Driver session startup. + """ + value = self.get_capability(WAIT_FOR_APP_LAUNCH) + return None if value is None else timedelta(seconds=value) + + @wait_for_app_launch.setter + def wait_for_app_launch(self, value: Union[timedelta, int]) -> None: + """ + Similar to createSessionTimeout, but is + applied on the server side. Enables Appium Windows Driver to wait for + a defined amount of time after an app launch is initiated prior to + attaching to the application session. The limit for this is 50 seconds. + """ + self.set_capability(WAIT_FOR_APP_LAUNCH, value.total_seconds() if isinstance(value, timedelta) else value) diff --git a/appium/protocols/__init__.py b/appium/protocols/__init__.py new file mode 100644 index 00000000..cc173e9d --- /dev/null +++ b/appium/protocols/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/appium/protocols/webdriver/__init__.py b/appium/protocols/webdriver/__init__.py new file mode 100644 index 00000000..cc173e9d --- /dev/null +++ b/appium/protocols/webdriver/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/appium/protocols/webdriver/can_execute_commands.py b/appium/protocols/webdriver/can_execute_commands.py new file mode 100644 index 00000000..de4f1b4a --- /dev/null +++ b/appium/protocols/webdriver/can_execute_commands.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +# 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. + +from typing import Dict, Protocol, Union + +from selenium.webdriver.remote.remote_connection import RemoteConnection + + +class CanExecuteCommands(Protocol): + command_executor: RemoteConnection + + def execute(self, driver_command: str, params: Union[Dict, None] = None) -> Dict: ... diff --git a/appium/protocols/webdriver/can_execute_scripts.py b/appium/protocols/webdriver/can_execute_scripts.py new file mode 100644 index 00000000..1d04f6ca --- /dev/null +++ b/appium/protocols/webdriver/can_execute_scripts.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +# 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. + +from typing import Any, List, Optional, Protocol + + +class CanExecuteScripts(Protocol): + def pin_script(self, script: str, script_key: Optional[Any] = None) -> Any: ... + + def unpin(self, script_key: Any) -> None: ... + + def get_pinned_scripts(self) -> List[str]: ... + + def execute_script(self, script: str, *args: Any) -> Any: ... + + def execute_async_script(self, script: str, *args: Any) -> Any: ... diff --git a/appium/protocols/webdriver/can_find_elements.py b/appium/protocols/webdriver/can_find_elements.py new file mode 100644 index 00000000..07b0b827 --- /dev/null +++ b/appium/protocols/webdriver/can_find_elements.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +# 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. + +from typing import TYPE_CHECKING, Dict, List, Protocol, Union, runtime_checkable + +if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement + + +@runtime_checkable +class CanFindElements(Protocol): + """Protocol for objects that can find web elements. + + Any class implementing this protocol must provide: + - find_element(by, value): Find a single element + - find_elements(by, value): Find multiple elements + """ + + def find_element(self, by: str, value: Union[str, Dict, None] = None) -> 'WebElement': ... + + def find_elements(self, by: str, value: Union[str, Dict, None] = None) -> List['WebElement']: ... diff --git a/appium/protocols/webdriver/can_remember_extension_presence.py b/appium/protocols/webdriver/can_remember_extension_presence.py new file mode 100644 index 00000000..1684788b --- /dev/null +++ b/appium/protocols/webdriver/can_remember_extension_presence.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +# 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. + +from typing import Protocol, TypeVar + +T = TypeVar('T') + + +class CanRememberExtensionPresence(Protocol): + def assert_extension_exists(self: T, ext_name: str) -> T: ... + + def mark_extension_absence(self: T, ext_name: str) -> T: ... diff --git a/appium/py.typed b/appium/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/appium/webdriver/common/mobileby.py b/appium/version.py similarity index 74% rename from appium/webdriver/common/mobileby.py rename to appium/version.py index 710248fe..0616a7c5 100644 --- a/appium/webdriver/common/mobileby.py +++ b/appium/version.py @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from selenium.webdriver.common.by import By +from importlib import metadata -class MobileBy(By): - IOS_UIAUTOMATION = '-ios uiautomation' - ANDROID_UIAUTOMATOR = '-android uiautomator' - ACCESSIBILITY_ID = 'accessibility id' +def _get_version(): + return metadata.version('Appium-Python-Client') + + +version = _get_version() diff --git a/appium/webdriver/appium_connection.py b/appium/webdriver/appium_connection.py new file mode 100644 index 00000000..9ca31182 --- /dev/null +++ b/appium/webdriver/appium_connection.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +# 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. + +import uuid +from typing import TYPE_CHECKING, Any, Dict + +from selenium.webdriver.remote.remote_connection import RemoteConnection + +from appium.common.helper import library_version + +if TYPE_CHECKING: + from urllib.parse import ParseResult + + +PREFIX_HEADER = 'appium/' + +HEADER_IDEMOTENCY_KEY = 'X-Idempotency-Key' + + +def _get_new_headers(key: str, headers: Dict[str, str]) -> Dict[str, str]: + """Return a new dictionary of heafers without the given key. + The key match is case-insensitive.""" + lower_key = key.lower() + return {k: v for k, v in headers.items() if k.lower() != lower_key} + + +class AppiumConnection(RemoteConnection): + """ + A subclass of selenium.webdriver.remote.remote_connection.Remoteconnection. + + The changes are + + * The default user agent + * Adds 'X-Idempotency-Key' header in a new session request to avoid proceeding + the same request multiple times in the Appium server side. + * https://github.com/appium/appium-base-driver/pull/400 + """ + + user_agent = f'{PREFIX_HEADER}{library_version()} ({RemoteConnection.user_agent})' + extra_headers = {} + + @classmethod + def get_remote_connection_headers(cls, parsed_url: 'ParseResult', keep_alive: bool = True) -> Dict[str, Any]: + """Override get_remote_connection_headers in RemoteConnection to control the extra headers. + This method will be used in sending a request method in this class. + """ + + if parsed_url.path.endswith('/session'): + # https://github.com/appium/appium-base-driver/pull/400 + cls.extra_headers[HEADER_IDEMOTENCY_KEY] = str(uuid.uuid4()) + else: + cls.extra_headers = _get_new_headers(HEADER_IDEMOTENCY_KEY, cls.extra_headers) + + return {**super().get_remote_connection_headers(parsed_url, keep_alive=keep_alive), **cls.extra_headers} diff --git a/appium/webdriver/appium_service.py b/appium/webdriver/appium_service.py new file mode 100644 index 00000000..93c926c6 --- /dev/null +++ b/appium/webdriver/appium_service.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python + +# 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. + +import os +import re +import subprocess as sp +import sys +import time +from typing import Any, Callable, List, Optional, Set + +from selenium.webdriver.remote.remote_connection import urllib3 + +DEFAULT_HOST = '127.0.0.1' +DEFAULT_PORT = 4723 +STARTUP_TIMEOUT_MS = 60000 +STATE_CHECK_INTERVAL_MS = 500 +MAIN_SCRIPT_PATH = 'appium/build/lib/main.js' +STATUS_URL = '/status' +DEFAULT_BASE_PATH = '/' +HTTP_STATUS_ERROR = 400 + + +class AppiumServiceError(RuntimeError): + pass + + +class AppiumStartupError(RuntimeError): + pass + + +class AppiumService: + def __init__(self) -> None: + self._process: Optional[sp.Popen] = None + self._cmd: Optional[List[str]] = None + + def start(self, **kwargs: Any) -> sp.Popen: + """Starts Appium service with given arguments. + + If you use the service to start Appium 1.x + then consider providing ['--base-path', '/wd/hub'] arguments. By default, + the service assumes Appium server listens on '/' path, which is the default path + for Appium 2. + + The service will be forcefully restarted if it is already running. + + Args: + env (dict): Environment variables mapping. The default system environment, + which is inherited from the parent process, is assigned by default. + node (str): The full path to the main NodeJS executable. The service will try + to retrieve it automatically if not provided. + npm (str): The full path to the Node Package Manager (npm) script. The service will try + to retrieve it automatically if not provided. + stdout (int): Check the documentation for subprocess.Popen for more details. + The default value is subprocess.DEVNULL on Windows and subprocess.PIPE on other platforms. + stderr (int): Check the documentation for subprocess.Popen for more details. + The default value is subprocess.DEVNULL on Windows and subprocess.PIPE on other platforms. + timeout_ms (int): The maximum time to wait until Appium process starts listening + for HTTP connections. If set to zero or a negative number then no wait will be applied. + 60000 ms by default. + main_script (str): The full path to the main Appium executable + (usually located at build/lib/main.js). If not set + then the service tries to detect the path automatically. + args (str): List of Appium arguments (all must be strings). Check + https://appium.io/docs/en/writing-running-appium/server-args/ for more details + about possible arguments and their values. + + Returns: You can use Popen.communicate interface or stderr/stdout properties + of the instance (stdout/stderr must not be set to None in such case) in order to retrieve the actual process + output. + + """ + self.stop() + + env = kwargs['env'] if 'env' in kwargs else None + node: str = kwargs.get('node') or get_node() + npm: str = kwargs.get('npm') or get_npm() + main_script: str = kwargs.get('main_script') or get_main_script(node, npm) + # A workaround for https://github.com/appium/python-client/issues/534 + default_std = sp.DEVNULL if sys.platform == 'win32' else sp.PIPE + stdout = kwargs['stdout'] if 'stdout' in kwargs else default_std + stderr = kwargs['stderr'] if 'stderr' in kwargs else default_std + timeout_ms = int(kwargs['timeout_ms']) if 'timeout_ms' in kwargs else STARTUP_TIMEOUT_MS + args: List[str] = [node, main_script] + if 'args' in kwargs: + args.extend(kwargs['args']) + self._cmd = args + self._process = sp.Popen(args=args, stdout=stdout, stderr=stderr, env=env) + error_msg: Optional[str] = None + startup_failure_msg = ( + 'Appium server process is unable to start. Make sure proper values have been ' + f"provided to 'node' ({node}), 'npm' ({npm}) and 'main_script' ({main_script}) " + f'method arguments.' + ) + if timeout_ms > 0: + server_url = _make_server_url(args) + try: + if not is_service_listening( + server_url, + timeout=timeout_ms / 1000, + custom_validator=self._assert_is_running, + ): + error_msg = ( + f'Appium server has started but is not listening on {server_url} ' + f'within {timeout_ms}ms timeout. Make sure proper values have been provided ' + f'to --base-path, --address and --port process arguments.' + ) + except AppiumStartupError: + error_msg = startup_failure_msg + elif not self.is_running: + error_msg = startup_failure_msg + if error_msg is not None: + if stderr == sp.PIPE and self._process.stderr is not None: + # noinspection PyUnresolvedReferences + err_output = self._process.stderr.read() + if err_output: + error_msg += f'\nOriginal error: {str(err_output)}' + self.stop() + raise AppiumServiceError(error_msg) + return self._process + + def stop(self, timeout: float = 5.5) -> bool: + """Stops Appium service if it is running. + + The call will be ignored if the service is not running + or has been already stopped. + + Args: + timeout: The maximum time in float seconds to wait for the server process to terminate + + Returns: + `True` if the service was running before being stopped + """ + was_running = False + if self.is_running: + assert self._process + was_running = True + self._process.terminate() + try: + self._process.communicate(timeout=timeout) + except sp.SubprocessError: + if sys.platform == 'win32': + sp.call(['taskkill', '/f', '/pid', str(self._process.pid)]) + else: + self._process.kill() + self._process = None + self._cmd = None + return was_running + + @property + def is_running(self) -> bool: + """Check if the service is running. + + :return: `True` if the service is running + """ + return self._process is not None and self._cmd is not None and self._process.poll() is None + + @property + def is_listening(self) -> bool: + """Check if the service is listening on the given/default host/port. + + The fact, that the service is running, does not always mean it is listening. + The default host/port/base path values can be customized by providing + --address/--port/--base-path command line arguments while starting the service. + + Returns: + `True` if the service is running and listening on the given/default host/port + """ + if not self.is_running: + return False + + assert self._cmd + try: + return is_service_listening( + _make_server_url(self._cmd), + timeout=STATE_CHECK_INTERVAL_MS, + custom_validator=self._assert_is_running, + ) + except AppiumStartupError: + return False + + def _assert_is_running(self) -> None: + if not self.is_running: + raise AppiumStartupError() + + +def is_service_listening(url: str, timeout: float = 5, custom_validator: Optional[Callable[[], None]] = None) -> bool: + """ + Check if the service is running + + Args: + url: Full server url + timeout: Timeout in float seconds + custom_validator: Custom callable method to be executed upon each validation loop before the timeout happens + + Returns: + True if Appium server is running before the timeout + """ + time_started_sec = time.perf_counter() + conn = urllib3.PoolManager(timeout=1.0) + while time.perf_counter() < time_started_sec + timeout: + if custom_validator is not None: + custom_validator() + # noinspection PyUnresolvedReferences + try: + resp = conn.request('HEAD', url) + if resp.status < HTTP_STATUS_ERROR: + return True + except urllib3.exceptions.HTTPError: + pass + time.sleep(STATE_CHECK_INTERVAL_MS / 1000.0) + return False + + +def find_executable(executable: str) -> Optional[str]: + path = os.environ['PATH'] + paths = path.split(os.pathsep) + _, ext = os.path.splitext(executable) + if sys.platform == 'win32' and not ext: + executable = executable + '.exe' + + if os.path.isfile(executable): + return executable + + for p in paths: + full_path = os.path.join(p, executable) + if os.path.isfile(full_path): + return full_path + + return None + + +def get_node() -> str: + result = find_executable('node') + if result is None: + raise AppiumServiceError('NodeJS main executable cannot be found. Make sure it is installed and present in PATH') + return result + + +def get_npm() -> str: + result = find_executable('npm.cmd' if sys.platform == 'win32' else 'npm') + if result is None: + raise AppiumServiceError( + 'Node Package Manager executable cannot be found. Make sure it is installed and present in PATH' + ) + return result + + +def get_main_script(node: Optional[str], npm: Optional[str]) -> str: + result: Optional[str] = None + npm_path = npm or get_npm() + for args in [['root', '-g'], ['root']]: + try: + modules_root = sp.check_output([npm_path] + args).strip().decode('utf-8') + full_path = os.path.join(modules_root, *MAIN_SCRIPT_PATH.split('/')) + if os.path.exists(full_path): + result = full_path + break + except sp.CalledProcessError: + continue + if result is None: + node_path = node or get_node() + try: + result = ( + sp.check_output([node_path, '-e', f'console.log(require.resolve("{MAIN_SCRIPT_PATH}"))']) + .decode('utf-8') + .strip() + ) + except sp.CalledProcessError as e: + raise AppiumServiceError(e.output) from e + return result + + +def _parse_arg_value(args: List[str], arg_names: Set[str], default: str) -> str: + for idx, arg in enumerate(args): + if arg in arg_names and idx < len(args) - 1: + return args[idx + 1] + return default + + +def _parse_port(args: List[str]) -> int: + return int(_parse_arg_value(args, {'--port', '-p'}, str(DEFAULT_PORT))) + + +def _parse_base_path(args: List[str]) -> str: + return _parse_arg_value(args, {'--base-path', '-pa'}, DEFAULT_BASE_PATH) + + +def _parse_host(args: List[str]) -> str: + return _parse_arg_value(args, {'--address', '-a'}, DEFAULT_HOST) + + +def _parse_protocol(args: List[str]) -> str: + return ( + 'https' + if _parse_arg_value(args, {'--ssl-cert-path'}, '') and _parse_arg_value(args, {'--ssl-key-path'}, '') + else 'http' + ) + + +def _make_status_path(args: List[str]) -> str: + base_path = _parse_base_path(args) + return STATUS_URL if base_path == DEFAULT_BASE_PATH else f'{re.sub(r"/+$", "", base_path)}{STATUS_URL}' + + +def _make_server_url(args: List[str]) -> str: + return f'{_parse_protocol(args)}://{_parse_host(args)}:{_parse_port(args)}{_make_status_path(args)}' + + +if __name__ == '__main__': + assert find_executable('node') is not None + assert find_executable('npm') is not None + service = AppiumService() + service.start(args=['--address', '127.0.0.1', '-p', str(DEFAULT_PORT)]) + # service.start(args=['--address', '127.0.0.1', '-p', '80'], timeout_ms=2000) + assert service.is_running + assert service.is_listening + service.stop() + assert not service.is_running + assert not service.is_listening diff --git a/appium/webdriver/applicationstate.py b/appium/webdriver/applicationstate.py new file mode 100644 index 00000000..0658b264 --- /dev/null +++ b/appium/webdriver/applicationstate.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +# 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. + + +class ApplicationState: + NOT_INSTALLED = 0 + NOT_RUNNING = 1 + RUNNING_IN_BACKGROUND_SUSPENDED = 2 + RUNNING_IN_BACKGROUND = 3 + RUNNING_IN_FOREGROUND = 4 diff --git a/appium/webdriver/client_config.py b/appium/webdriver/client_config.py new file mode 100644 index 00000000..ae2c0e29 --- /dev/null +++ b/appium/webdriver/client_config.py @@ -0,0 +1,38 @@ +# 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. + +from selenium.webdriver.remote.client_config import ClientConfig + + +class AppiumClientConfig(ClientConfig): + """ClientConfig class for Appium Python client. + This class inherits selenium.webdriver.remote.client_config.ClientConfig. + """ + + def __init__(self, remote_server_addr: str, *args, **kwargs): + """ + Please refer to selenium.webdriver.remote.client_config.ClientConfig documentation + about available arguments. Only 'direct_connection' below is AppiumClientConfig + specific argument. + + Args: + direct_connection: If enables [directConnect](https://github.com/appium/python-client?tab=readme-ov-file#direct-connect-urls) + feature. + """ + self._direct_connection = kwargs.pop('direct_connection', False) + super().__init__(remote_server_addr, *args, **kwargs) + + @property + def direct_connection(self) -> bool: + """Return if [directConnect](https://github.com/appium/python-client?tab=readme-ov-file#direct-connect-urls) + is enabled.""" + return self._direct_connection diff --git a/appium/webdriver/clipboard_content_type.py b/appium/webdriver/clipboard_content_type.py new file mode 100644 index 00000000..7c1211b3 --- /dev/null +++ b/appium/webdriver/clipboard_content_type.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +# 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. + + +class ClipboardContentType: + PLAINTEXT = 'plaintext' + IMAGE = 'image' + URL = 'url' diff --git a/appium/webdriver/command_method.py b/appium/webdriver/command_method.py new file mode 100644 index 00000000..dde45cb4 --- /dev/null +++ b/appium/webdriver/command_method.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +# 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. + +import enum + + +class CommandMethod(enum.Enum): + GET = 'GET' + HEAD = 'HEAD' + POST = 'POST' + PUT = 'PUT' + DELETE = 'DELETE' + CONNECT = 'CONNECT' + OPTIONS = 'OPTIONS' + TRACE = 'TRACE' + PATCH = 'PATCH' diff --git a/appium/webdriver/common/appiumby.py b/appium/webdriver/common/appiumby.py new file mode 100644 index 00000000..371573ab --- /dev/null +++ b/appium/webdriver/common/appiumby.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +# 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. + +from typing import Literal + +from selenium.webdriver.common.by import By + + +class AppiumBy(By): + IOS_PREDICATE = '-ios predicate string' + IOS_CLASS_CHAIN = '-ios class chain' + ANDROID_UIAUTOMATOR = '-android uiautomator' + ANDROID_VIEWTAG = '-android viewtag' + ANDROID_DATA_MATCHER = '-android datamatcher' + ANDROID_VIEW_MATCHER = '-android viewmatcher' + ACCESSIBILITY_ID = 'accessibility id' + IMAGE = '-image' + CUSTOM = '-custom' + + # For Flutter integration usage https://github.com/AppiumTestDistribution/appium-flutter-integration-driver/tree/main + FLUTTER_INTEGRATION_SEMANTICS_LABEL = '-flutter semantics label' + FLUTTER_INTEGRATION_TYPE = '-flutter type' + FLUTTER_INTEGRATION_KEY = '-flutter key' + FLUTTER_INTEGRATION_TEXT = '-flutter text' + FLUTTER_INTEGRATION_TEXT_CONTAINING = '-flutter text containing' + + +ByType = Literal[ + '-ios predicate string', + '-ios class chain', + '-android uiautomator', + '-android viewtag', + '-android datamatcher', + '-android viewmatcher', + 'accessibility id', + '-image', + '-custom', + '-flutter semantics label', + '-flutter type', + '-flutter key', + '-flutter text', + '-flutter text containing', +] diff --git a/appium/webdriver/common/multi_action.py b/appium/webdriver/common/multi_action.py deleted file mode 100644 index fb80a520..00000000 --- a/appium/webdriver/common/multi_action.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python - -# 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. - - -# The Selenium team implemented something like the Multi Action API in the form of -# "action chains" (https://code.google.com/p/selenium/source/browse/py/selenium/webdriver/common/action_chains.py). -# These do not quite work for this situation, and do not allow for ad hoc action -# chaining as the spec requires. - -import copy - -from appium.webdriver.mobilecommand import MobileCommand as Command - - -class MultiAction(object): - def __init__(self, driver, element=None): - self._driver = driver - self._element = element - self._touch_actions = [] - - def add(self, *touch_actions): - """Add TouchAction objects to the MultiAction, to be performed later. - - :Args: - - touch_actions - one or more TouchAction objects describing a chain of actions to be performed by one finger - - :Usage: - a1 = TouchAction(driver) - a1.press(el1).move_to(el2).release() - a2 = TouchAction(driver) - a2.press(el2).move_to(el1).release() - - MultiAction(driver).add(a1, a2) - """ - for touch_action in touch_actions: - if self._touch_actions is None: - self._touch_actions = [] - - # deep copy, so that once they are in here, the user can't muck about - self._touch_actions.append(copy.deepcopy(touch_action)) - - def perform(self): - """Perform the actions stored in the object. - - :Usage: - a1 = TouchAction(driver) - a1.press(el1).move_to(el2).release() - a2 = TouchAction(driver) - a2.press(el2).move_to(el1).release() - - MultiAction(driver).add(a1, a2).perform() - """ - self._driver.execute(Command.MULTI_ACTION, self.json_wire_gestures) - - # clean up and be ready for the next batch - self._touch_actions = [] - - return self - - @property - def json_wire_gestures(self): - actions = [] - for action in self._touch_actions: - actions.append(action.json_wire_gestures) - if self._element is not None: - return {'actions': actions, 'elementId': self._element.id} - else: - return {'actions': actions} diff --git a/appium/webdriver/common/touch_action.py b/appium/webdriver/common/touch_action.py deleted file mode 100644 index ccc542c5..00000000 --- a/appium/webdriver/common/touch_action.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env python - -# 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. - - -# The Selenium team implemented a version of the Touch Action API in their code -# (https://code.google.com/p/selenium/source/browse/py/selenium/webdriver/common/touch_actions.py) -# but it is deficient in many ways, and does not work in such a way as to be -# amenable to Appium's use of iOS UIAutomation and Android UIAutomator -# So it is reimplemented here. -# -# Theirs is `TouchActions`. Appium's is `TouchAction`. - -import copy - -from appium.webdriver.mobilecommand import MobileCommand as Command - - -class TouchAction(object): - def __init__(self, driver=None): - self._driver = driver - self._actions = [] - - def tap(self, element=None, x=None, y=None, count=1): - """Perform a tap action on the element - - :Args: - - element - the element to tap - - x - (optional) x coordinate to tap, relative to the top left corner of the element. - - y - (optional) y coordinate. If y is used, x must also be set, and vice versa - - :Usage: - """ - opts = self._get_opts(element, x, y) - opts['count'] = count - self._add_action('tap', opts) - - return self - - def press(self, el=None, x=None, y=None): - """Begin a chain with a press down action at a particular element or point - """ - self._add_action('press', self._get_opts(el, x, y)) - - return self - - def long_press(self, el=None, x=None, y=None, duration=1000): - """Begin a chain with a press down that lasts `duration` milliseconds - """ - self._add_action('longPress', self._get_opts(el, x, y, duration)) - - return self - - def wait(self, ms=0): - """Pause for `ms` milliseconds. - """ - if ms is None: - ms = 0 - - opts = {'ms': ms} - - self._add_action('wait', opts) - - return self - - def move_to(self, el=None, x=None, y=None): - """Move the pointer from the previous point to the element or point specified - """ - self._add_action('moveTo', self._get_opts(el, x, y)) - - return self - - def release(self): - """End the action by lifting the pointer off the screen - """ - self._add_action('release', {}) - - return self - - def perform(self): - """Perform the action by sending the commands to the server to be operated upon - """ - params = {'actions': self._actions} - self._driver.execute(Command.TOUCH_ACTION, params) - - # get rid of actions so the object can be reused - self._actions = [] - - return self - - @property - def json_wire_gestures(self): - gestures = [] - for action in self._actions: - gestures.append(copy.deepcopy(action)) - return gestures - - def _add_action(self, action, options): - gesture = { - 'action': action, - 'options': options, - } - self._actions.append(gesture) - - def _get_opts(self, element, x, y, duration = None): - opts = {} - if element is not None: - opts['element'] = element.id - - # it makes no sense to have x but no y, or vice versa. - if x is None or y is None: - x, y = None, None - opts['x'] = x - opts['y'] = y - - if duration is not None: - opts['duration'] = duration - - return opts - diff --git a/appium/webdriver/connectiontype.py b/appium/webdriver/connectiontype.py index f3682a38..7db69ba3 100644 --- a/appium/webdriver/connectiontype.py +++ b/appium/webdriver/connectiontype.py @@ -12,21 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. -from enum import Enum - """ Connection types are specified here: https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile#120 - Value (Alias) | Data | Wifi | Airplane Mode - ------------------------------------------------- - 0 (None) | 0 | 0 | 0 - 1 (Airplane Mode) | 0 | 0 | 1 - 2 (Wifi only) | 0 | 1 | 0 - 4 (Data only) | 1 | 0 | 0 - 6 (All network on) | 1 | 1 | 0 + + +--------------------+------+------+---------------+ + | Value (Alias) | Data | Wifi | Airplane Mode | + +====================+======+======+===============+ + | 0 (None) | 0 | 0 | 0 | + +--------------------+------+------+---------------+ + | 1 (Airplane Mode) | 0 | 0 | 1 | + +--------------------+------+------+---------------+ + | 2 (Wifi only) | 0 | 1 | 0 | + +--------------------+------+------+---------------+ + | 4 (Data only) | 1 | 0 | 0 | + +--------------------+------+------+---------------+ + | 6 (All network on) | 1 | 1 | 0 | + +--------------------+------+------+---------------+ + """ -class ConnectionType(Enum): + + +class ConnectionType: NO_CONNECTION = 0 AIRPLANE_MODE = 1 WIFI_ONLY = 2 diff --git a/appium/webdriver/errorhandler.py b/appium/webdriver/errorhandler.py index eea57a76..b2a5cab9 100644 --- a/appium/webdriver/errorhandler.py +++ b/appium/webdriver/errorhandler.py @@ -12,19 +12,114 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json +from typing import Any, Dict, List, Sequence, Type, Union + +import selenium.common.exceptions as sel_exceptions from selenium.webdriver.remote import errorhandler -from selenium.common.exceptions import WebDriverException -from appium.common.exceptions import NoSuchContextException +import appium.common.exceptions as appium_exceptions + +ERROR_TO_EXC_MAPPING: Dict[str, Type[sel_exceptions.WebDriverException]] = { + 'element click intercepted': sel_exceptions.ElementClickInterceptedException, + 'element not interactable': sel_exceptions.ElementNotInteractableException, + 'insecure certificate': sel_exceptions.InsecureCertificateException, + 'invalid argument': sel_exceptions.InvalidArgumentException, + 'invalid cookie domain': sel_exceptions.InvalidCookieDomainException, + 'invalid element state': sel_exceptions.InvalidElementStateException, + 'invalid selector': sel_exceptions.InvalidSelectorException, + 'invalid session id': sel_exceptions.InvalidSessionIdException, + 'javascript error': sel_exceptions.JavascriptException, + 'move target out of bounds': sel_exceptions.MoveTargetOutOfBoundsException, + 'no such alert': sel_exceptions.NoAlertPresentException, + 'no such cookie': sel_exceptions.NoSuchCookieException, + 'no such element': sel_exceptions.NoSuchElementException, + 'no such frame': sel_exceptions.NoSuchFrameException, + 'no such window': sel_exceptions.NoSuchWindowException, + 'no such shadow root': sel_exceptions.NoSuchShadowRootException, + 'script timeout': sel_exceptions.TimeoutException, + 'session not created': sel_exceptions.SessionNotCreatedException, + 'stale element reference': sel_exceptions.StaleElementReferenceException, + 'detached shadow root': sel_exceptions.NoSuchShadowRootException, + 'timeout': sel_exceptions.TimeoutException, + 'unable to set cookie': sel_exceptions.UnableToSetCookieException, + 'unable to capture screen': sel_exceptions.ScreenshotException, + 'unexpected alert open': sel_exceptions.UnexpectedAlertPresentException, + 'unknown command': sel_exceptions.UnknownMethodException, + 'unknown error': sel_exceptions.WebDriverException, + 'unknown method': sel_exceptions.UnknownMethodException, + 'unsupported operation': sel_exceptions.UnknownMethodException, + 'element not visible': sel_exceptions.ElementNotVisibleException, + 'element not selectable': sel_exceptions.ElementNotSelectableException, + 'invalid coordinates': sel_exceptions.InvalidCoordinatesException, +} + + +def format_stacktrace(original: Union[None, str, Sequence]) -> List[str]: + if not original: + return [] + if isinstance(original, str): + return original.split('\n') + + result: List[str] = [] + try: + for frame in original: + if not isinstance(frame, dict): + continue + + line = frame.get('lineNumber', '') + file = frame.get('fileName', '') + if line: + file = f'{file}:{line}' + meth = frame.get('methodName', '') + if 'className' in frame: + meth = f'{frame["className"]}.{meth}' + result.append(f' at {meth} ({file})') + except TypeError: + pass + return result class MobileErrorHandler(errorhandler.ErrorHandler): - def check_response(self, response): - try: - super(MobileErrorHandler, self).check_response(response) - except WebDriverException as wde: - if wde.msg == 'No such context found.': - raise NoSuchContextException(wde.msg, wde.screen, wde.stacktrace) - else: - raise wde + def check_response(self, response: Dict[str, Any]) -> None: + """ + https://www.w3.org/TR/webdriver/#errors + """ + payload = response.get('value', '') + if isinstance(payload, dict): + payload_dict = payload + else: + try: + payload_dict = json.loads(payload) + except (json.JSONDecodeError, TypeError): + return + if not isinstance(payload_dict, dict): + return + value = payload_dict.get('value') + if not isinstance(value, dict): + return + error = value.get('error') + if not error: + return + + message = value.get('message', error) + stacktrace = value.get('stacktrace', '') + # In theory, we should also be checking HTTP status codes. + # Java client, for example, prints a warning if the actual `error` + # value does not match to the response's HTTP status code. + exception_class: Type[sel_exceptions.WebDriverException] = ERROR_TO_EXC_MAPPING.get( + error, sel_exceptions.WebDriverException + ) + if exception_class is sel_exceptions.WebDriverException and message: + if message == 'No such context found.': + exception_class = appium_exceptions.NoSuchContextException + elif message == 'That command could not be executed in the current context.': + exception_class = appium_exceptions.InvalidSwitchToTargetException + if exception_class is sel_exceptions.UnexpectedAlertPresentException: + raise sel_exceptions.UnexpectedAlertPresentException( + msg=message, + stacktrace=format_stacktrace(stacktrace), + alert_text=value.get('data'), + ) + raise exception_class(msg=message, stacktrace=format_stacktrace(stacktrace)) diff --git a/appium/webdriver/extensions/__init__.py b/appium/webdriver/extensions/__init__.py new file mode 100644 index 00000000..cc173e9d --- /dev/null +++ b/appium/webdriver/extensions/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/appium/webdriver/extensions/action_helpers.py b/appium/webdriver/extensions/action_helpers.py new file mode 100644 index 00000000..9c370922 --- /dev/null +++ b/appium/webdriver/extensions/action_helpers.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +# 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. + +from typing import TYPE_CHECKING, List, Optional, Tuple, cast + +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.actions import interaction +from selenium.webdriver.common.actions.action_builder import ActionBuilder +from selenium.webdriver.common.actions.mouse_button import MouseButton +from selenium.webdriver.common.actions.pointer_input import PointerInput +from typing_extensions import Self + +from appium.webdriver.webelement import WebElement + +if TYPE_CHECKING: + # noinspection PyUnresolvedReferences + from appium.webdriver.webdriver import WebDriver + + +class ActionHelpers: + def scroll(self, origin_el: WebElement, destination_el: WebElement, duration: Optional[int] = None) -> Self: + """Scrolls from one element to another + + Args: + origin_el: the element from which to begin scrolling (center of element) + destination_el: the element to scroll to (center of element) + duration: defines speed of scroll action when moving from originalEl to destinationEl. + Default is 600 ms for W3C spec. + + Usage: + driver.scroll(el1, el2) + + Returns: + Union['WebDriver', 'ActionHelpers']: Self instance + """ + # XCUITest x W3C spec has no duration by default in server side + if duration is None: + duration = 600 + + touch_input = PointerInput(interaction.POINTER_TOUCH, 'touch') + + actions = ActionChains(cast('WebDriver', self)) + actions.w3c_actions = ActionBuilder(self, mouse=touch_input) + + # https://github.com/SeleniumHQ/selenium/blob/3c82c868d4f2a7600223a1b3817301d0b04d28e4/py/selenium/webdriver/common/actions/pointer_actions.py#L83 + actions.w3c_actions.pointer_action.move_to(origin_el) + actions.w3c_actions.pointer_action.pointer_down() + # setup duration for second move only, assuming duration always has atleast default value + actions.w3c_actions = ActionBuilder(self, mouse=touch_input, duration=duration) + actions.w3c_actions.pointer_action.move_to(destination_el) + actions.w3c_actions.pointer_action.release() + actions.perform() + return self + + def drag_and_drop(self, origin_el: WebElement, destination_el: WebElement, pause: Optional[float] = None) -> Self: + """Drag the origin element to the destination element + + Args: + origin_el: the element to drag + destination_el: the element to drag to + pause: how long the action pauses before moving after the tap and hold, in float seconds. + + Returns: + Union['WebDriver', 'ActionHelpers']: Self instance + """ + actions = ActionChains(cast('WebDriver', self)) + # 'mouse' pointer action + actions.w3c_actions.pointer_action.click_and_hold(origin_el) + if pause is not None and pause > 0: + actions.w3c_actions.pointer_action.pause(pause) + actions.w3c_actions.pointer_action.move_to(destination_el) + actions.w3c_actions.pointer_action.release() + actions.perform() + return self + + def tap(self, positions: List[Tuple[int, int]], duration: Optional[int] = None) -> Self: + """Taps on an particular place with up to five fingers, holding for a + certain time + + Args: + positions: an array of tuples representing the x/y coordinates of + the fingers to tap. Length can be up to five. + duration: length of time to tap, in ms + + Usage: + driver.tap([(100, 20), (100, 60), (100, 100)], 500) + + Returns: + Union['WebDriver', 'ActionHelpers']: Self instance + """ + if len(positions) == 1: + actions = ActionChains(cast('WebDriver', self)) + actions.w3c_actions = ActionBuilder(self, mouse=PointerInput(interaction.POINTER_TOUCH, 'touch')) + x = positions[0][0] + y = positions[0][1] + actions.w3c_actions.pointer_action.move_to_location(x, y) + actions.w3c_actions.pointer_action.pointer_down() + if duration: + actions.w3c_actions.pointer_action.pause(duration / 1000) + else: + actions.w3c_actions.pointer_action.pause(0.1) + actions.w3c_actions.pointer_action.release() + actions.perform() + else: + finger = 0 + actions = ActionChains(cast('WebDriver', self)) + actions.w3c_actions.devices = [] + + for position in positions: + finger += 1 + x = position[0] + y = position[1] + + # https://github.com/SeleniumHQ/selenium/blob/trunk/py/selenium/webdriver/common/actions/pointer_input.py + new_input = actions.w3c_actions.add_pointer_input('touch', f'finger{finger}') + new_input.create_pointer_move(x=x, y=y) + new_input.create_pointer_down(button=MouseButton.LEFT) + if duration: + new_input.create_pause(duration / 1000) + else: + new_input.create_pause(0.1) + new_input.create_pointer_up(MouseButton.LEFT) + actions.perform() + return self + + def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration: int = 0) -> Self: + """Swipe from one point to another point, for an optional duration. + + Args: + start_x: x-coordinate at which to start + start_y: y-coordinate at which to start + end_x: x-coordinate at which to stop + end_y: y-coordinate at which to stop + duration: defines the swipe speed as time taken to swipe from point a to point b, in ms. + + Usage: + driver.swipe(100, 100, 100, 400) + + Returns: + Union['WebDriver', 'ActionHelpers']: Self instance + """ + touch_input = PointerInput(interaction.POINTER_TOUCH, 'touch') + + actions = ActionChains(cast('WebDriver', self)) + actions.w3c_actions = ActionBuilder(self, mouse=touch_input) + actions.w3c_actions.pointer_action.move_to_location(start_x, start_y) + actions.w3c_actions.pointer_action.pointer_down() + if duration > 0: + actions.w3c_actions = ActionBuilder(self, mouse=touch_input, duration=duration) + actions.w3c_actions.pointer_action.move_to_location(end_x, end_y) + actions.w3c_actions.pointer_action.release() + actions.perform() + return self + + def flick(self, start_x: int, start_y: int, end_x: int, end_y: int) -> Self: + """Flick from one point to another point. + + Args: + start_x: x-coordinate at which to start + start_y: y-coordinate at which to start + end_x: x-coordinate at which to stop + end_y: y-coordinate at which to stop + + Usage: + driver.flick(100, 100, 100, 400) + + Returns: + Union['WebDriver', 'ActionHelpers']: Self instance + """ + actions = ActionChains(cast('WebDriver', self)) + actions.w3c_actions = ActionBuilder(self, mouse=PointerInput(interaction.POINTER_TOUCH, 'touch')) + actions.w3c_actions.pointer_action.move_to_location(start_x, start_y) + actions.w3c_actions.pointer_action.pointer_down() + actions.w3c_actions.pointer_action.move_to_location(end_x, end_y) + actions.w3c_actions.pointer_action.release() + actions.perform() + return self diff --git a/appium/webdriver/extensions/android/__init__.py b/appium/webdriver/extensions/android/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appium/webdriver/extensions/android/activities.py b/appium/webdriver/extensions/android/activities.py new file mode 100644 index 00000000..76da521b --- /dev/null +++ b/appium/webdriver/extensions/android/activities.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +# 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. + +from selenium.common.exceptions import TimeoutException, UnknownMethodException +from selenium.webdriver.support.ui import WebDriverWait + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence +from appium.webdriver.mobilecommand import MobileCommand as Command + + +class Activities(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + @property + def current_activity(self) -> str: + """Retrieves the current activity running on the device. + + Returns: + str: The current activity name running on the device + """ + ext_name = 'mobile: getCurrentActivity' + try: + return self.assert_extension_exists(ext_name).execute_script(ext_name) + except UnknownMethodException: + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute(Command.GET_CURRENT_ACTIVITY)['value'] + + def wait_activity(self, activity: str, timeout: int, interval: int = 1) -> bool: + """Wait for an activity: block until target activity presents or time out. + + This is an Android-only method. + + Args: + activity: target activity + timeout: max wait time, in seconds + interval: sleep interval between retries, in seconds + + Returns: + `True` if the target activity is shown + """ + try: + WebDriverWait(self, timeout, interval).until( # type: ignore[type-var] + lambda d: d.current_activity == activity + ) + return True + except TimeoutException: + return False + + def _add_commands(self) -> None: + self.command_executor.add_command( + Command.GET_CURRENT_ACTIVITY, + 'GET', + '/session/$sessionId/appium/device/current_activity', + ) diff --git a/appium/webdriver/extensions/android/common.py b/appium/webdriver/extensions/android/common.py new file mode 100644 index 00000000..fe75260b --- /dev/null +++ b/appium/webdriver/extensions/android/common.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +# 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. + +from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence +from appium.webdriver.mobilecommand import MobileCommand as Command + + +class Common(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def open_notifications(self) -> Self: + """Open notification shade in Android (API Level 18 and above) + + Returns: + Union['WebDriver', 'Common']: Self instance + """ + ext_name = 'mobile: openNotifications' + try: + self.assert_extension_exists(ext_name).execute_script(ext_name) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.OPEN_NOTIFICATIONS, {}) + return self + + @property + def current_package(self) -> str: + """Retrieves the current package running on the device.""" + ext_name = 'mobile: getCurrentPackage' + try: + return self.assert_extension_exists(ext_name).execute_script(ext_name) + except UnknownMethodException: + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute(Command.GET_CURRENT_PACKAGE)['value'] + + def _add_commands(self) -> None: + self.command_executor.add_command( + Command.GET_CURRENT_PACKAGE, + 'GET', + '/session/$sessionId/appium/device/current_package', + ) + self.command_executor.add_command( + Command.OPEN_NOTIFICATIONS, + 'POST', + '/session/$sessionId/appium/device/open_notifications', + ) diff --git a/appium/webdriver/extensions/android/display.py b/appium/webdriver/extensions/android/display.py new file mode 100644 index 00000000..28abdcbb --- /dev/null +++ b/appium/webdriver/extensions/android/display.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +# 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. + +from selenium.common.exceptions import UnknownMethodException + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence +from appium.webdriver.mobilecommand import MobileCommand as Command + + +class Display(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def get_display_density(self) -> int: + """Get the display density, Android only + + Returns: + The display density of the Android device(dpi) + + Usage: + self.driver.get_display_density() + + Return: + int: The display density + """ + ext_name = 'mobile: getDisplayDensity' + try: + return self.assert_extension_exists(ext_name).execute_script(ext_name) + except UnknownMethodException: + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute(Command.GET_DISPLAY_DENSITY)['value'] + + def _add_commands(self) -> None: + self.command_executor.add_command( + Command.GET_DISPLAY_DENSITY, + 'GET', + '/session/$sessionId/appium/device/display_density', + ) diff --git a/appium/webdriver/extensions/android/gsm.py b/appium/webdriver/extensions/android/gsm.py new file mode 100644 index 00000000..ed43d3c6 --- /dev/null +++ b/appium/webdriver/extensions/android/gsm.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python + +# 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. + +from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self + +from appium.common.helper import extract_const_attributes +from appium.common.logger import logger +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence +from appium.webdriver.mobilecommand import MobileCommand as Command + + +class GsmCallActions: + CALL = 'call' + ACCEPT = 'accept' + CANCEL = 'cancel' + HOLD = 'hold' + + +class GsmSignalStrength: + NONE_OR_UNKNOWN = 0 + POOR = 1 + MODERATE = 2 + GOOD = 3 + GREAT = 4 + + +class GsmVoiceState: + UNREGISTERED = 'unregistered' + HOME = 'home' + ROAMING = 'roaming' + SEARCHING = 'searching' + DENIED = 'denied' + OFF = 'off' + ON = 'on' + + +class Gsm(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def make_gsm_call(self, phone_number: str, action: str) -> Self: + """Make GSM call (Emulator only) + + Android only. + + Args: + phone_number: The phone number to call to. + action: The call action. + A member of the const `appium.webdriver.extensions.android.gsm.GsmCallActions` + + Usage: + self.driver.make_gsm_call('5551234567', GsmCallActions.CALL) + + Returns: + Union['WebDriver', 'Gsm']: Self instance + """ + ext_name = 'mobile: gsmCall' + constants = extract_const_attributes(GsmCallActions) + if action not in constants.values(): + logger.warning( + f'{action} is unknown. Consider using one of {list(constants.keys())} constants. ' + f'(e.g. {GsmCallActions.__name__}.CALL)' + ) + args = {'phoneNumber': phone_number, 'action': action} + try: + self.assert_extension_exists(ext_name).execute_script(ext_name, args) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.MAKE_GSM_CALL, args) + return self + + def set_gsm_signal(self, strength: int) -> Self: + """Set GSM signal strength (Emulator only) + + Android only. + + Args: + strength: Signal strength. + A member of the enum :obj:`appium.webdriver.extensions.android.gsm.GsmSignalStrength` + + Usage: + self.driver.set_gsm_signal(GsmSignalStrength.GOOD) + + Returns: + Union['WebDriver', 'Gsm']: Self instance + """ + ext_name = 'mobile: gsmSignal' + constants = extract_const_attributes(GsmSignalStrength) + if strength not in constants.values(): + logger.warning( + f'{strength} is out of range. Consider using one of {list(constants.keys())} constants. ' + f'(e.g. {GsmSignalStrength.__name__}.GOOD)' + ) + try: + self.assert_extension_exists(ext_name).execute_script(ext_name, {'strength': strength}) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute( + Command.SET_GSM_SIGNAL, {'signalStrength': strength, 'signalStrengh': strength} + ) + return self + + def set_gsm_voice(self, state: str) -> Self: + """Set GSM voice state (Emulator only) + + Android only. + + Args: + state: State of GSM voice. + A member of the const `appium.webdriver.extensions.android.gsm.GsmVoiceState` + + Usage: + self.driver.set_gsm_voice(GsmVoiceState.HOME) + + Returns: + Union['WebDriver', 'Gsm']: Self instance + """ + ext_name = 'mobile: gmsVoice' + constants = extract_const_attributes(GsmVoiceState) + if state not in constants.values(): + logger.warning( + f'{state} is unknown. Consider using one of {list(constants.keys())} constants. ' + f'(e.g. {GsmVoiceState.__name__}.HOME)' + ) + args = {'state': state} + try: + self.assert_extension_exists(ext_name).execute_script(ext_name, args) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.SET_GSM_VOICE, args) + return self + + def _add_commands(self) -> None: + self.command_executor.add_command(Command.MAKE_GSM_CALL, 'POST', '/session/$sessionId/appium/device/gsm_call') + self.command_executor.add_command(Command.SET_GSM_SIGNAL, 'POST', '/session/$sessionId/appium/device/gsm_signal') + self.command_executor.add_command(Command.SET_GSM_VOICE, 'POST', '/session/$sessionId/appium/device/gsm_voice') diff --git a/appium/webdriver/extensions/android/nativekey.py b/appium/webdriver/extensions/android/nativekey.py new file mode 100644 index 00000000..05b55879 --- /dev/null +++ b/appium/webdriver/extensions/android/nativekey.py @@ -0,0 +1,1119 @@ +#!/usr/bin/env python + +# 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. + + +class AndroidKey: + # Key code constant: Unknown key code. + UNKNOWN = 0 + + # Key code constant: Soft Left key. + # Usually situated below the display on phones and used as a multi-function + # feature key for selecting a software defined function shown on the bottom left + # of the display. + SOFT_LEFT = 1 + + # Key code constant: Soft Right key. + # Usually situated below the display on phones and used as a multi-function + # feature key for selecting a software defined function shown on the bottom right + # of the display. + SOFT_RIGHT = 2 + + # Key code constant: Home key. + # This key is handled by the framework and is never delivered to applications. + HOME = 3 + + # Key code constant: Back key. + BACK = 4 + + # Key code constant: Call key. + CALL = 5 + + # Key code constant: End Call key. + ENDCALL = 6 + + # Key code constant: '0' key. + DIGIT_0 = 7 + + # Key code constant: '1' key. + DIGIT_1 = 8 + + # Key code constant: '2' key. + DIGIT_2 = 9 + + # Key code constant: '3' key. + DIGIT_3 = 10 + + # Key code constant: '4' key. + DIGIT_4 = 11 + + # Key code constant: '5' key. + DIGIT_5 = 12 + + # Key code constant: '6' key. + DIGIT_6 = 13 + + # Key code constant: '7' key. + DIGIT_7 = 14 + + # Key code constant: '8' key. + DIGIT_8 = 15 + + # Key code constant: '9' key. + DIGIT_9 = 16 + + # Key code constant: '*' key. + STAR = 17 + + # Key code constant: '#' key. + POUND = 18 + + # Key code constant: Directional Pad Up key. + # May also be synthesized from trackball motions. + DPAD_UP = 19 + + # Key code constant: Directional Pad Down key. + # May also be synthesized from trackball motions. + DPAD_DOWN = 20 + + # Key code constant: Directional Pad Left key. + # May also be synthesized from trackball motions. + DPAD_LEFT = 21 + + # Key code constant: Directional Pad Right key. + # May also be synthesized from trackball motions. + DPAD_RIGHT = 22 + + # Key code constant: Directional Pad Center key. + # May also be synthesized from trackball motions. + DPAD_CENTER = 23 + + # Key code constant: Volume Up key. + # Adjusts the speaker volume up. + VOLUME_UP = 24 + + # Key code constant: Volume Down key. + # Adjusts the speaker volume down. + VOLUME_DOWN = 25 + + # Key code constant: Power key. + POWER = 26 + + # Key code constant: Camera key. + # Used to launch a camera application or take pictures. + CAMERA = 27 + + # Key code constant: Clear key. + CLEAR = 28 + + # Key code constant: 'A' key. + A = 29 + + # Key code constant: 'B' key. + B = 30 + + # Key code constant: 'C' key. + C = 31 + + # Key code constant: 'D' key. + D = 32 + + # Key code constant: 'E' key. + E = 33 + + # Key code constant: 'F' key. + F = 34 + + # Key code constant: 'G' key. + G = 35 + + # Key code constant: 'H' key. + H = 36 + + # Key code constant: 'I' key. + I = 37 + + # Key code constant: 'J' key. + J = 38 + + # Key code constant: 'K' key. + K = 39 + + # Key code constant: 'L' key. + L = 40 + + # Key code constant: 'M' key. + M = 41 + + # Key code constant: 'N' key. + N = 42 + + # Key code constant: 'O' key. + O = 43 + + # Key code constant: 'P' key. + P = 44 + + # Key code constant: 'Q' key. + Q = 45 + + # Key code constant: 'R' key. + R = 46 + + # Key code constant: 'S' key. + S = 47 + + # Key code constant: 'T' key. + T = 48 + + # Key code constant: 'U' key. + U = 49 + + # Key code constant: 'V' key. + V = 50 + + # Key code constant: 'W' key. + W = 51 + + # Key code constant: 'X' key. + X = 52 + + # Key code constant: 'Y' key. + Y = 53 + + # Key code constant: 'Z' key. + Z = 54 + + # Key code constant: ',' key. + COMMA = 55 + + # Key code constant: '.' key. + PERIOD = 56 + + # Key code constant: Left Alt modifier key. + ALT_LEFT = 57 + + # Key code constant: Right Alt modifier key. + ALT_RIGHT = 58 + + # Key code constant: Left Shift modifier key. + SHIFT_LEFT = 59 + + # Key code constant: Right Shift modifier key. + SHIFT_RIGHT = 60 + + # Key code constant: Tab key. + TAB = 61 + + # Key code constant: Space key. + SPACE = 62 + + # Key code constant: Symbol modifier key. + # Used to enter alternate symbols. + SYM = 63 + + # Key code constant: Explorer special function key. + # Used to launch a browser application. + EXPLORER = 64 + + # Key code constant: Envelope special function key. + # Used to launch a mail application. + ENVELOPE = 65 + + # Key code constant: Enter key. + ENTER = 66 + + # Key code constant: Backspace key. + # Deletes characters before the insertion point, unlike {@link #FORWARD_DEL}. + DEL = 67 + + # Key code constant: '`' (backtick) key. + GRAVE = 68 + + # Key code constant: '-'. + MINUS = 69 + + # Key code constant: '=' key. + EQUALS = 70 + + # Key code constant: '[' key. + LEFT_BRACKET = 71 + + # Key code constant: ']' key. + RIGHT_BRACKET = 72 + + # Key code constant: '\' key. + BACKSLASH = 73 + + # Key code constant: ';' key. + SEMICOLON = 74 + + # Key code constant: ''' (apostrophe) key. + APOSTROPHE = 75 + + # Key code constant: '/' key. + SLASH = 76 + + # Key code constant: '@' key. + AT = 77 + + # Key code constant: Number modifier key. + # Used to enter numeric symbols. + # This key is not Num Lock; it is more like {@link #ALT_LEFT} and is + # interpreted as an ALT key + NUM = 78 + + # Key code constant: Headset Hook key. + # Used to hang up calls and stop media. + HEADSETHOOK = 79 + + # Key code constant: Camera Focus key. + # Used to focus the camera. + FOCUS = 80 # *Camera* focus + + # Key code constant: '+' key. + PLUS = 81 + + # Key code constant: Menu key. + MENU = 82 + + # Key code constant: Notification key. + NOTIFICATION = 83 + + # Key code constant: Search key. + SEARCH = 84 + + # Key code constant: Play/Pause media key. + MEDIA_PLAY_PAUSE = 85 + + # Key code constant: Stop media key. + MEDIA_STOP = 86 + + # Key code constant: Play Next media key. + MEDIA_NEXT = 87 + + # Key code constant: Play Previous media key. + MEDIA_PREVIOUS = 88 + + # Key code constant: Rewind media key. + MEDIA_REWIND = 89 + + # Key code constant: Fast Forward media key. + MEDIA_FAST_FORWARD = 90 + + # Key code constant: Mute key. + # Mutes the microphone, unlike {@link #VOLUME_MUTE}. + MUTE = 91 + + # Key code constant: Page Up key. + PAGE_UP = 92 + + # Key code constant: Page Down key. + PAGE_DOWN = 93 + + # Key code constant: Picture Symbols modifier key. + # Used to switch symbol sets (Emoji, Kao-moji). + + PICTSYMBOLS = 94 # switch symbol-sets (Emoji,Kao-moji) + + # Key code constant: Switch Charset modifier key. + # Used to switch character sets (Kanji, Katakana). + + SWITCH_CHARSET = 95 # switch char-sets (Kanji,Katakana) + + # Key code constant: A Button key. + # On a game controller, the A button should be either the button labeled A + # or the first button on the bottom row of controller buttons. + BUTTON_A = 96 + + # Key code constant: B Button key. + # On a game controller, the B button should be either the button labeled B + # or the second button on the bottom row of controller buttons. + BUTTON_B = 97 + + # Key code constant: C Button key. + # On a game controller, the C button should be either the button labeled C + # or the third button on the bottom row of controller buttons. + BUTTON_C = 98 + + # Key code constant: X Button key. + # On a game controller, the X button should be either the button labeled X + # or the first button on the upper row of controller buttons. + BUTTON_X = 99 + + # Key code constant: Y Button key. + # On a game controller, the Y button should be either the button labeled Y + # or the second button on the upper row of controller buttons. + BUTTON_Y = 100 + + # Key code constant: Z Button key. + # On a game controller, the Z button should be either the button labeled Z + # or the third button on the upper row of controller buttons. + BUTTON_Z = 101 + + # Key code constant: L1 Button key. + # On a game controller, the L1 button should be either the button labeled L1 (or L) + # or the top left trigger button. + BUTTON_L1 = 102 + + # Key code constant: R1 Button key. + # On a game controller, the R1 button should be either the button labeled R1 (or R) + # or the top right trigger button. + BUTTON_R1 = 103 + + # Key code constant: L2 Button key. + # On a game controller, the L2 button should be either the button labeled L2 + # or the bottom left trigger button. + BUTTON_L2 = 104 + + # Key code constant: R2 Button key. + # On a game controller, the R2 button should be either the button labeled R2 + # or the bottom right trigger button. + BUTTON_R2 = 105 + + # Key code constant: Left Thumb Button key. + # On a game controller, the left thumb button indicates that the left (or only) + # joystick is pressed. + BUTTON_THUMBL = 106 + + # Key code constant: Right Thumb Button key. + # On a game controller, the right thumb button indicates that the right + # joystick is pressed. + BUTTON_THUMBR = 107 + + # Key code constant: Start Button key. + # On a game controller, the button labeled Start. + BUTTON_START = 108 + + # Key code constant: Select Button key. + # On a game controller, the button labeled Select. + BUTTON_SELECT = 109 + + # Key code constant: Mode Button key. + # On a game controller, the button labeled Mode. + BUTTON_MODE = 110 + + # Key code constant: Escape key. + ESCAPE = 111 + + # Key code constant: Forward Delete key. + # Deletes characters ahead of the insertion point, unlike {@link #DEL}. + FORWARD_DEL = 112 + + # Key code constant: Left Control modifier key. + CTRL_LEFT = 113 + + # Key code constant: Right Control modifier key. + CTRL_RIGHT = 114 + + # Key code constant: Caps Lock key. + CAPS_LOCK = 115 + + # Key code constant: Scroll Lock key. + SCROLL_LOCK = 116 + + # Key code constant: Left Meta modifier key. + META_LEFT = 117 + + # Key code constant: Right Meta modifier key. + META_RIGHT = 118 + + # Key code constant: Function modifier key. + FUNCTION = 119 + + # Key code constant: System Request / Print Screen key. + SYSRQ = 120 + + # Key code constant: Break / Pause key. + BREAK = 121 + + # Key code constant: Home Movement key. + # Used for scrolling or moving the cursor around to the start of a line + # or to the top of a list. + MOVE_HOME = 122 + + # Key code constant: End Movement key. + # Used for scrolling or moving the cursor around to the end of a line + # or to the bottom of a list. + MOVE_END = 123 + + # Key code constant: Insert key. + # Toggles insert / overwrite edit mode. + INSERT = 124 + + # Key code constant: Forward key. + # Navigates forward in the history stack. Complement of {@link #BACK}. + FORWARD = 125 + + # Key code constant: Play media key. + MEDIA_PLAY = 126 + + # Key code constant: Pause media key. + MEDIA_PAUSE = 127 + + # Key code constant: Close media key. + # May be used to close a CD tray, for example. + MEDIA_CLOSE = 128 + + # Key code constant: Eject media key. + # May be used to eject a CD tray, for example. + MEDIA_EJECT = 129 + + # Key code constant: Record media key. + MEDIA_RECORD = 130 + + # Key code constant: F1 key. + F1 = 131 + + # Key code constant: F2 key. + F2 = 132 + + # Key code constant: F3 key. + F3 = 133 + + # Key code constant: F4 key. + F4 = 134 + + # Key code constant: F5 key. + F5 = 135 + + # Key code constant: F6 key. + F6 = 136 + + # Key code constant: F7 key. + F7 = 137 + + # Key code constant: F8 key. + F8 = 138 + + # Key code constant: F9 key. + F9 = 139 + + # Key code constant: F10 key. + F10 = 140 + + # Key code constant: F11 key. + F11 = 141 + + # Key code constant: F12 key. + F12 = 142 + + # Key code constant: Num Lock key. + # This is the Num Lock key; it is different from {@link #NUM}. + # This key alters the behavior of other keys on the numeric keypad. + NUM_LOCK = 143 + + # Key code constant: Numeric keypad '0' key. + NUMPAD_0 = 144 + + # Key code constant: Numeric keypad '1' key. + NUMPAD_1 = 145 + + # Key code constant: Numeric keypad '2' key. + NUMPAD_2 = 146 + + # Key code constant: Numeric keypad '3' key. + NUMPAD_3 = 147 + + # Key code constant: Numeric keypad '4' key. + NUMPAD_4 = 148 + + # Key code constant: Numeric keypad '5' key. + NUMPAD_5 = 149 + + # Key code constant: Numeric keypad '6' key. + NUMPAD_6 = 150 + + # Key code constant: Numeric keypad '7' key. + NUMPAD_7 = 151 + + # Key code constant: Numeric keypad '8' key. + NUMPAD_8 = 152 + + # Key code constant: Numeric keypad '9' key. + NUMPAD_9 = 153 + + # Key code constant: Numeric keypad '/' key (for division). + NUMPAD_DIVIDE = 154 + + # Key code constant: Numeric keypad '#' key (for multiplication). + NUMPAD_MULTIPLY = 155 + + # Key code constant: Numeric keypad '-' key (for subtraction). + NUMPAD_SUBTRACT = 156 + + # Key code constant: Numeric keypad '+' key (for addition). + NUMPAD_ADD = 157 + + # Key code constant: Numeric keypad '.' key (for decimals or digit grouping). + NUMPAD_DOT = 158 + + # Key code constant: Numeric keypad ',' key (for decimals or digit grouping). + NUMPAD_COMMA = 159 + + # Key code constant: Numeric keypad Enter key. + NUMPAD_ENTER = 160 + + # Key code constant: Numeric keypad '=' key. + NUMPAD_EQUALS = 161 + + # Key code constant: Numeric keypad '(' key. + NUMPAD_LEFT_PAREN = 162 + + # Key code constant: Numeric keypad ')' key. + NUMPAD_RIGHT_PAREN = 163 + + # Key code constant: Volume Mute key. + # Mutes the speaker, unlike {@link #MUTE}. + # This key should normally be implemented as a toggle such that the first press + # mutes the speaker and the second press restores the original volume. + VOLUME_MUTE = 164 + + # Key code constant: Info key. + # Common on TV remotes to show additional information related to what is + # currently being viewed. + INFO = 165 + + # Key code constant: Channel up key. + # On TV remotes, increments the television channel. + CHANNEL_UP = 166 + + # Key code constant: Channel down key. + # On TV remotes, decrements the television channel. + CHANNEL_DOWN = 167 + + # Key code constant: Zoom in key. + KEYCODE_ZOOM_IN = 168 + + # Key code constant: Zoom out key. + KEYCODE_ZOOM_OUT = 169 + + # Key code constant: TV key. + # On TV remotes, switches to viewing live TV. + TV = 170 + + # Key code constant: Window key. + # On TV remotes, toggles picture-in-picture mode or other windowing functions. + WINDOW = 171 + + # Key code constant: Guide key. + # On TV remotes, shows a programming guide. + GUIDE = 172 + + # Key code constant: DVR key. + # On some TV remotes, switches to a DVR mode for recorded shows. + DVR = 173 + + # Key code constant: Bookmark key. + # On some TV remotes, bookmarks content or web pages. + BOOKMARK = 174 + + # Key code constant: Toggle captions key. + # Switches the mode for closed-captioning text, for example during television shows. + CAPTIONS = 175 + + # Key code constant: Settings key. + # Starts the system settings activity. + SETTINGS = 176 + + # Key code constant: TV power key. + # On TV remotes, toggles the power on a television screen. + TV_POWER = 177 + + # Key code constant: TV input key. + # On TV remotes, switches the input on a television screen. + TV_INPUT = 178 + + # Key code constant: Set-top-box power key. + # On TV remotes, toggles the power on an external Set-top-box. + STB_POWER = 179 + + # Key code constant: Set-top-box input key. + # On TV remotes, switches the input mode on an external Set-top-box. + STB_INPUT = 180 + + # Key code constant: A/V Receiver power key. + # On TV remotes, toggles the power on an external A/V Receiver. + AVR_POWER = 181 + + # Key code constant: A/V Receiver input key. + # On TV remotes, switches the input mode on an external A/V Receiver. + AVR_INPUT = 182 + + # Key code constant: Red "programmable" key. + # On TV remotes, acts as a contextual/programmable key. + PROG_RED = 183 + + # Key code constant: Green "programmable" key. + # On TV remotes, actsas a contextual/programmable key. + PROG_GREEN = 184 + + # Key code constant: Yellow "programmable" key. + # On TV remotes, acts as a contextual/programmable key. + PROG_YELLOW = 185 + + # Key code constant: Blue "programmable" key. + # On TV remotes, acts as a contextual/programmable key. + PROG_BLUE = 186 + + # Key code constant: App switch key. + # Should bring up the application switcher dialog. + APP_SWITCH = 187 + + # Key code constant: Generic Game Pad Button #1. + BUTTON_1 = 188 + + # Key code constant: Generic Game Pad Button #2. + BUTTON_2 = 189 + + # Key code constant: Generic Game Pad Button #3. + BUTTON_3 = 190 + + # Key code constant: Generic Game Pad Button #4. + BUTTON_4 = 191 + + # Key code constant: Generic Game Pad Button #5. + BUTTON_5 = 192 + + # Key code constant: Generic Game Pad Button #6. + BUTTON_6 = 193 + + # Key code constant: Generic Game Pad Button #7. + BUTTON_7 = 194 + + # Key code constant: Generic Game Pad Button #8. + BUTTON_8 = 195 + + # Key code constant: Generic Game Pad Button #9. + BUTTON_9 = 196 + + # Key code constant: Generic Game Pad Button #10. + BUTTON_10 = 197 + + # Key code constant: Generic Game Pad Button #11. + BUTTON_11 = 198 + + # Key code constant: Generic Game Pad Button #12. + BUTTON_12 = 199 + + # Key code constant: Generic Game Pad Button #13. + BUTTON_13 = 200 + + # Key code constant: Generic Game Pad Button #14. + BUTTON_14 = 201 + + # Key code constant: Generic Game Pad Button #15. + BUTTON_15 = 202 + + # Key code constant: Generic Game Pad Button #16. + BUTTON_16 = 203 + + # Key code constant: Language Switch key. + # Toggles the current input language such as switching between English and Japanese on + # a QWERTY keyboard. On some devices, the same function may be performed by + # pressing Shift+Spacebar. + LANGUAGE_SWITCH = 204 + + # Key code constant: Manner Mode key. + # Toggles silent or vibrate mode on and off to make the device behave more politely + # in certain settings such as on a crowded train. On some devices, the key may only + # operate when long-pressed. + MANNER_MODE = 205 + + # Key code constant: 3D Mode key. + # Toggles the display between 2D and 3D mode. + MODE_3D = 206 + + # Key code constant: Contacts special function key. + # Used to launch an address book application. + CONTACTS = 207 + + # Key code constant: Calendar special function key. + # Used to launch a calendar application. + CALENDAR = 208 + + # Key code constant: Music special function key. + # Used to launch a music player application. + MUSIC = 209 + + # Key code constant: Calculator special function key. + # Used to launch a calculator application. + CALCULATOR = 210 + + # Key code constant: Japanese full-width / half-width key. + ZENKAKU_HANKAKU = 211 + + # Key code constant: Japanese alphanumeric key. + EISU = 212 + + # Key code constant: Japanese non-conversion key. + MUHENKAN = 213 + + # Key code constant: Japanese conversion key. + HENKAN = 214 + + # Key code constant: Japanese katakana / hiragana key. + KATAKANA_HIRAGANA = 215 + + # Key code constant: Japanese Yen key. + YEN = 216 + + # Key code constant: Japanese Ro key. + RO = 217 + + # Key code constant: Japanese kana key. + KANA = 218 + + # Key code constant: Assist key. + # Launches the global assist activity. Not delivered to applications. + ASSIST = 219 + + # Key code constant: Brightness Down key. + # Adjusts the screen brightness down. + BRIGHTNESS_DOWN = 220 + + # Key code constant: Brightness Up key. + # Adjusts the screen brightness up. + BRIGHTNESS_UP = 221 + + # Key code constant: Audio Track key. + # Switches the audio tracks. + MEDIA_AUDIO_TRACK = 222 + + # Key code constant: Sleep key. + # Puts the device to sleep. Behaves somewhat like {@link #POWER} but it + # has no effect if the device is already asleep. + SLEEP = 223 + + # Key code constant: Wakeup key. + # Wakes up the device. Behaves somewhat like {@link #POWER} but it + # has no effect if the device is already awake. + WAKEUP = 224 + + # Key code constant: Pairing key. + # Initiates peripheral pairing mode. Useful for pairing remote control + # devices or game controllers, especially if no other input mode is + # available. + PAIRING = 225 + + # Key code constant: Media Top Menu key. + # Goes to the top of media menu. + MEDIA_TOP_MENU = 226 + + # Key code constant: '11' key. + KEY_11 = 227 + + # Key code constant: '12' key. + KEY_12 = 228 + + # Key code constant: Last Channel key. + # Goes to the last viewed channel. + LAST_CHANNEL = 229 + + # Key code constant: TV data service key. + # Displays data services like weather, sports. + TV_DATA_SERVICE = 230 + + # Key code constant: Voice Assist key. + # Launches the global voice assist activity. Not delivered to applications. + VOICE_ASSIST = 231 + + # Key code constant: Radio key. + # Toggles TV service / Radio service. + TV_RADIO_SERVICE = 232 + + # Key code constant: Teletext key. + # Displays Teletext service. + TV_TELETEXT = 233 + + # Key code constant: Number entry key. + # Initiates to enter multi-digit channel nubmber when each digit key is assigned + # for selecting separate channel. Corresponds to Number Entry Mode (0x1D) of CEC + # User Control Code. + TV_NUMBER_ENTRY = 234 + + # Key code constant: Analog Terrestrial key. + # Switches to analog terrestrial broadcast service. + TV_TERRESTRIAL_ANALOG = 235 + + # Key code constant: Digital Terrestrial key. + # Switches to digital terrestrial broadcast service. + TV_TERRESTRIAL_DIGITAL = 236 + + # Key code constant: Satellite key. + # Switches to digital satellite broadcast service. + TV_SATELLITE = 237 + + # Key code constant: BS key. + # Switches to BS digital satellite broadcasting service available in Japan. + TV_SATELLITE_BS = 238 + + # Key code constant: CS key. + # Switches to CS digital satellite broadcasting service available in Japan. + TV_SATELLITE_CS = 239 + + # Key code constant: BS/CS key. + # Toggles between BS and CS digital satellite services. + TV_SATELLITE_SERVICE = 240 + + # Key code constant: Toggle Network key. + # Toggles selecting broadcast services. + TV_NETWORK = 241 + + # Key code constant: Antenna/Cable key. + # Toggles broadcast input source between antenna and cable. + TV_ANTENNA_CABLE = 242 + + # Key code constant: HDMI #1 key. + # Switches to HDMI input #1. + TV_INPUT_HDMI_1 = 243 + + # Key code constant: HDMI #2 key. + # Switches to HDMI input #2. + TV_INPUT_HDMI_2 = 244 + + # Key code constant: HDMI #3 key. + # Switches to HDMI input #3. + TV_INPUT_HDMI_3 = 245 + + # Key code constant: HDMI #4 key. + # Switches to HDMI input #4. + TV_INPUT_HDMI_4 = 246 + + # Key code constant: Composite #1 key. + # Switches to composite video input #1. + TV_INPUT_COMPOSITE_1 = 247 + + # Key code constant: Composite #2 key. + # Switches to composite video input #2. + TV_INPUT_COMPOSITE_2 = 248 + + # Key code constant: Component #1 key. + # Switches to component video input #1. + TV_INPUT_COMPONENT_1 = 249 + + # Key code constant: Component #2 key. + # Switches to component video input #2. + TV_INPUT_COMPONENT_2 = 250 + + # Key code constant: VGA #1 key. + # Switches to VGA (analog RGB) input #1. + TV_INPUT_VGA_1 = 251 + + # Key code constant: Audio description key. + # Toggles audio description off / on. + TV_AUDIO_DESCRIPTION = 252 + + # Key code constant: Audio description mixing volume up key. + # Louden audio description volume as compared with normal audio volume. + TV_AUDIO_DESCRIPTION_MIX_UP = 253 + + # Key code constant: Audio description mixing volume down key. + # Lessen audio description volume as compared with normal audio volume. + TV_AUDIO_DESCRIPTION_MIX_DOWN = 254 + + # Key code constant: Zoom mode key. + # Changes Zoom mode (Normal, Full, Zoom, Wide-zoom, etc.) + TV_ZOOM_MODE = 255 + + # Key code constant: Contents menu key. + # Goes to the title list. Corresponds to Contents Menu (0x0B) of CEC User Control + # Code + TV_CONTENTS_MENU = 256 + + # Key code constant: Media context menu key. + # Goes to the context menu of media contents. Corresponds to Media Context-sensitive + # Menu (0x11) of CEC User Control Code. + TV_MEDIA_CONTEXT_MENU = 257 + + # Key code constant: Timer programming key. + # Goes to the timer recording menu. Corresponds to Timer Programming (0x54) of + # CEC User Control Code. + TV_TIMER_PROGRAMMING = 258 + + # Key code constant: Help key. + HELP = 259 + + # Key code constant: Navigate to previous key. + # Goes backward by one item in an ordered collection of items. + NAVIGATE_PREVIOUS = 260 + + # Key code constant: Navigate to next key. + # Advances to the next item in an ordered collection of items. + NAVIGATE_NEXT = 261 + + # Key code constant: Navigate in key. + # Activates the item that currently has focus or expands to the next level of a navigation + # hierarchy. + NAVIGATE_IN = 262 + + # Key code constant: Navigate out key. + # Backs out one level of a navigation hierarchy or collapses the item that currently has + # focus. + NAVIGATE_OUT = 263 + + # Key code constant: Primary stem key for Wear. + # Main power/reset button on watch. + STEM_PRIMARY = 264 + + # Key code constant: Generic stem key 1 for Wear. + STEM_1 = 265 + + # Key code constant: Generic stem key 2 for Wear. + STEM_2 = 266 + + # Key code constant: Generic stem key 3 for Wear. + STEM_3 = 267 + + # Key code constant: Directional Pad Up-Left. + DPAD_UP_LEFT = 268 + + # Key code constant: Directional Pad Down-Left. + DPAD_DOWN_LEFT = 269 + + # Key code constant: Directional Pad Up-Right. + DPAD_UP_RIGHT = 270 + + # Key code constant: Directional Pad Down-Right. + DPAD_DOWN_RIGHT = 271 + + # Key code constant: Skip forward media key. + MEDIA_SKIP_FORWARD = 272 + + # Key code constant: Skip backward media key. + MEDIA_SKIP_BACKWARD = 273 + + # Key code constant: Step forward media key. + # Steps media forward, one frame at a time. + MEDIA_STEP_FORWARD = 274 + + # Key code constant: Step backward media key. + # Steps media backward, one frame at a time. + MEDIA_STEP_BACKWARD = 275 + + # Key code constant: put device to sleep unless a wakelock is held. + SOFT_SLEEP = 276 + + # Key code constant: Cut key. + CUT = 277 + + # Key code constant: Copy key. + COPY = 278 + + gamepad_buttons = [ + BUTTON_A, + BUTTON_B, + BUTTON_C, + BUTTON_X, + BUTTON_Y, + BUTTON_Z, + BUTTON_L1, + BUTTON_R1, + BUTTON_L2, + BUTTON_R2, + BUTTON_THUMBL, + BUTTON_THUMBR, + BUTTON_START, + BUTTON_SELECT, + BUTTON_MODE, + BUTTON_1, + BUTTON_2, + BUTTON_3, + BUTTON_4, + BUTTON_5, + BUTTON_6, + BUTTON_7, + BUTTON_8, + BUTTON_9, + BUTTON_10, + BUTTON_11, + BUTTON_12, + BUTTON_13, + BUTTON_14, + BUTTON_15, + BUTTON_16, + ] + + @staticmethod + def is_gamepad_button(code: int) -> bool: + """Returns true if the specified nativekey is a gamepad button.""" + return code in AndroidKey.gamepad_buttons + + confirm_buttons = [DPAD_CENTER, ENTER, SPACE, NUMPAD_ENTER] + + @staticmethod + def is_confirm_key(code: int) -> bool: + """Returns true if the key will, by default, trigger a click on the focused view.""" + return code in AndroidKey.confirm_buttons + + media_buttons = [ + MEDIA_PLAY, + MEDIA_PAUSE, + MEDIA_PLAY_PAUSE, + MUTE, + HEADSETHOOK, + MEDIA_STOP, + MEDIA_NEXT, + MEDIA_PREVIOUS, + MEDIA_REWIND, + MEDIA_RECORD, + MEDIA_FAST_FORWARD, + ] + + @staticmethod + def is_media_key(code: int) -> bool: + """Returns true if this key is a media key, which can be send to apps that are + interested in media key events.""" + return code in AndroidKey.media_buttons + + system_buttons = [ + MENU, + SOFT_RIGHT, + HOME, + BACK, + CALL, + ENDCALL, + VOLUME_UP, + VOLUME_DOWN, + VOLUME_MUTE, + MUTE, + POWER, + HEADSETHOOK, + MEDIA_PLAY, + MEDIA_PAUSE, + MEDIA_PLAY_PAUSE, + MEDIA_STOP, + MEDIA_NEXT, + MEDIA_PREVIOUS, + MEDIA_REWIND, + MEDIA_RECORD, + MEDIA_FAST_FORWARD, + CAMERA, + FOCUS, + SEARCH, + BRIGHTNESS_DOWN, + BRIGHTNESS_UP, + MEDIA_AUDIO_TRACK, + ] + + @staticmethod + def is_system_key(code: int) -> bool: + """Returns true if the key is a system key, System keys can not be used for menu shortcuts.""" + return code in AndroidKey.system_buttons + + wake_buttons = [BACK, MENU, WAKEUP, PAIRING, STEM_1, STEM_2, STEM_3] + + @staticmethod + def is_wake_key(code: int) -> bool: + """Returns true if the key is a wake key.""" + return code in AndroidKey.wake_buttons diff --git a/appium/webdriver/extensions/android/network.py b/appium/webdriver/extensions/android/network.py new file mode 100644 index 00000000..6054e29d --- /dev/null +++ b/appium/webdriver/extensions/android/network.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python + +# 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. + +from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self + +from appium.common.helper import extract_const_attributes +from appium.common.logger import logger +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence +from appium.webdriver.mobilecommand import MobileCommand as Command + + +class NetSpeed: + GSM = 'gsm' # GSM/CSD (up: 14.4(kbps), down: 14.4(kbps)) + SCSD = 'scsd' # HSCSD (up: 14.4, down: 57.6) + GPRS = 'gprs' # GPRS (up: 28.8, down: 57.6) + EDGE = 'edge' # EDGE/EGPRS (up: 473.6, down: 473.6) + UMTS = 'umts' # UMTS/3G (up: 384.0, down: 384.0) + HSDPA = 'hsdpa' # HSDPA (up: 5760.0, down: 13,980.0) + LTE = 'lte' # LTE (up: 58,000, down: 173,000) + EVDO = 'evdo' # EVDO (up: 75,000, down: 280,000) + FULL = 'full' # No limit, the default (up: 0.0, down: 0.0) + + +class NetworkMask: + WIFI = 0b010 + DATA = 0b100 + AIRPLANE_MODE = 0b001 + + +class Network(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + @property + def network_connection(self) -> int: + """Returns an integer bitmask specifying the network connection type. + + Android only. + Possible values are available through the enumeration `appium.webdriver.ConnectionType` + + This API only works reliably on emulators (any version) and real devices + since API level 31. + """ + ext_name = 'mobile: getConnectivity' + try: + result_map = self.assert_extension_exists(ext_name).execute_script(ext_name) + return ( + (NetworkMask.WIFI if result_map['wifi'] else 0) + | (NetworkMask.DATA if result_map['data'] else 0) + | (NetworkMask.AIRPLANE_MODE if result_map['airplaneMode'] else 0) + ) + except UnknownMethodException: + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute(Command.GET_NETWORK_CONNECTION, {})['value'] + + def set_network_connection(self, connection_type: int) -> int: + """Sets the network connection type. Android only. + + Possible values: + + +--------------------+------+------+---------------+ + | Value (Alias) | Data | Wifi | Airplane Mode | + +====================+======+======+===============+ + | 0 (None) | 0 | 0 | 0 | + +--------------------+------+------+---------------+ + | 1 (Airplane Mode) | 0 | 0 | 1 | + +--------------------+------+------+---------------+ + | 2 (Wifi only) | 0 | 1 | 0 | + +--------------------+------+------+---------------+ + | 4 (Data only) | 1 | 0 | 0 | + +--------------------+------+------+---------------+ + | 6 (All network on) | 1 | 1 | 0 | + +--------------------+------+------+---------------+ + + These are available through the enumeration `appium.webdriver.ConnectionType` + + This API only works reliably on emulators (any version) and real devices + since API level 31. + + Args: + connection_type: a member of the enum `appium.webdriver.ConnectionType` + + Return: + int: Set network connection type + """ + ext_name = 'mobile: setConnectivity' + try: + return self.assert_extension_exists(ext_name).execute_script( + ext_name, + { + 'wifi': bool(connection_type & NetworkMask.WIFI), + 'data': bool(connection_type & NetworkMask.DATA), + 'airplaneMode': bool(connection_type & NetworkMask.AIRPLANE_MODE), + }, + ) + except UnknownMethodException: + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute( + Command.SET_NETWORK_CONNECTION, {'parameters': {'type': connection_type}} + )['value'] + + def toggle_wifi(self) -> Self: + """Toggle the wifi on the device, Android only. + This API only works reliably on emulators (any version) and real devices + since API level 31. + + Returns: + Union['WebDriver', 'Network']: Self instance + """ + ext_name = 'mobile: setConnectivity' + try: + self.assert_extension_exists(ext_name).execute_script( + ext_name, {'wifi': not (self.network_connection & NetworkMask.WIFI)} + ) + except UnknownMethodException: + self.mark_extension_absence(ext_name).execute(Command.TOGGLE_WIFI, {}) + return self + + def set_network_speed(self, speed_type: str) -> Self: + """Set the network speed emulation. + + Android Emulator only. + + Args: + speed_type: The network speed type. + A member of the const appium.webdriver.extensions.android.network.NetSpeed. + + Usage: + self.driver.set_network_speed(NetSpeed.LTE) + + Returns: + Union['WebDriver', 'Network']: Self instance + """ + constants = extract_const_attributes(NetSpeed) + if speed_type not in constants.values(): + logger.warning( + f'{speed_type} is unknown. Consider using one of {list(constants.keys())} constants. ' + f'(e.g. {NetSpeed.__name__}.LTE)' + ) + ext_name = 'mobile: networkSpeed' + try: + self.assert_extension_exists(ext_name).execute_script(ext_name, {'speed': speed_type}) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.SET_NETWORK_SPEED, {'netspeed': speed_type}) + return self + + def _add_commands(self) -> None: + self.command_executor.add_command(Command.TOGGLE_WIFI, 'POST', '/session/$sessionId/appium/device/toggle_wifi') + self.command_executor.add_command( + Command.GET_NETWORK_CONNECTION, + 'GET', + '/session/$sessionId/network_connection', + ) + self.command_executor.add_command( + Command.SET_NETWORK_CONNECTION, + 'POST', + '/session/$sessionId/network_connection', + ) + self.command_executor.add_command( + Command.SET_NETWORK_SPEED, + 'POST', + '/session/$sessionId/appium/device/network_speed', + ) diff --git a/appium/webdriver/extensions/android/performance.py b/appium/webdriver/extensions/android/performance.py new file mode 100644 index 00000000..f5781cf3 --- /dev/null +++ b/appium/webdriver/extensions/android/performance.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +# 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. + +from typing import Dict, List, Union + +from selenium.common.exceptions import UnknownMethodException + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence +from appium.webdriver.mobilecommand import MobileCommand as Command + + +class Performance(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def get_performance_data( + self, package_name: str, data_type: str, data_read_timeout: Union[int, None] = None + ) -> List[List[str]]: + """Returns the information of the system state + which is supported to read as like cpu, memory, network traffic, and battery. + + Android only. + + Args: + package_name: The package name of the application + data_type: The type of system state which wants to read. + It should be one of the supported performance data types. + Check :func:`.get_performance_data_types` for supported types + data_read_timeout: The number of attempts to read + + Usage: + self.driver.get_performance_data('my.app.package', 'cpuinfo', 5) + + Returns: + The data along to `data_type` + """ + ext_name = 'mobile: getPerformanceData' + args: Dict[str, Union[str, int]] = {'packageName': package_name, 'dataType': data_type} + try: + return self.assert_extension_exists(ext_name).execute_script(ext_name, args) + except UnknownMethodException: + # TODO: Remove the fallback + if data_read_timeout is not None: + args['dataReadTimeout'] = data_read_timeout + return self.mark_extension_absence(ext_name).execute(Command.GET_PERFORMANCE_DATA, args)['value'] + + def get_performance_data_types(self) -> List[str]: + """Returns the information types of the system state + which is supported to read as like cpu, memory, network traffic, and battery. + Android only. + + Usage: + self.driver.get_performance_data_types() + + Returns: + Available data types + """ + ext_name = 'mobile: getPerformanceDataTypes' + try: + return self.assert_extension_exists(ext_name).execute_script(ext_name) + except UnknownMethodException: + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute(Command.GET_PERFORMANCE_DATA_TYPES)['value'] + + def _add_commands(self) -> None: + self.command_executor.add_command( + Command.GET_PERFORMANCE_DATA, + 'POST', + '/session/$sessionId/appium/getPerformanceData', + ) + self.command_executor.add_command( + Command.GET_PERFORMANCE_DATA_TYPES, + 'POST', + '/session/$sessionId/appium/performanceData/types', + ) diff --git a/appium/webdriver/extensions/android/power.py b/appium/webdriver/extensions/android/power.py new file mode 100644 index 00000000..537ab680 --- /dev/null +++ b/appium/webdriver/extensions/android/power.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python + +# 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. + +from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence +from appium.webdriver.mobilecommand import MobileCommand as Command + + +class Power(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + AC_OFF, AC_ON = 'off', 'on' + + def set_power_capacity(self, percent: int) -> Self: + """Emulate power capacity change on the connected emulator. + + Android only. + + Args: + percent: The power capacity to be set. Can be set from 0 to 100 + + Usage: + self.driver.set_power_capacity(50) + + Returns: + Union['WebDriver', 'Power']: Self instance + """ + ext_name = 'mobile: powerCapacity' + args = {'percent': percent} + try: + self.assert_extension_exists(ext_name).execute_script(ext_name, args) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.SET_POWER_CAPACITY, args) + return self + + def set_power_ac(self, ac_state: str) -> Self: + """Emulate power state change on the connected emulator. + + Android only. + + Args: + ac_state: The power ac state to be set. Use `Power.AC_OFF`, `Power.AC_ON` + + Usage: + | self.driver.set_power_ac(Power.AC_OFF) + | self.driver.set_power_ac(Power.AC_ON) + + Returns: + Union['WebDriver', 'Power']: Self instance + """ + ext_name = 'mobile: powerAC' + args = {'state': ac_state} + try: + self.assert_extension_exists(ext_name).execute_script(ext_name, args) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.SET_POWER_AC, args) + return self + + def _add_commands(self) -> None: + self.command_executor.add_command( + Command.SET_POWER_CAPACITY, + 'POST', + '/session/$sessionId/appium/device/power_capacity', + ) + self.command_executor.add_command(Command.SET_POWER_AC, 'POST', '/session/$sessionId/appium/device/power_ac') diff --git a/appium/webdriver/extensions/android/sms.py b/appium/webdriver/extensions/android/sms.py new file mode 100644 index 00000000..f5769c56 --- /dev/null +++ b/appium/webdriver/extensions/android/sms.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +# 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. + +from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence +from appium.webdriver.mobilecommand import MobileCommand as Command + + +class Sms(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def send_sms(self, phone_number: str, message: str) -> Self: + """Emulate send SMS event on the connected emulator. + + Android only. + + Args: + phone_number: The phone number of message sender + message: The message to send + + Usage: + self.driver.send_sms('555-123-4567', 'Hey lol') + + Returns: + Union['WebDriver', 'Sms']: Self instance + """ + ext_name = 'mobile: sendSms' + args = {'phoneNumber': phone_number, 'message': message} + try: + self.assert_extension_exists(ext_name).execute_script(ext_name, args) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.SEND_SMS, args) + return self + + def _add_commands(self) -> None: + self.command_executor.add_command(Command.SEND_SMS, 'POST', '/session/$sessionId/appium/device/send_sms') diff --git a/appium/webdriver/extensions/android/system_bars.py b/appium/webdriver/extensions/android/system_bars.py new file mode 100644 index 00000000..a02c21f8 --- /dev/null +++ b/appium/webdriver/extensions/android/system_bars.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python + +# 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. + +from typing import Dict, Union + +from selenium.common.exceptions import UnknownMethodException + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence +from appium.webdriver.mobilecommand import MobileCommand as Command + + +class SystemBars(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def get_system_bars(self) -> Dict[str, Dict[str, Union[int, bool]]]: + """Retrieve visibility and bounds information of the status and navigation bars. + + Android only. + + Returns: + A dictionary whose keys are + - statusBar + - visible + - x + - y + - width + - height + - navigationBar + - visible + - x + - y + - width + - height + """ + ext_name = 'mobile: getSystemBars' + try: + return self.assert_extension_exists(ext_name).execute_script(ext_name) + except UnknownMethodException: + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute(Command.GET_SYSTEM_BARS)['value'] + + def _add_commands(self) -> None: + self.command_executor.add_command( + Command.GET_SYSTEM_BARS, + 'GET', + '/session/$sessionId/appium/device/system_bars', + ) diff --git a/appium/webdriver/extensions/applications.py b/appium/webdriver/extensions/applications.py new file mode 100644 index 00000000..b259422e --- /dev/null +++ b/appium/webdriver/extensions/applications.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python + +# 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. +from typing import Any, Dict, Union + +from selenium.common.exceptions import InvalidArgumentException, UnknownMethodException +from typing_extensions import Self + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence + +from ..mobilecommand import MobileCommand as Command + + +class Applications(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def background_app(self, seconds: int) -> Self: + """Puts the application in the background on the device for a certain duration. + + Args: + seconds: the duration for the application to remain in the background. + Providing a negative value will continue immediately after putting the app + under test to the background. + + Returns: + Union['WebDriver', 'Applications']: Self instance + """ + ext_name = 'mobile: backgroundApp' + args = {'seconds': seconds} + try: + self.assert_extension_exists(ext_name).execute_script(ext_name, args) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.BACKGROUND, args) + return self + + def is_app_installed(self, bundle_id: str) -> bool: + """Checks whether the application specified by `bundle_id` is installed on the device. + + Args: + bundle_id: the id of the application to query + + Returns: + `True` if app is installed + """ + ext_name = 'mobile: isAppInstalled' + try: + return self.assert_extension_exists(ext_name).execute_script( + ext_name, + { + 'bundleId': bundle_id, + 'appId': bundle_id, + }, + ) + except (UnknownMethodException, InvalidArgumentException): + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute( + Command.IS_APP_INSTALLED, + { + 'bundleId': bundle_id, + }, + )['value'] + + def install_app(self, app_path: str, **options: Any) -> Self: + """Install the application found at `app_path` on the device. + + Args: + app_path: the local or remote path to the application to install + + Keyword Args: + replace (bool): [Android only] whether to reinstall/upgrade the package if it is + already present on the device under test. True by default + timeout (int): [Android only] how much time to wait for the installation to complete. + 60000ms by default. + allowTestPackages (bool): [Android only] whether to allow installation of packages marked + as test in the manifest. False by default + useSdcard (bool): [Android only] whether to use the SD card to install the app. False by default + grantPermissions (bool): [Android only] whether to automatically grant application permissions + on Android 6+ after the installation completes. False by default + + Returns: + Union['WebDriver', 'Applications']: Self instance + """ + ext_name = 'mobile: installApp' + try: + self.assert_extension_exists(ext_name).execute_script( + 'mobile: installApp', + { + 'app': app_path, + 'appPath': app_path, + **(options or {}), + }, + ) + except (UnknownMethodException, InvalidArgumentException): + # TODO: Remove the fallback + data: Dict[str, Any] = {'appPath': app_path} + if options: + data.update({'options': options}) + self.mark_extension_absence(ext_name).execute(Command.INSTALL_APP, data) + return self + + def remove_app(self, app_id: str, **options: Any) -> Self: + """Remove the specified application from the device. + + Args: + app_id: the application id to be removed + + Keyword Args: + keepData (bool): [Android only] whether to keep application data and caches after it is uninstalled. + False by default + timeout (int): [Android only] how much time to wait for the uninstall to complete. + 20000ms by default. + + Returns: + Union['WebDriver', 'Applications']: Self instance + """ + ext_name = 'mobile: removeApp' + try: + self.assert_extension_exists(ext_name).execute_script( + ext_name, + { + 'appId': app_id, + 'bundleId': app_id, + **(options or {}), + }, + ) + except (UnknownMethodException, InvalidArgumentException): + # TODO: Remove the fallback + data: Dict[str, Any] = {'appId': app_id} + if options: + data.update({'options': options}) + self.mark_extension_absence(ext_name).execute(Command.REMOVE_APP, data) + return self + + def terminate_app(self, app_id: str, **options: Any) -> bool: + """Terminates the application if it is running. + + Args: + app_id: the application id to be terminates + + Keyword Args: + `timeout` (int): [Android only] how much time to wait for the uninstall to complete. + 500ms by default. + + Returns: + True if the app has been successfully terminated + """ + ext_name = 'mobile: terminateApp' + try: + return self.assert_extension_exists(ext_name).execute_script( + ext_name, + { + 'appId': app_id, + 'bundleId': app_id, + **(options or {}), + }, + ) + except (UnknownMethodException, InvalidArgumentException): + # TODO: Remove the fallback + data: Dict[str, Any] = {'appId': app_id} + if options: + data.update({'options': options}) + return self.mark_extension_absence(ext_name).execute(Command.TERMINATE_APP, data)['value'] + + def activate_app(self, app_id: str) -> Self: + """Activates the application if it is not running + or is running in the background. + + Args: + app_id: the application id to be activated + + Returns: + Union['WebDriver', 'Applications']: Self instance + """ + ext_name = 'mobile: activateApp' + try: + self.assert_extension_exists(ext_name).execute_script( + ext_name, + { + 'appId': app_id, + 'bundleId': app_id, + }, + ) + except (UnknownMethodException, InvalidArgumentException): + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.ACTIVATE_APP, {'appId': app_id}) + return self + + def query_app_state(self, app_id: str) -> int: + """Queries the state of the application. + + Args: + app_id: the application id to be queried + + Returns: + One of possible application state constants. See ApplicationState + class for more details. + """ + ext_name = 'mobile: queryAppState' + try: + return self.assert_extension_exists(ext_name).execute_script( + ext_name, + { + 'appId': app_id, + 'bundleId': app_id, + }, + ) + except (UnknownMethodException, InvalidArgumentException): + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute( + Command.QUERY_APP_STATE, + { + 'appId': app_id, + }, + )['value'] + + def app_strings(self, language: Union[str, None] = None, string_file: Union[str, None] = None) -> Dict[str, str]: + """Returns the application strings from the device for the specified + language. + + Args: + language: strings language code + string_file: the name of the string file to query. Only relevant for XCUITest driver + + Returns: + The key is string id and the value is the content. + """ + ext_name = 'mobile: getAppStrings' + data = {} + if language is not None: + data['language'] = language + if string_file is not None: + data['stringFile'] = string_file + try: + return self.assert_extension_exists(ext_name).execute_script(ext_name, data) + except UnknownMethodException: + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute(Command.GET_APP_STRINGS, data)['value'] + + def _add_commands(self) -> None: + self.command_executor.add_command(Command.BACKGROUND, 'POST', '/session/$sessionId/appium/app/background') + self.command_executor.add_command( + Command.IS_APP_INSTALLED, + 'POST', + '/session/$sessionId/appium/device/app_installed', + ) + self.command_executor.add_command(Command.INSTALL_APP, 'POST', '/session/$sessionId/appium/device/install_app') + self.command_executor.add_command(Command.REMOVE_APP, 'POST', '/session/$sessionId/appium/device/remove_app') + self.command_executor.add_command( + Command.TERMINATE_APP, + 'POST', + '/session/$sessionId/appium/device/terminate_app', + ) + self.command_executor.add_command( + Command.ACTIVATE_APP, + 'POST', + '/session/$sessionId/appium/device/activate_app', + ) + self.command_executor.add_command( + Command.QUERY_APP_STATE, + 'POST', + '/session/$sessionId/appium/device/app_state', + ) + self.command_executor.add_command(Command.GET_APP_STRINGS, 'POST', '/session/$sessionId/appium/app/strings') diff --git a/appium/webdriver/extensions/clipboard.py b/appium/webdriver/extensions/clipboard.py new file mode 100644 index 00000000..f5354f2e --- /dev/null +++ b/appium/webdriver/extensions/clipboard.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python + +# 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. + +import base64 +from typing import Optional + +from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence +from appium.webdriver.clipboard_content_type import ClipboardContentType + +from ..mobilecommand import MobileCommand as Command + + +class Clipboard(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def set_clipboard( + self, content: bytes, content_type: str = ClipboardContentType.PLAINTEXT, label: Optional[str] = None + ) -> Self: + """Set the content of the system clipboard + + Args: + content: The content to be set as bytearray string + content_type: One of ClipboardContentType items. Only ClipboardContentType.PLAINTEXT + is supported on Android + label: label argument, which only works for Android + + Returns: + Union['WebDriver', 'Clipboard']: Self instance + """ + ext_name = 'mobile: setClipboard' + options = { + 'content': base64.b64encode(content).decode('UTF-8'), + 'contentType': content_type, + } + if label: + options['label'] = label + try: + self.assert_extension_exists(ext_name).execute_script(ext_name, options) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.SET_CLIPBOARD, options) + return self + + def set_clipboard_text(self, text: str, label: Optional[str] = None) -> Self: + """Copies the given text to the system clipboard + + Args: + text: The text to be set + label:label argument, which only works for Android + + Returns: + Union['WebDriver', 'Clipboard']: Self instance + """ + return self.set_clipboard(bytes(str(text), 'UTF-8'), ClipboardContentType.PLAINTEXT, label) + + def get_clipboard(self, content_type: str = ClipboardContentType.PLAINTEXT) -> bytes: + """Receives the content of the system clipboard + + Args: + content_type: One of ClipboardContentType items. Only ClipboardContentType.PLAINTEXT + is supported on Android + + Returns: + Clipboard content as bytearray. Or empty bytes if the clipboard is empty + """ + ext_name = 'mobile: getClipboard' + options = {'contentType': content_type} + try: + base64_str = self.assert_extension_exists(ext_name).execute_script(ext_name, options) + except UnknownMethodException: + # TODO: Remove the fallback + base64_str = self.mark_extension_absence(ext_name).execute(Command.GET_CLIPBOARD, options)['value'] + return base64.b64decode(base64_str) + + def get_clipboard_text(self) -> str: + """Receives the text of the system clipboard + + Returns: + The actual clipboard text or an empty string if the clipboard is empty + """ + return self.get_clipboard(ClipboardContentType.PLAINTEXT).decode('UTF-8') + + def _add_commands(self) -> None: + self.command_executor.add_command( + Command.SET_CLIPBOARD, + 'POST', + '/session/$sessionId/appium/device/set_clipboard', + ) + self.command_executor.add_command( + Command.GET_CLIPBOARD, + 'POST', + '/session/$sessionId/appium/device/get_clipboard', + ) diff --git a/appium/webdriver/extensions/context.py b/appium/webdriver/extensions/context.py new file mode 100644 index 00000000..628432e1 --- /dev/null +++ b/appium/webdriver/extensions/context.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +# 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. + +from typing import List + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands + +from ..mobilecommand import MobileCommand as Command + + +class Context(CanExecuteCommands): + @property + def contexts(self) -> List[str]: + """Returns the contexts within the current session. + + Usage: + driver.contexts + + Return: + :obj:`list` of :obj:`str`: The contexts within the current session + + """ + return self.execute(Command.CONTEXTS)['value'] + + @property + def current_context(self) -> str: + """Returns the current context of the current session. + + Usage: + driver.current_context + + Return: + str: The context of the current session + """ + return self.execute(Command.GET_CURRENT_CONTEXT)['value'] + + @property + def context(self) -> str: + """Returns the current context of the current session. + + Usage: + driver.context + + Return: + str: The context of the current session + """ + return self.current_context + + def _add_commands(self) -> None: + self.command_executor.add_command(Command.CONTEXTS, 'GET', '/session/$sessionId/contexts') + self.command_executor.add_command(Command.GET_CURRENT_CONTEXT, 'GET', '/session/$sessionId/context') + self.command_executor.add_command(Command.SWITCH_TO_CONTEXT, 'POST', '/session/$sessionId/context') diff --git a/appium/webdriver/extensions/device_time.py b/appium/webdriver/extensions/device_time.py new file mode 100644 index 00000000..22ac3c25 --- /dev/null +++ b/appium/webdriver/extensions/device_time.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +# 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. + +from typing import Optional + +from selenium.common.exceptions import UnknownMethodException + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence + +from ..mobilecommand import MobileCommand as Command + + +class DeviceTime(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + @property + def device_time(self) -> str: + """Returns the date and time from the device. + + Return: + str: The date and time + """ + ext_name = 'mobile: getDeviceTime' + try: + return self.assert_extension_exists(ext_name).execute_script(ext_name) + except UnknownMethodException: + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute(Command.GET_DEVICE_TIME_GET, {})['value'] + + def get_device_time(self, format: Optional[str] = None) -> str: + """Returns the date and time from the device. + + Args: + format: The set of format specifiers. Read https://momentjs.com/docs/ + to get the full list of supported datetime format specifiers. + If unset, return :func:`.device_time` as default format is `YYYY-MM-DDTHH:mm:ssZ`, + which complies to ISO-8601 + + Usage: + | self.driver.get_device_time() + | self.driver.get_device_time("YYYY-MM-DD") + + Return: + str: The date and time + """ + ext_name = 'mobile: getDeviceTime' + if format is None: + return self.device_time + try: + return self.assert_extension_exists(ext_name).execute_script(ext_name, {'format': format}) + except UnknownMethodException: + return self.mark_extension_absence(ext_name).execute(Command.GET_DEVICE_TIME_POST, {'format': format})['value'] + + def _add_commands(self) -> None: + self.command_executor.add_command( + Command.GET_DEVICE_TIME_GET, + 'GET', + '/session/$sessionId/appium/device/system_time', + ) + self.command_executor.add_command( + Command.GET_DEVICE_TIME_POST, + 'POST', + '/session/$sessionId/appium/device/system_time', + ) diff --git a/appium/webdriver/extensions/execute_driver.py b/appium/webdriver/extensions/execute_driver.py new file mode 100644 index 00000000..698c0218 --- /dev/null +++ b/appium/webdriver/extensions/execute_driver.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +# 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. + +from typing import Any, Dict, Optional, Union + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands + +from ..mobilecommand import MobileCommand as Command + + +class ExecuteDriver(CanExecuteCommands): + # TODO Inner class case + def execute_driver(self, script: str, script_type: str = 'webdriverio', timeout_ms: Optional[int] = None) -> Any: + """Run a set of script against the current session, allowing execution of many commands in one Appium request. + Please read http://appium.io/docs/en/commands/session/execute-driver for more details about the acceptable + scripts and the output format. + + Args: + script: The string consisting of the script itself + script_type: The name of the script type. Defaults to 'webdriverio'. + timeout_ms: The number of `ms` Appium should wait for the script to finish before + killing it due to timeout_ms. + + Usage: + | self.driver.execute_driver(script='return [];') + | self.driver.execute_driver(script='return [];', script_type='webdriverio') + | self.driver.execute_driver(script='return [];', script_type='webdriverio', timeout_ms=10000) + + Returns: + ExecuteDriver.Result: The result of the script. It has 'result' and 'logs' keys. + + Raises: + WebDriverException: If something error happens in the script. The message has the original error message. + """ + + class Result: + def __init__(self, res: Dict): + self.result = res['result'] + self.logs = res['logs'] + + option: Dict[str, Union[str, int]] = {'script': script, 'type': script_type} + if timeout_ms is not None: + option['timeout'] = timeout_ms + + response = self.execute(Command.EXECUTE_DRIVER, option)['value'] + return Result(response) + + def _add_commands(self) -> None: + self.command_executor.add_command(Command.EXECUTE_DRIVER, 'POST', '/session/$sessionId/appium/execute_driver') diff --git a/appium/webdriver/extensions/execute_mobile_command.py b/appium/webdriver/extensions/execute_mobile_command.py new file mode 100644 index 00000000..8d78d418 --- /dev/null +++ b/appium/webdriver/extensions/execute_mobile_command.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +# 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. + +from typing import Any, Dict + +from typing_extensions import Self + +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts + + +class ExecuteMobileCommand(CanExecuteScripts): + def press_button(self, button_name: str) -> Self: + """Sends a physical button name to the device to simulate the user pressing. + + iOS only. + Possible button names can be found in + https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Categories/XCUIDevice%2BFBHelpers.h + + Args: + button_name: the button name to be sent to the device + + Returns: + Union['WebDriver', 'ExecuteMobileCommand']: Self instance + + """ + data = {'name': button_name} + self.execute_script('mobile: pressButton', data) + return self + + @property + def battery_info(self) -> Dict[str, Any]: + """Retrieves battery information for the device under test. + + Returns: + `dict`: containing the following entries + level: Battery level in range [0.0, 1.0], where 1.0 means 100% charge. + Any value lower than 0 means the level cannot be retrieved + state: Platform-dependent battery state value. + On iOS (XCUITest): + 1: Unplugged + 2: Charging + 3: Full + Any other value means the state cannot be retrieved + On Android (UIAutomator2): + 2: Charging + 3: Discharging + 4: Not charging + 5: Full + Any other value means the state cannot be retrieved + """ + return self.execute_script('mobile: batteryInfo') diff --git a/appium/webdriver/extensions/flutter_integration/__init__.py b/appium/webdriver/extensions/flutter_integration/__init__.py new file mode 100644 index 00000000..cc173e9d --- /dev/null +++ b/appium/webdriver/extensions/flutter_integration/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/appium/webdriver/extensions/flutter_integration/flutter_commands.py b/appium/webdriver/extensions/flutter_integration/flutter_commands.py new file mode 100644 index 00000000..5e469cae --- /dev/null +++ b/appium/webdriver/extensions/flutter_integration/flutter_commands.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python + +# 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. + +import os +from typing import Any, Dict, List, Optional, Tuple, Union + +from appium.common.helper import encode_file_to_base64 +from appium.webdriver.extensions.flutter_integration.flutter_finder import FlutterFinder +from appium.webdriver.extensions.flutter_integration.scroll_directions import ScrollDirection +from appium.webdriver.webdriver import WebDriver +from appium.webdriver.webelement import WebElement + + +class FlutterCommand: + def __init__(self, driver: WebDriver) -> None: + self.driver = driver + + # wait commands + + def wait_for_visible( + self, + locator: Union[WebElement, FlutterFinder], + timeout: Optional[float] = None, + ) -> None: + """ + Waits for a element to become visible. + + Args: + locator (Union[WebElement, FlutterFinder]): The element to wait for; can be a WebElement or a FlutterFinder. + timeout (Optional[float]): Maximum wait time in seconds. Defaults to a predefined timeout if not specified. + + Returns: + None + """ + opts: Dict[str, Any] = self.__get_locator_options(locator) + if timeout is not None: + opts['timeout'] = timeout + + self.execute_flutter_command('waitForVisible', opts) + + def wait_for_invisible( + self, + locator: Union[WebElement, FlutterFinder], + timeout: Optional[float] = None, + ) -> None: + """ + Waits for a element to become invisible. + + Args: + locator (Union[WebElement, FlutterFinder]): The element to wait for; can be a WebElement or a FlutterFinder. + timeout (Optional[float]): Maximum wait time in seconds. Defaults to a predefined timeout if not specified. + + Returns: + None: + """ + opts: Dict[str, Any] = self.__get_locator_options(locator) + if timeout is not None: + opts['timeout'] = timeout + + self.execute_flutter_command('waitForAbsent', opts) + + # flutter action commands + + def perform_double_click(self, element: WebElement, offset: Optional[Tuple[int, int]] = None) -> None: + """ + Performs a double-click on the given element, with an optional offset. + + Args: + element (WebElement): The element to double-click on. This parameter is required. + offset (Optional[Tuple[int, int]]): The x and y offsets from the element to click at. If not specified, the click is performed at the element's center. + + Returns: + None: + """ + opts: Dict[str, Union[WebElement, Dict[str, int]]] = {'origin': element} + if offset is not None: + opts['offset'] = {'x': offset[0], 'y': offset[1]} + self.execute_flutter_command('doubleClick', opts) + + def perform_long_press(self, element: WebElement, offset: Optional[Tuple[int, int]] = None) -> None: + """ + Performs a long press on the given element, with an optional offset. + + Args: + element (WebElement): The element to perform the long press on. This parameter is required. + offset (Optional[Tuple[int, int]]): The x and y offsets from the element to perform the long press at. If not specified, the long press is performed at the element's center. + + Returns: + None: + """ + opts: Dict[str, Union[WebElement, Dict[str, int]]] = {'origin': element} + if offset is not None: + opts['offset'] = {'x': offset[0], 'y': offset[1]} + self.execute_flutter_command('longPress', opts) + + def perform_drag_and_drop(self, source: WebElement, target: WebElement) -> None: + """ + Performs a drag-and-drop operation from a source element to a target element. + + Args: + source (WebElement): The element to drag from. + target (WebElement): The element to drop onto. + + Returns: + None: + """ + self.execute_flutter_command('dragAndDrop', {'source': source, 'target': target}) + + def scroll_till_visible( + self, + scroll_to: FlutterFinder, + scroll_direction: ScrollDirection = ScrollDirection.DOWN, + **opts: Any, + ) -> WebElement: + """ + Scrolls until the specified element becomes visible. + + Args: + scroll_to (FlutterFinder): The Flutter element to scroll to. + scroll_direction (ScrollDirection): The direction to scroll up or down. Defaults to `ScrollDirection.DOWN`. + + KeywordArgs: + scrollView (str): The view of the scroll. Default value is 'Scrollable' + delta (int): delta for the scroll. Default value is 64 + maxScrolls (int): Max times to scroll. Default value is 15 + settleBetweenScrollsTimeout (float): settle timeout in milliseconds. Default value is 5000 + dragDuration (float): time gap between each scroll in milliseconds. Default value is 100 + + Returns: + Webelement: scrolled element + """ + opts['finder'] = scroll_to.to_dict() + opts['scrollDirection'] = scroll_direction.value + return self.execute_flutter_command('scrollTillVisible', opts) + + def inject_mock_image(self, value: str) -> str: + """ + Injects a mock image to the device. The input can be a file path or a base64-encoded string. + + Args: + value (str): The file path of the image or a base64-encoded string. + + Returns: + str: Image ID of the injected image. + """ + if os.path.isfile(value): + base64_encoded_image = encode_file_to_base64(value) + else: + base64_encoded_image = value + return self.execute_flutter_command('injectImage', {'base64Image': base64_encoded_image}) + + def activate_injected_image(self, image_id: str) -> None: + """ + Activates an injected image with image ID. + + Args: + image_id (str): The ID of the injected image to activate. + + Returns: + None: + """ + self.execute_flutter_command('activateInjectedImage', {'imageId': image_id}) + + def get_render_tree( + self, + widget_type: Optional[str] = None, + key: Optional[str] = None, + text: Optional[str] = None, + ) -> List[Optional[Dict]]: + """ + Returns the render tree of the root widget. + + Args: + widget_type (Optional[str]): The type of the widget to primary filter by. + key (Optional[str]): The key of the widget to filter by. + text (Optional[str]): The text of the widget to filter by. + + Returns: + List[Optional[Dict]]: A list of dictionaries or None values representing the render tree. + + The result is a nested list of dictionaries representing each widget and its properties, + such as type, key, size, attribute, state, visual information, and hierarchy. + + The example widget includes the following code, which is rendered as part of the widget tree: + + .. code-block:: dart + + Semantics( + key: const Key('add_activity_semantics'), + label: 'add_activity_button', + button: true, + child: FloatingActionButton.small( + key: const Key('add_activity_button'), + tooltip: 'add_activity_button', + heroTag: 'add', + backgroundColor: const Color(0xFF2E2E3A), + onPressed: null, + child: Icon( + Icons.add, + size: 16, + color: Colors.amber.shade200.withOpacity(0.5), + semanticLabel: 'Add icon', + ), + ), + ) + + Example execute command: + + .. code-block:: python + + >>> flutter_command = FlutterCommand(driver) # noqa + >>> flutter_command.get_render_tree(widget_type='Semantics', key='add_activity_semantics') + output >> [ + { + "type": "Semantics", + "elementType": "SingleChildRenderObjectElement", + "description": "Semantics-[<'add_activity_semantics'>]", + "depth": 0, + "key": "[<'add_activity_semantics'>]", + "attributes": { + "semanticsLabel": "add_activity_button" + }, + "visual": {}, + "state": {}, + "rect": { + "x": 0, + "y": 0, + "width": 48, + "height": 48 + }, + "children": [ + { + "type": "FloatingActionButton", + "elementType": "StatelessElement", + "description": "FloatingActionButton-[<'add_activity_button'>]", + "depth": 1, + "key": "[<'add_activity_button'>]", + "attributes": {}, + "visual": {}, + "state": {}, + "rect": { + "x": 0, + "y": 0, + "width": 48, + "height": 48 + }, + "children": [ + {...}, + "children": [...] + } + ] + } + ] + } + ] + """ + opts = {} + if widget_type is not None: + opts['widgetType'] = widget_type + if key is not None: + opts['key'] = key + if text is not None: + opts['text'] = text + + return self.execute_flutter_command('renderTree', opts) + + def execute_flutter_command(self, scriptName: str, params: dict) -> Any: + """ + Executes a Flutter command by sending a script and parameters to the flutter integration driver. + + Args: + scriptName (str): The name of the Flutter command to execute. + This will be prefixed with 'flutter:' when passed to the driver. + params (dict): A dictionary of parameters to be passed along with the Flutter command. + + Returns: + Any: The result of the command execution. The return value depends on the + specific Flutter command being executed. + """ + return self.driver.execute_script(f'flutter: {scriptName}', params) + + def __get_locator_options(self, locator: Union[WebElement, 'FlutterFinder']) -> Dict[str, Union[dict, WebElement]]: + if isinstance(locator, WebElement): + return {'element': locator} + return {'locator': locator.to_dict()} diff --git a/appium/webdriver/extensions/flutter_integration/flutter_finder.py b/appium/webdriver/extensions/flutter_integration/flutter_finder.py new file mode 100644 index 00000000..0aed46a9 --- /dev/null +++ b/appium/webdriver/extensions/flutter_integration/flutter_finder.py @@ -0,0 +1,55 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you 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. + +from typing import Tuple, Union, cast + +from selenium.webdriver.common.by import ByType as SeleniumByType + +from appium.webdriver.common.appiumby import AppiumBy +from appium.webdriver.common.appiumby import ByType as AppiumByType + + +class FlutterFinder: + def __init__(self, using: Union[SeleniumByType, AppiumByType], value: str) -> None: + self.using = using + self.value = value + + @staticmethod + def by_key(value: str) -> 'FlutterFinder': + return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_KEY), value) + + @staticmethod + def by_text(value: str) -> 'FlutterFinder': + return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_TEXT), value) + + @staticmethod + def by_semantics_label(value: str) -> 'FlutterFinder': + return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_SEMANTICS_LABEL), value) + + @staticmethod + def by_type(value: str) -> 'FlutterFinder': + return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_TYPE), value) + + @staticmethod + def by_text_containing(value: str) -> 'FlutterFinder': + return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING), value) + + def to_dict(self) -> dict: + return {'using': self.using, 'value': self.value} + + def as_args(self) -> Tuple[str, str]: + return self.using, self.value diff --git a/appium/webdriver/extensions/flutter_integration/scroll_directions.py b/appium/webdriver/extensions/flutter_integration/scroll_directions.py new file mode 100644 index 00000000..7624b5b2 --- /dev/null +++ b/appium/webdriver/extensions/flutter_integration/scroll_directions.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class ScrollDirection(Enum): + UP = 'up' + DOWN = 'down' diff --git a/appium/webdriver/extensions/hw_actions.py b/appium/webdriver/extensions/hw_actions.py new file mode 100644 index 00000000..b6bbb468 --- /dev/null +++ b/appium/webdriver/extensions/hw_actions.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python + +# 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. + +from typing import Optional + +from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence + +from ..mobilecommand import MobileCommand as Command + + +class HardwareActions(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def lock(self, seconds: Optional[int] = None) -> Self: + """Lock the device. No changes are made if the device is already unlocked. + + Args: + seconds: The duration to lock the device, in seconds. + The device is going to be locked forever until `unlock` is called + if it equals or is less than zero, otherwise this call blocks until + the timeout expires and unlocks the screen automatically. + + Returns: + Union['WebDriver', 'HardwareActions']: Self instance + """ + ext_name = 'mobile: lock' + args = {'seconds': seconds or 0} + try: + self.assert_extension_exists(ext_name).execute_script(ext_name, args) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.LOCK, args) + return self + + def unlock(self) -> Self: + """Unlock the device. No changes are made if the device is already locked. + + Returns: + Union['WebDriver', 'HardwareActions']: Self instance + """ + ext_name = 'mobile: unlock' + try: + if not self.assert_extension_exists(ext_name).execute_script('mobile: isLocked'): + return self + self.execute_script(ext_name) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.UNLOCK) + return self + + def is_locked(self) -> bool: + """Checks whether the device is locked. + + Returns: + `True` if the device is locked + """ + ext_name = 'mobile: isLocked' + try: + return self.assert_extension_exists(ext_name).execute_script('mobile: isLocked') + except UnknownMethodException: + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute(Command.IS_LOCKED)['value'] + + def shake(self) -> Self: + """Shake the device. + + Returns: + Union['WebDriver', 'HardwareActions']: Self instance + """ + ext_name = 'mobile: shake' + try: + self.assert_extension_exists(ext_name).execute_script(ext_name) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.SHAKE) + return self + + def touch_id(self, match: bool) -> Self: + """Simulate touchId on iOS Simulator + + Args: + match: Simulates a successful touch (`True`) or a failed touch (`False`) + + Returns: + Union['WebDriver', 'HardwareActions']: Self instance + """ + self.execute_script( + 'mobile: sendBiometricMatch', + { + 'type': 'touchId', + 'match': match, + }, + ) + return self + + def toggle_touch_id_enrollment(self) -> Self: + """Toggle enroll touchId on iOS Simulator + + Returns: + Union['WebDriver', 'HardwareActions']: Self instance + """ + is_enrolled = self.execute_script('mobile: isBiometricEnrolled') + self.execute_script('mobile: enrollBiometric', {'isEnabled': not is_enrolled}) + return self + + def finger_print(self, finger_id: int) -> Self: + """Authenticate users by using their finger print scans on supported Android emulators. + + Args: + finger_id: Finger prints stored in Android Keystore system (from 1 to 10) + """ + ext_name = 'mobile: fingerprint' + args = {'fingerprintId': finger_id} + try: + self.assert_extension_exists(ext_name).execute_script(ext_name, args) + except UnknownMethodException: + self.mark_extension_absence(ext_name).execute(Command.FINGER_PRINT, args) + return self + + def _add_commands(self) -> None: + self.command_executor.add_command(Command.LOCK, 'POST', '/session/$sessionId/appium/device/lock') + self.command_executor.add_command(Command.UNLOCK, 'POST', '/session/$sessionId/appium/device/unlock') + self.command_executor.add_command(Command.IS_LOCKED, 'POST', '/session/$sessionId/appium/device/is_locked') + self.command_executor.add_command(Command.SHAKE, 'POST', '/session/$sessionId/appium/device/shake') + self.command_executor.add_command(Command.TOUCH_ID, 'POST', '/session/$sessionId/appium/simulator/touch_id') + self.command_executor.add_command( + Command.TOGGLE_TOUCH_ID_ENROLLMENT, + 'POST', + '/session/$sessionId/appium/simulator/toggle_touch_id_enrollment', + ) + self.command_executor.add_command( + Command.FINGER_PRINT, + 'POST', + '/session/$sessionId/appium/device/finger_print', + ) diff --git a/appium/webdriver/extensions/images_comparison.py b/appium/webdriver/extensions/images_comparison.py new file mode 100644 index 00000000..04b8f114 --- /dev/null +++ b/appium/webdriver/extensions/images_comparison.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python + +# 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. + +from typing import Any, Dict, Union + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands + +from ..mobilecommand import MobileCommand as Command + +Base64Payload = Union[str, bytes] + + +class ImagesComparison(CanExecuteCommands): + def match_images_features(self, base64_image1: Base64Payload, base64_image2: Base64Payload, **opts: Any) -> Dict[str, Any]: + """Performs images matching by features. + + Read + https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_matcher/py_matcher.html + for more details on this topic. + The method supports all image formats, which are supported by OpenCV itself. + + Args: + base64_image1: base64-encoded content of the first image + base64_image2: base64-encoded content of the second image + + Keyword Args: + visualize (bool): Set it to True in order to return the visualization of the matching operation. + matching visualization. False by default + detectorName (str): One of possible feature detector names: + 'AKAZE', 'AGAST', 'BRISK', 'FAST', 'GFTT', 'KAZE', 'MSER', 'SIFT', 'ORB' + Some of these detectors are not enabled in the default OpenCV deployment. + 'ORB' By default. + matchFunc (str): One of supported matching functions names: + 'FlannBased', 'BruteForce', 'BruteForceL1', 'BruteForceHamming', + 'BruteForceHammingLut', 'BruteForceSL2' + 'BruteForce' by default + goodMatchesFactor (int): The maximum count of "good" matches (e. g. with minimal distances). + This count is unlimited by default. + + Returns: + The dictionary containing the following entries: + + visualization (bytes): base64-encoded content of PNG visualization of the current comparison + operation. This entry is only present if `visualize` option is enabled + count (int): The count of matched edges on both images. + The more matching edges there are no both images the more similar they are. + totalCount (int): The total count of matched edges on both images. + It is equal to `count` if `goodMatchesFactor` does not limit the matches, + otherwise it contains the total count of matches before `goodMatchesFactor` is + applied. + points1 (dict): The array of matching points on the first image. Each point is a dictionary + with 'x' and 'y' keys + rect1 (dict): The bounding rect for the `points1` array or a zero rect if not enough matching points + were found. The rect is represented by a dictionary with 'x', 'y', 'width' and 'height' keys + points2 (dict): The array of matching points on the second image. Each point is a dictionary + with 'x' and 'y' keys + rect2 (dict): The bounding rect for the `points2` array or a zero rect if not enough matching points + were found. The rect is represented by a dictionary with 'x', 'y', 'width' and 'height' keys + """ + options = { + 'mode': 'matchFeatures', + 'firstImage': _adjust_image_payload(base64_image1), + 'secondImage': _adjust_image_payload(base64_image2), + 'options': opts, + } + return self.execute(Command.COMPARE_IMAGES, options)['value'] + + def find_image_occurrence( + self, base64_full_image: Base64Payload, base64_partial_image: Base64Payload, **opts: Any + ) -> Dict[str, Union[bytes, Dict]]: + """Performs images matching by template to find possible occurrence of the partial image + in the full image. + + Read + https://docs.opencv.org/2.4/doc/tutorials/imgproc/histograms/template_matching/template_matching.html + for more details on this topic. + The method supports all image formats, which are supported by OpenCV itself. + + Args: + base64_full_image: base64-encoded content of the full image + base64_partial_image: base64-encoded content of the partial image + + Keyword Args: + visualize (bool): Set it to True in order to return the visualization of the matching operation. + False by default + + Returns: + The dictionary containing the following entries: + visualization (bytes): base64-encoded content of PNG visualization of the current comparison + operation. This entry is only present if `visualize` option is enabled + rect (dict): The region of the partial image occurrence on the full image. + The rect is represented by a dictionary with 'x', 'y', 'width' and 'height' keys + """ + options = { + 'mode': 'matchTemplate', + 'firstImage': _adjust_image_payload(base64_full_image), + 'secondImage': _adjust_image_payload(base64_partial_image), + 'options': opts, + } + return self.execute(Command.COMPARE_IMAGES, options)['value'] + + def get_images_similarity( + self, base64_image1: Base64Payload, base64_image2: Base64Payload, **opts: Any + ) -> Dict[str, Union[bytes, Dict]]: + """Performs images matching to calculate the similarity score between them. + + The flow there is similar to the one used in + `find_image_occurrence`, but it is mandatory that both images are of equal resolution. + The method supports all image formats, which are supported by OpenCV itself. + + Args: + base64_image1: base64-encoded content of the first image + base64_image2: base64-encoded content of the second image + + Keyword Args: + visualize (boo): Set it to True in order to return the visualization of the matching operation. + False by default + + Returns: + The dictionary containing the following entries: + visualization (bytes): base64-encoded content of PNG visualization of the current comparison + operation. This entry is only present if `visualize` option is enabled + score (float): The similarity score as a float number in range [0.0, 1.0]. + 1.0 is the highest score (means both images are totally equal). + """ + options = { + 'mode': 'getSimilarity', + 'firstImage': _adjust_image_payload(base64_image1), + 'secondImage': _adjust_image_payload(base64_image2), + 'options': opts, + } + return self.execute(Command.COMPARE_IMAGES, options)['value'] + + def _add_commands(self) -> None: + self.command_executor.add_command(Command.COMPARE_IMAGES, 'POST', '/session/$sessionId/appium/compare_images') + + +def _adjust_image_payload(payload: Base64Payload) -> str: + try: + return payload if isinstance(payload, str) else payload.decode('utf-8') + except UnicodeDecodeError as e: + raise ValueError('The image payload cannot be serialized to a string. Make sure to base64-encode it first') from e diff --git a/appium/webdriver/extensions/keyboard.py b/appium/webdriver/extensions/keyboard.py new file mode 100644 index 00000000..4640b116 --- /dev/null +++ b/appium/webdriver/extensions/keyboard.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python + +# 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. + +from typing import Dict, Optional + +from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence + +from ..mobilecommand import MobileCommand as Command + + +class Keyboard(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def hide_keyboard(self, key_name: Optional[str] = None, key: Optional[str] = None, strategy: Optional[str] = None) -> Self: + """Hides the software keyboard on the device. + + In iOS, use `key_name` to press + a particular key, or `strategy`. In Android, no parameters are used. + + Args: + key_name: key to press + key: + strategy: strategy for closing the keyboard (e.g., `tapOutside`) + + Returns: + Union['WebDriver', 'Keyboard']: Self instance + """ + ext_name = 'mobile: hideKeyboard' + try: + self.assert_extension_exists(ext_name).execute_script( + ext_name, {**({'keys': [key or key_name]} if key or key_name else {})} + ) + except UnknownMethodException: + # TODO: Remove the fallback + data: Dict[str, Optional[str]] = {} + if key_name is not None: + data['keyName'] = key_name + elif key is not None: + data['key'] = key + elif strategy is None: + strategy = 'tapOutside' + data['strategy'] = strategy + self.mark_extension_absence(ext_name).execute(Command.HIDE_KEYBOARD, data) + return self + + def is_keyboard_shown(self) -> bool: + """Attempts to detect whether a software keyboard is present + + Returns: + `True` if keyboard is shown + """ + ext_name = 'mobile: isKeyboardShown' + try: + return self.assert_extension_exists(ext_name).execute_script(ext_name) + except UnknownMethodException: + return self.mark_extension_absence(ext_name).execute(Command.IS_KEYBOARD_SHOWN)['value'] + + def keyevent(self, keycode: int, metastate: Optional[int] = None) -> Self: + """Sends a keycode to the device. + + Android only. + Possible keycodes can be found in http://developer.android.com/reference/android/view/KeyEvent.html. + + Args: + keycode: the keycode to be sent to the device + metastate: meta information about the keycode being sent + + Returns: + Union['WebDriver', 'Keyboard']: Self instance + """ + return self.press_keycode(keycode=keycode, metastate=metastate) + + def press_keycode(self, keycode: int, metastate: Optional[int] = None, flags: Optional[int] = None) -> Self: + """Sends a keycode to the device. + + Android only. Possible keycodes can be found + in http://developer.android.com/reference/android/view/KeyEvent.html. + + Args: + keycode: the keycode to be sent to the device + metastate: meta information about the keycode being sent + flags: the set of key event flags + + Returns: + Union['WebDriver', 'Keyboard']: Self instance + """ + ext_name = 'mobile: pressKey' + args = {'keycode': keycode} + if metastate is not None: + args['metastate'] = metastate + if flags is not None: + args['flags'] = flags + try: + self.assert_extension_exists(ext_name).execute_script(ext_name, args) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.PRESS_KEYCODE, args) + return self + + def long_press_keycode(self, keycode: int, metastate: Optional[int] = None, flags: Optional[int] = None) -> Self: + """Sends a long press of keycode to the device. + + Android only. Possible keycodes can be found in + http://developer.android.com/reference/android/view/KeyEvent.html. + + Args: + keycode: the keycode to be sent to the device + metastate: meta information about the keycode being sent + flags: the set of key event flags + + Returns: + Union['WebDriver', 'Keyboard']: Self instance + """ + ext_name = 'mobile: pressKey' + args = {'keycode': keycode} + if metastate is not None: + args['metastate'] = metastate + if flags is not None: + args['flags'] = flags + try: + self.assert_extension_exists(ext_name).execute_script( + ext_name, + { + **args, + 'isLongPress': True, + }, + ) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute(Command.LONG_PRESS_KEYCODE, args) + return self + + def _add_commands(self) -> None: + self.command_executor.add_command( + Command.HIDE_KEYBOARD, + 'POST', + '/session/$sessionId/appium/device/hide_keyboard', + ) + self.command_executor.add_command( + Command.IS_KEYBOARD_SHOWN, + 'GET', + '/session/$sessionId/appium/device/is_keyboard_shown', + ) + self.command_executor.add_command(Command.KEY_EVENT, 'POST', '/session/$sessionId/appium/device/keyevent') + self.command_executor.add_command( + Command.PRESS_KEYCODE, + 'POST', + '/session/$sessionId/appium/device/press_keycode', + ) + self.command_executor.add_command( + Command.LONG_PRESS_KEYCODE, + 'POST', + '/session/$sessionId/appium/device/long_press_keycode', + ) diff --git a/appium/webdriver/extensions/location.py b/appium/webdriver/extensions/location.py new file mode 100644 index 00000000..3141050a --- /dev/null +++ b/appium/webdriver/extensions/location.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python + +# 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. + +from typing import Dict, Union + +from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts + +from ..mobilecommand import MobileCommand as Command + + +class Location(CanExecuteCommands, CanExecuteScripts): + def toggle_location_services(self) -> Self: + """Toggle the location services on the device. + This API only reliably since Android 12 (API level 31) + + Android only. + + Returns: + Union['WebDriver', 'Location']: Self instance + """ + try: + self.execute_script('mobile: toggleGps') + except UnknownMethodException: + # TODO: Remove the fallback + self.execute(Command.TOGGLE_LOCATION_SERVICES) + return self + + def set_location( + self, + latitude: Union[float, str], + longitude: Union[float, str], + altitude: Union[float, str, None] = None, + speed: Union[float, str, None] = None, + satellites: Union[float, str, None] = None, + ) -> Self: + """Set the location of the device + + Args: + latitude: String or numeric value between -90.0 and 90.00 + longitude: String or numeric value between -180.0 and 180.0 + altitude: String or numeric value (Android real device only) + speed: String or numeric value larger than 0.0 (Android real devices only) + satellites: String or numeric value of active GPS satellites in range 1..12. (Android emulators only) + + Returns: + Union['WebDriver', 'Location']: Self instance + """ + data = { + 'location': { + 'latitude': latitude, + 'longitude': longitude, + } + } + if altitude is not None: + data['location']['altitude'] = altitude + if speed is not None: + data['location']['speed'] = speed + if satellites is not None: + data['location']['satellites'] = satellites + self.execute(Command.SET_LOCATION, data) + return self + + @property + def location(self) -> Dict[str, float]: + """Retrieves the current location + + Returns: + A dictionary whose keys are + - latitude (float) + - longitude (float) + - altitude (float) + """ + return self.execute(Command.GET_LOCATION)['value'] + + def _add_commands(self) -> None: + """Add location endpoints. They are not int w3c spec.""" + self.command_executor.add_command( + Command.TOGGLE_LOCATION_SERVICES, + 'POST', + '/session/$sessionId/appium/device/toggle_location_services', + ) + self.command_executor.add_command(Command.GET_LOCATION, 'GET', '/session/$sessionId/location') + self.command_executor.add_command(Command.SET_LOCATION, 'POST', '/session/$sessionId/location') diff --git a/appium/webdriver/extensions/log_event.py b/appium/webdriver/extensions/log_event.py new file mode 100644 index 00000000..4ca53b3f --- /dev/null +++ b/appium/webdriver/extensions/log_event.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +# 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. + +from typing import Dict, List, Union + +from typing_extensions import Self + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands + +from ..mobilecommand import MobileCommand as Command + + +class LogEvent(CanExecuteCommands): + def get_events(self, type: Union[List[str], None] = None) -> Dict[str, Union[str, int]]: + """Retrieves events information from the current session + (Since Appium 1.16.0) + + Args: + type: The event type to filter with + + Usage: + | events = driver.get_events() + | events = driver.get_events(['appium:funEvent']) + + Returns: + `dict`: A dictionary of events timing information containing the following entries + | commands: (`list` of `dict`) List of dictionaries containing the following entries + | cmd: The command name that has been sent to the appium server + | startTime: Received time + | endTime: Response time + """ + data = {} + if type is not None: + data['type'] = type + return self.execute(Command.GET_EVENTS, data)['value'] + + def log_event(self, vendor: str, event: str) -> Self: + """Log a custom event on the Appium server. + (Since Appium 1.16.0) + + Args: + vendor: The vendor to log + event: The event to log + + Usage: + driver.log_event('appium', 'funEvent') + + Returns: + Union['WebDriver', 'LogEvent']: Self instance + """ + data = {'vendor': vendor, 'event': event} + self.execute(Command.LOG_EVENT, data) + return self + + def _add_commands(self) -> None: + self.command_executor.add_command(Command.GET_EVENTS, 'POST', '/session/$sessionId/appium/events') + self.command_executor.add_command(Command.LOG_EVENT, 'POST', '/session/$sessionId/appium/log_event') diff --git a/appium/webdriver/extensions/logs.py b/appium/webdriver/extensions/logs.py new file mode 100644 index 00000000..53e16594 --- /dev/null +++ b/appium/webdriver/extensions/logs.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +# 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. + +from typing import Any, Dict, List + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands + +from ..mobilecommand import MobileCommand as Command + + +class Logs(CanExecuteCommands): + @property + def log_types(self) -> List[str]: + """Gets a list of the available log types. This only works with w3c + compliant browsers. + + Example: + .. code-block:: python + + driver.log_types + """ + return self.execute(Command.GET_AVAILABLE_LOG_TYPES)['value'] + + def get_log(self, log_type: str) -> List[Dict[str, Any]]: + """Gets the log for a given log type. + + Args: + log_type: Type of log that which will be returned + + Example: + .. code-block:: python + + driver.get_log('browser') + driver.get_log('driver') + driver.get_log('client') + driver.get_log('server') + """ + return self.execute(Command.GET_LOG, {'type': log_type})['value'] + + def _add_commands(self) -> None: + self.command_executor.add_command(Command.GET_LOG, 'POST', '/session/$sessionId/se/log') + self.command_executor.add_command(Command.GET_AVAILABLE_LOG_TYPES, 'GET', '/session/$sessionId/se/log/types') diff --git a/appium/webdriver/extensions/remote_fs.py b/appium/webdriver/extensions/remote_fs.py new file mode 100644 index 00000000..5ccebd37 --- /dev/null +++ b/appium/webdriver/extensions/remote_fs.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python + +# 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. + +import base64 +from typing import Optional + +from selenium.common.exceptions import InvalidArgumentException, UnknownMethodException +from typing_extensions import Self + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence + +from ..mobilecommand import MobileCommand as Command + + +class RemoteFS(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def pull_file(self, path: str) -> str: + """Retrieves the file at `path`. + + Args: + path: the path to the file on the device + + Returns: + The file's contents encoded as Base64. + """ + ext_name = 'mobile: pullFile' + try: + return self.assert_extension_exists(ext_name).execute_script(ext_name, {'remotePath': path}) + except UnknownMethodException: + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute(Command.PULL_FILE, {'path': path})['value'] + + def pull_folder(self, path: str) -> str: + """Retrieves a folder at `path`. + + Args: + path: the path to the folder on the device + + Returns: + The folder's contents zipped and encoded as Base64. + """ + ext_name = 'mobile: pullFolder' + try: + return self.assert_extension_exists(ext_name).execute_script(ext_name, {'remotePath': path}) + except UnknownMethodException: + # TODO: Remove the fallback + return self.mark_extension_absence(ext_name).execute(Command.PULL_FOLDER, {'path': path})['value'] + + def push_file(self, destination_path: str, base64data: Optional[str] = None, source_path: Optional[str] = None) -> Self: + """Puts the data from the file at `source_path`, encoded as Base64, in the file specified as `path`. + + Specify either `base64data` or `source_path`, if both specified default to `source_path` + + Args: + destination_path: the location on the device/simulator where the local file contents should be saved + base64data: file contents, encoded as Base64, to be written + to the file on the device/simulator + source_path: local file path for the file to be loaded on device + + Returns: + Union['WebDriver', 'RemoteFS']: Self instance + """ + if source_path is None and base64data is None: + raise InvalidArgumentException('Must either pass base64 data or a local file path') + + if source_path is not None: + try: + with open(source_path, 'rb') as f: + file_data = f.read() + except IOError as e: + message = f'source_path "{source_path}" could not be found. Are you sure the file exists?' + raise InvalidArgumentException(message) from e + base64data = base64.b64encode(file_data).decode('utf-8') + + ext_name = 'mobile: pushFile' + try: + self.assert_extension_exists(ext_name).execute_script( + ext_name, + { + 'remotePath': destination_path, + 'payload': base64data, + }, + ) + except UnknownMethodException: + # TODO: Remove the fallback + self.mark_extension_absence(ext_name).execute( + Command.PUSH_FILE, + { + 'path': destination_path, + 'data': base64data, + }, + ) + return self + + def _add_commands(self) -> None: + self.command_executor.add_command(Command.PULL_FILE, 'POST', '/session/$sessionId/appium/device/pull_file') + self.command_executor.add_command(Command.PULL_FOLDER, 'POST', '/session/$sessionId/appium/device/pull_folder') + self.command_executor.add_command(Command.PUSH_FILE, 'POST', '/session/$sessionId/appium/device/push_file') diff --git a/appium/webdriver/extensions/screen_record.py b/appium/webdriver/extensions/screen_record.py new file mode 100644 index 00000000..07c9b2ba --- /dev/null +++ b/appium/webdriver/extensions/screen_record.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python + +# 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. + +from typing import Any, Union + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands + +from ..mobilecommand import MobileCommand as Command + + +class ScreenRecord(CanExecuteCommands): + def start_recording_screen(self, **options: Any) -> Union[bytes, str]: + """Start asynchronous screen recording process. + + +--------------+-----+---------+-----+-------+ + | Keyword Args | iOS | Android | Win | macOS | + +==============+=====+=========+=====+=======+ + | remotePath | O | O | O | O | + +--------------+-----+---------+-----+-------+ + | user | O | O | O | O | + +--------------+-----+---------+-----+-------+ + | password | O | O | O | O | + +--------------+-----+---------+-----+-------+ + | method | O | O | O | O | + +--------------+-----+---------+-----+-------+ + | timeLimit | O | O | O | O | + +--------------+-----+---------+-----+-------+ + | forceRestart | O | O | O | O | + +--------------+-----+---------+-----+-------+ + | fileFieldName| O | O | O | O | + +--------------+-----+---------+-----+-------+ + | formFields | O | O | O | O | + +--------------+-----+---------+-----+-------+ + | headers | O | O | O | O | + +--------------+-----+---------+-----+-------+ + | videoQuality | O | | | | + +--------------+-----+---------+-----+-------+ + | videoType | O | | | | + +--------------+-----+---------+-----+-------+ + | videoFps | O | | | | + +--------------+-----+---------+-----+-------+ + | videoFilter | O | | O | O | + +--------------+-----+---------+-----+-------+ + | videoScale | O | | | | + +--------------+-----+---------+-----+-------+ + | pixelFormat | O | | | | + +--------------+-----+---------+-----+-------+ + | videoSize | | O | | | + +--------------+-----+---------+-----+-------+ + | bitRate | | O | | | + +--------------+-----+---------+-----+-------+ + | bugReport | | O | | | + +--------------+-----+---------+-----+-------+ + | fps | | | O | O | + +--------------+-----+---------+-----+-------+ + | captureCursor| | | O | O | + +--------------+-----+---------+-----+-------+ + | captureClicks| | | O | O | + +--------------+-----+---------+-----+-------+ + | deviceId | | | | O | + +--------------+-----+---------+-----+-------+ + | preset | | | O | O | + +--------------+-----+---------+-----+-------+ + | audioInput | | | O | | + +--------------+-----+---------+-----+-------+ + + Keyword Args: + remotePath (str): The remotePath upload option is the path to the remote location, + where the resulting video from the previous screen recording should be uploaded. + The following protocols are supported: http/https (multipart), ftp. + Missing value (the default setting) means the content of the resulting + file should be encoded as Base64 and passed as the endpoint response value, but + an exception will be thrown if the generated media file is too big to + fit into the available process memory. + This option only has an effect if there is/was an active screen recording session + and forced restart is not enabled (the default setting). + user (str): The name of the user for the remote authentication. + Only has an effect if both `remotePath` and `password` are set. + password (str): The password for the remote authentication. + Only has an effect if both `remotePath` and `user` are set. + method (str): The HTTP method name ('PUT'/'POST'). PUT method is used by default. + Only has an effect if `remotePath` is set. + timeLimit (int): The actual time limit of the recorded video in seconds. + The default value for both iOS and Android is 180 seconds (3 minutes). + The default value for macOS is 600 seconds (10 minutes). + The maximum value for Android is 3 minutes. + The maximum value for iOS is 10 minutes. + The maximum value for macOS is 10000 seconds (166 minutes). + forcedRestart (bool): Whether to ignore the result of previous capture and start a new recording + immediately (`True` value). By default (`False`) the endpoint will try to catch and + return the result of the previous capture if it's still available. + fileFieldName (str): [multipart/form-data requests] The name of the form field + containing the binary payload. "file" by default. (Since Appium 1.18.0) + formFields (dict): [multipart/form-data requests] Additional form fields mapping. If any entry has + the same key as `fileFieldName` then it is going to be ignored. (Since Appium 1.18.0) + headers (dict): [multipart/form-data requests] Headers mapping (Since Appium 1.18.0) + + videoQuality (str): [iOS] The video encoding quality: 'low', 'medium', 'high', 'photo'. Defaults + to 'medium'. + videoType (str): [iOS] The format of the screen capture to be recorded. + Available formats: Execute `ffmpeg -codecs` in the terminal to see the list of supported video codecs. + 'mjpeg' by default. (Since Appium 1.10.0) + videoFps (int): [iOS] The Frames Per Second rate of the recorded video. Change this value if the + resulting video is too slow or too fast. Defaults to 10. This can decrease the resulting file size. + videoFilters (str): [iOS, Win, macOS] The FFMPEG video filters to apply. These filters allow to scale, + flip, rotate and do many other useful transformations on the source video stream. The format of the + property must comply with https://ffmpeg.org/ffmpeg-filters.html. (Since Appium 1.15) + videoScale (str): [iOS] The scaling value to apply. Read https://trac.ffmpeg.org/wiki/Scaling for + possible values. No scale is applied by default. If videoFilters are set then the scale setting is + effectively ignored. (Since Appium 1.10.0) + pixelFormat (str): [iOS] Output pixel format. Run `ffmpeg -pix_fmts` to list possible values. + For Quicktime compatibility, set to "yuv420p" along with videoType: "libx264". (Since Appium 1.12.0) + + videoSize (str): [Android] The video size of the generated media file. The format is WIDTHxHEIGHT. + The default value is the device's native display resolution (if supported), + 1280x720 if not. For best results, use a size supported by your device's + Advanced Video Coding (AVC) encoder. + bitRate (int): [Android] The video bit rate for the video, in bits per second. + The default value is 4000000 (4 Mbit/s). You can increase the bit rate to improve video quality, + but doing so results in larger movie files. + bugReport (str): [Android] Makes the recorder to display an additional information on the video overlay, + such as a timestamp, that is helpful in videos captured to illustrate bugs. + This option is only supported since API level 27 (Android P). + + fps (int): [Win, macOS] The count of frames per second in the resulting video. + Increasing fps value also increases the size of the resulting video file and the CPU usage. + captureCursor (bool): [Win, macOS] Whether to capture the mouse cursor while recording the screen. + Disabled by default. + captureClick (bool): [Win, macOS] Whether to capture the click gestures while recording the screen. + Disabled by default. + deviceId (int): [macOS] Screen device index to use for the recording. + The list of available devices could be retrieved using + `ffmpeg -f avfoundation -list_devices true -i` command. + This option is mandatory and must be always provided. + preset (str): [Win, macOS] A preset is a collection of options that will provide a certain encoding + speed to compression ratio. A slower preset will provide better compression + (compression is quality per filesize). This means that, for example, if you target a certain file size + or constant bit rate, you will achieve better quality with a slower preset. + Read https://trac.ffmpeg.org/wiki/Encode/H.264 for more details. + Possible values are 'ultrafast', 'superfast', 'veryfast'(default), 'faster', 'fast', 'medium', 'slow', + 'slower', 'veryslow' + + Returns: + bytes: Base-64 encoded content of the recorded media + if `stop_recording_screen` isn't called after previous `start_recording_screen`. + Otherwise returns an empty string. + """ + if 'password' in options: + options['pass'] = options['password'] + del options['password'] + return self.execute(Command.START_RECORDING_SCREEN, {'options': options})['value'] + + def stop_recording_screen(self, **options: Any) -> bytes: + """Gather the output from the previously started screen recording to a media file. + + Keyword Args: + remotePath (str): The remotePath upload option is the path to the remote location, + where the resulting video should be uploaded. + The following protocols are supported: http/https (multipart), ftp. + Missing value (the default setting) means the content of the resulting + file should be encoded as Base64 and passed as the endpoint response value, but + an exception will be thrown if the generated media file is too big to + fit into the available process memory. + user (str): The name of the user for the remote authentication. + Only has an effect if both `remotePath` and `password` are set. + password (str): The password for the remote authentication. + Only has an effect if both `remotePath` and `user` are set. + method (str): The HTTP method name ('PUT'/'POST'). PUT method is used by default. + Only has an effect if `remotePath` is set. + fileFieldName (str): [multipart/form-data requests] The name of the form field + containing the binary payload. "file" by default. (Since Appium 1.18.0) + formFields (dict): [multipart/form-data requests] Additional form fields mapping. If any entry has + the same key as `fileFieldName` then it is going to be ignored. (Since Appium 1.18.0) + headers (dict): [multipart/form-data requests] Headers mapping (Since Appium 1.18.0) + + Returns: + bytes: Base-64 encoded content of the recorded media file or an empty string + if the file has been successfully uploaded to a remote location + (depends on the actual `remotePath` value). + """ + if 'password' in options: + options['pass'] = options['password'] + del options['password'] + return self.execute(Command.STOP_RECORDING_SCREEN, {'options': options})['value'] + + def _add_commands(self) -> None: + self.command_executor.add_command( + Command.START_RECORDING_SCREEN, + 'POST', + '/session/$sessionId/appium/start_recording_screen', + ) + self.command_executor.add_command( + Command.STOP_RECORDING_SCREEN, + 'POST', + '/session/$sessionId/appium/stop_recording_screen', + ) diff --git a/appium/webdriver/extensions/session.py b/appium/webdriver/extensions/session.py new file mode 100644 index 00000000..e0bcd8b6 --- /dev/null +++ b/appium/webdriver/extensions/session.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +# 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. + +from typing import Dict + +from appium.common.logger import logger +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands + +from ..mobilecommand import MobileCommand as Command + + +class Session(CanExecuteCommands): + @property + def events(self) -> Dict: + """Retrieves events information from the current session + + Usage: + events = driver.events + + Returns: + `dict`: containing events timing information from the current session + """ + try: + return self.execute(Command.GET_SESSION)['value']['events'] + except Exception as e: + logger.warning('Could not find events information in the session. Error: %s', e) + return {} + + def _add_commands(self) -> None: + self.command_executor.add_command(Command.GET_SESSION, 'GET', '/session/$sessionId') diff --git a/appium/webdriver/extensions/settings.py b/appium/webdriver/extensions/settings.py new file mode 100644 index 00000000..d9a55111 --- /dev/null +++ b/appium/webdriver/extensions/settings.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +# 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. + +from typing import Any, Dict + +from typing_extensions import Self + +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands + +from ..mobilecommand import MobileCommand as Command + + +class Settings(CanExecuteCommands): + def get_settings(self) -> Dict[str, Any]: + """Returns the appium server Settings for the current session. + + Do not get Settings confused with Desired Capabilities, they are + separate concepts. See https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/settings.md + + Returns: + Current settings + """ + return self.execute(Command.GET_SETTINGS, {})['value'] + + def update_settings(self, settings: Dict[str, Any]) -> Self: + """Set settings for the current session. + + For more on settings, see: https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/settings.md + + Args: + settings: dictionary of settings to apply to the current test session + """ + self.execute(Command.UPDATE_SETTINGS, {'settings': settings}) + return self + + def _add_commands(self) -> None: + self.command_executor.add_command(Command.GET_SETTINGS, 'GET', '/session/$sessionId/appium/settings') + self.command_executor.add_command(Command.UPDATE_SETTINGS, 'POST', '/session/$sessionId/appium/settings') diff --git a/appium/webdriver/locator_converter.py b/appium/webdriver/locator_converter.py new file mode 100644 index 00000000..4c6416c6 --- /dev/null +++ b/appium/webdriver/locator_converter.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +# 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. + +from typing import Tuple + +from selenium.webdriver.remote.locator_converter import LocatorConverter + + +class AppiumLocatorConverter(LocatorConverter): + """A custom locator converter in Appium. + + Appium supports locators which are not defined in W3C WebDriver, + so Appium Python client wants to keep the given locators + to the Appium server as-is. + """ + + def convert(self, by: str, value: str) -> Tuple[str, str]: + return (by, value) diff --git a/appium/webdriver/mobilecommand.py b/appium/webdriver/mobilecommand.py index edd79526..79ac25e1 100644 --- a/appium/webdriver/mobilecommand.py +++ b/appium/webdriver/mobilecommand.py @@ -13,41 +13,92 @@ # limitations under the License. -class MobileCommand(object): - CONTEXTS = 'getContexts', - GET_CURRENT_CONTEXT = 'getCurrentContext', +class MobileCommand: + # Common + GET_SESSION = 'getSession' + + GET_STATUS = 'getStatus' + + ## MJSONWP for Selenium v4 + GET_LOCATION = 'getLocation' + SET_LOCATION = 'setLocation' + + CLEAR = 'clear' + LOCATION_IN_VIEW = 'locationInView' + + CONTEXTS = 'getContexts' + GET_CURRENT_CONTEXT = 'getCurrentContext' SWITCH_TO_CONTEXT = 'switchToContext' - TOUCH_ACTION = 'touchAction' - MULTI_ACTION = 'multiAction' - OPEN_NOTIFICATIONS = 'openNotifications' - GET_NETWORK_CONNECTION = 'getNetworkConnection' - SET_NETWORK_CONNECTION = 'setNetworkConnection' - GET_AVAILABLE_IME_ENGINES = 'getAvailableIMEEngines' - IS_IME_ACTIVE = 'isIMEActive' - ACTIVATE_IME_ENGINE = 'activateIMEEngine' - DEACTIVATE_IME_ENGINE = 'deactivateIMEEngine' - GET_ACTIVE_IME_ENGINE = 'getActiveEngine' - # Appium Commands - GET_APP_STRINGS = 'getAppStrings' - PRESS_KEYCODE = 'pressKeyCode' - KEY_EVENT = 'keyEvent' # Needed for Selendroid - LONG_PRESS_KEYCODE = 'longPressKeyCode' - GET_CURRENT_ACTIVITY = 'getCurrentActivity' - SET_IMMEDIATE_VALUE = 'setImmediateValue' - PULL_FILE = 'pullFile' - PULL_FOLDER = 'pullFolder' - PUSH_FILE = 'pushFile' - COMPLEX_FIND = 'complexFind' BACKGROUND = 'background' - IS_APP_INSTALLED = 'isAppInstalled' + GET_APP_STRINGS = 'getAppStrings' + + IS_LOCKED = 'isLocked' + LOCK = 'lock' + UNLOCK = 'unlock' + GET_DEVICE_TIME_GET = 'getDeviceTimeGet' + GET_DEVICE_TIME_POST = 'getDeviceTimePost' INSTALL_APP = 'installApp' REMOVE_APP = 'removeApp' - LAUNCH_APP = 'launchApp' - CLOSE_APP = 'closeApp' - END_TEST_COVERAGE = 'endTestCoverage' - LOCK = 'lock' + IS_APP_INSTALLED = 'isAppInstalled' + TERMINATE_APP = 'terminateApp' + ACTIVATE_APP = 'activateApp' + QUERY_APP_STATE = 'queryAppState' SHAKE = 'shake' - RESET = 'reset' HIDE_KEYBOARD = 'hideKeyboard' - REPLACE_KEYS = 'replaceKeys' + PRESS_KEYCODE = 'pressKeyCode' + LONG_PRESS_KEYCODE = 'longPressKeyCode' + KEY_EVENT = 'keyEvent' # Needed for Selendroid + PUSH_FILE = 'pushFile' + PULL_FILE = 'pullFile' + PULL_FOLDER = 'pullFolder' + GET_CLIPBOARD = 'getClipboard' + SET_CLIPBOARD = 'setClipboard' + FINGER_PRINT = 'fingerPrint' + GET_SETTINGS = 'getSettings' + UPDATE_SETTINGS = 'updateSettings' + START_RECORDING_SCREEN = 'startRecordingScreen' + STOP_RECORDING_SCREEN = 'stopRecordingScreen' + COMPARE_IMAGES = 'compareImages' + IS_KEYBOARD_SHOWN = 'isKeyboardShown' + + EXECUTE_DRIVER = 'executeDriver' + + GET_EVENTS = 'getLogEvents' + LOG_EVENT = 'logCustomEvent' + + ## MJSONWP for Selenium v4 + IS_ELEMENT_DISPLAYED = 'isElementDisplayed' + GET_CAPABILITIES = 'getCapabilities' + GET_SCREEN_ORIENTATION = 'getScreenOrientation' + SET_SCREEN_ORIENTATION = 'setScreenOrientation' + + # To override selenium commands + GET_LOG = 'getLog' + GET_AVAILABLE_LOG_TYPES = 'getAvailableLogTypes' + + # Android + OPEN_NOTIFICATIONS = 'openNotifications' + GET_CURRENT_ACTIVITY = 'getCurrentActivity' + GET_CURRENT_PACKAGE = 'getCurrentPackage' + GET_SYSTEM_BARS = 'getSystemBars' + GET_DISPLAY_DENSITY = 'getDisplayDensity' + TOGGLE_WIFI = 'toggleWiFi' + TOGGLE_LOCATION_SERVICES = 'toggleLocationServices' + GET_PERFORMANCE_DATA_TYPES = 'getPerformanceDataTypes' + GET_PERFORMANCE_DATA = 'getPerformanceData' + GET_NETWORK_CONNECTION = 'getNetworkConnection' + SET_NETWORK_CONNECTION = 'setNetworkConnection' + + # Android Emulator + SEND_SMS = 'sendSms' + MAKE_GSM_CALL = 'makeGsmCall' + SET_GSM_SIGNAL = 'setGsmSignal' + SET_GSM_VOICE = 'setGsmVoice' + SET_NETWORK_SPEED = 'setNetworkSpeed' + SET_POWER_CAPACITY = 'setPowerCapacity' + SET_POWER_AC = 'setPowerAc' + + # iOS + TOUCH_ID = 'touchId' + TOGGLE_TOUCH_ID_ENROLLMENT = 'toggleTouchIdEnrollment' diff --git a/appium/webdriver/switch_to.py b/appium/webdriver/switch_to.py index 7aa73576..ed9db7c7 100644 --- a/appium/webdriver/switch_to.py +++ b/appium/webdriver/switch_to.py @@ -12,20 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional + from selenium.webdriver.remote.switch_to import SwitchTo +from typing_extensions import Self from .mobilecommand import MobileCommand class MobileSwitchTo(SwitchTo): - def context(self, context_name): - """ - Sets the context for the current session. + def context(self, context_name: Optional[str]) -> Self: + """Sets the context for the current session. + Passing `None` is equal to switching to native context. - :Args: - - context_name: The name of the context to switch to. + Args: + context_name: The name of the context to switch to. - :Usage: + Usage: driver.switch_to.context('WEBVIEW_1') """ self._driver.execute(MobileCommand.SWITCH_TO_CONTEXT, {'name': context_name}) + return self diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 6e39b45a..5cdd2321 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -12,719 +12,484 @@ # See the License for the specific language governing permissions and # limitations under the License. -from selenium import webdriver +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union -from .connectiontype import ConnectionType -from .mobilecommand import MobileCommand as Command +from selenium import webdriver +from selenium.common.exceptions import ( + InvalidArgumentException, + SessionNotCreatedException, + UnknownMethodException, + WebDriverException, +) +from selenium.webdriver.remote.command import Command as RemoteCommand +from selenium.webdriver.remote.remote_connection import RemoteConnection +from typing_extensions import Self + +from appium.common.logger import logger +from appium.options.common.base import AppiumOptions + +from .appium_connection import AppiumConnection +from .client_config import AppiumClientConfig from .errorhandler import MobileErrorHandler +from .extensions.action_helpers import ActionHelpers +from .extensions.android.activities import Activities +from .extensions.android.common import Common +from .extensions.android.display import Display +from .extensions.android.gsm import Gsm +from .extensions.android.network import Network +from .extensions.android.performance import Performance +from .extensions.android.power import Power +from .extensions.android.sms import Sms +from .extensions.android.system_bars import SystemBars +from .extensions.applications import Applications +from .extensions.clipboard import Clipboard +from .extensions.context import Context +from .extensions.device_time import DeviceTime +from .extensions.execute_driver import ExecuteDriver +from .extensions.execute_mobile_command import ExecuteMobileCommand +from .extensions.hw_actions import HardwareActions +from .extensions.images_comparison import ImagesComparison +from .extensions.keyboard import Keyboard +from .extensions.location import Location +from .extensions.log_event import LogEvent +from .extensions.logs import Logs +from .extensions.remote_fs import RemoteFS +from .extensions.screen_record import ScreenRecord +from .extensions.session import Session +from .extensions.settings import Settings +from .locator_converter import AppiumLocatorConverter +from .mobilecommand import MobileCommand as Command from .switch_to import MobileSwitchTo from .webelement import WebElement as MobileWebElement -from appium.webdriver.common.mobileby import MobileBy -from appium.webdriver.common.touch_action import TouchAction -from appium.webdriver.common.multi_action import MultiAction -from selenium.webdriver.common.by import By -from selenium.webdriver.remote.webelement import WebElement +class ExtensionBase: + """ + Used to define an extension command as driver's methods. + Example: + When you want to add `example_command` which calls a get request to + `session/$sessionId/path/to/your/custom/url`. -class WebDriver(webdriver.Remote): - def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', - desired_capabilities=None, browser_profile=None, proxy=None, keep_alive=False): + #. Defines an extension as a subclass of `ExtensionBase` + .. code-block:: python - super(WebDriver, self).__init__(command_executor, desired_capabilities, browser_profile, proxy, keep_alive) + class YourCustomCommand(ExtensionBase): + def method_name(self): + return 'custom_method_name' - if self.command_executor is not None: - self._addCommands() + # Define a method with the name of `method_name` + def custom_method_name(self): + # Generally the response of Appium follows `{ 'value': { data } }` + # format. + return self.execute()['value'] - self.error_handler = MobileErrorHandler() - self._switch_to = MobileSwitchTo(self) + # Used to register the command pair as "Appium command" in this driver. + def add_command(self): + return ('GET', 'session/$sessionId/path/to/your/custom/url') - # add new method to the `find_by_*` pantheon - By.IOS_UIAUTOMATION = MobileBy.IOS_UIAUTOMATION - By.ANDROID_UIAUTOMATOR = MobileBy.ANDROID_UIAUTOMATOR - By.ACCESSIBILITY_ID = MobileBy.ACCESSIBILITY_ID + #. Creates a session with the extension. + .. code-block:: python - # add methods to the WebElement class - WebElement.set_value = set_value + # Appium capabilities + options = AppiumOptions() + driver = webdriver.Remote('http://localhost:4723/wd/hub', options=options, + extensions=[YourCustomCommand]) - @property - def contexts(self): - """ - Returns the contexts within the current session. + #. Calls the custom command + .. code-block:: python - :Usage: - driver.contexts - """ - return self.execute(Command.CONTEXTS)['value'] + # Then, the driver calls a get request against + # `session/$sessionId/path/to/your/custom/url`. `$sessionId` will be + # replaced properly in the driver. Then, the method returns + # the `value` part of the response. + driver.custom_method_name() - @property - def current_context(self): - """ - Returns the current context of the current session. + #. Remove added commands (if needed) + .. code-block:: python - :Usage: - driver.current_context - """ - return self.execute(Command.GET_CURRENT_CONTEXT)['value'] + # New commands are added by `setattr`. They remain in the module, + # so you should explicitly delete them to define the same name method + # with different arguments or process in the method. + driver.delete_extensions() - @property - def context(self): - """ - Returns the current context of the current session. - :Usage: - driver.context - """ - return self.current_context + You can give arbitrary arguments for the command like the below. - def find_element_by_ios_uiautomation(self, uia_string): - """Finds an element by uiautomation in iOS. + .. code-block:: python - :Args: - - uia_string - The element name in the iOS UIAutomation library + class YourCustomCommand(ExtensionBase): + def method_name(self): + return 'custom_method_name' - :Usage: - driver.find_element_by_ios_uiautomation('.elements()[1].cells()[2]') - """ - return self.find_element(by=By.IOS_UIAUTOMATION, value=uia_string) + def test_command(self, argument): + return self.execute(argument)['value'] - def find_elements_by_ios_uiautomation(self, uia_string): - """Finds elements by uiautomation in iOS. + def add_command(self): + return ('post', 'session/$sessionId/path/to/your/custom/url') - :Args: - - uia_string - The element name in the iOS UIAutomation library + driver = webdriver.Remote('http://localhost:4723/wd/hub', options=options, + extensions=[YourCustomCommand]) - :Usage: - driver.find_elements_by_ios_uiautomation('.elements()[1].cells()[2]') - """ - return self.find_elements(by=By.IOS_UIAUTOMATION, value=uia_string) + # Then, the driver sends a post request to `session/$sessionId/path/to/your/custom/url` + # with `{'dummy_arg': 'as a value'}` JSON body. + driver.custom_method_name({'dummy_arg': 'as a value'}) - def find_element_by_android_uiautomator(self, uia_string): - """Finds element by uiautomator in Android. - :Args: - - uia_string - The element name in the Android UIAutomator library + When you customize the URL dynamically with element id. - :Usage: - driver.find_element_by_android_uiautomator('.elements()[1].cells()[2]') - """ - return self.find_element(by=By.ANDROID_UIAUTOMATOR, value=uia_string) + .. code-block:: python - def find_elements_by_android_uiautomator(self, uia_string): - """Finds elements by uiautomator in Android. + class CustomURLCommand(ExtensionBase): + def method_name(self): + return 'custom_method_name' - :Args: - - uia_string - The element name in the Android UIAutomator library + def custom_method_name(self, element_id): + return self.execute({'id': element_id})['value'] - :Usage: - driver.find_elements_by_android_uiautomator('.elements()[1].cells()[2]') - """ - return self.find_elements(by=By.ANDROID_UIAUTOMATOR, value=uia_string) + def add_command(self): + return ('GET', 'session/$sessionId/path/to/your/custom/$id/url') - def find_element_by_accessibility_id(self, id): - """Finds an element by accessibility id. + driver = webdriver.Remote('http://localhost:4723/wd/hub', options=options, + extensions=[YourCustomCommand]) + element = driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='id') - :Args: - - id - a string corresponding to a recursive element search using the - Id/Name that the native Accessibility options utilize + # Then, the driver calls a get request to `session/$sessionId/path/to/your/custom/$id/url` + # with replacing the `$id` with the given `element.id` + driver.custom_method_name(element.id) - :Usage: - driver.find_element_by_accessibility_id() - """ - return self.find_element(by=By.ACCESSIBILITY_ID, value=id) + """ - def find_elements_by_accessibility_id(self, id): - """Finds elements by accessibility id. + def __init__(self, execute: Callable[[str, Dict], Dict[str, Any]]): + self._execute = execute - :Args: - - id - a string corresponding to a recursive element search using the - Id/Name that the native Accessibility options utilize + def execute(self, parameters: Union[Dict[str, Any], None] = None) -> Any: + param = {} + if parameters: + param = parameters + return self._execute(self.method_name(), param) - :Usage: - driver.find_elements_by_accessibility_id() + def method_name(self) -> str: """ - return self.find_elements(by=By.ACCESSIBILITY_ID, value=id) + Expected to return a method name. + This name will be available as a driver method. - def create_web_element(self, element_id): + Returns: + 'str' The method name. """ - Creates a web element with the specified element_id. - Overrides method in Selenium WebDriver in order to always give them - Appium WebElement - """ - return MobileWebElement(self, element_id) + raise NotImplementedError() - # convenience method added to Appium (NOT Selenium 3) - def scroll(self, origin_el, destination_el): - """Scrolls from one element to another - - :Args: - - originalEl - the element from which to being scrolling - - destinationEl - the element to scroll to - - :Usage: - driver.scroll(el1, el2) + def add_command(self) -> Tuple[str, str]: """ - action = TouchAction(self) - action.press(origin_el).move_to(destination_el).release().perform() - return self - - # convenience method added to Appium (NOT Selenium 3) - def drag_and_drop(self, origin_el, destination_el): - """Drag the origin element to the destination element - - :Args: - - originEl - the element to drag - - destinationEl - the element to drag to - """ - action = TouchAction(self) - action.long_press(origin_el).move_to(destination_el).release().perform() - return self - - # convenience method added to Appium (NOT Selenium 3) - def tap(self, positions, duration=None): - """Taps on an particular place with up to five fingers, holding for a - certain time - - :Args: - - positions - an array of tuples representing the x/y coordinates of - the fingers to tap. Length can be up to five. - - duration - (optional) length of time to tap, in ms - - :Usage: - driver.tap([(100, 20), (100, 60), (100, 100)], 500) + Expected to define the pair of HTTP method and its URL. """ - if len(positions) == 1: - action = TouchAction(self) - x = positions[0][0] - y = positions[0][1] - if duration: - duration = duration - action.long_press(x=x, y=y, duration=duration).release() - else: - action.tap(x=x, y=y).release() - action.perform() - else: - ma = MultiAction(self) - for position in positions: - x = position[0] - y = position[1] - action = TouchAction(self) - if duration: - duration *= 1000 # we take seconds, but send milliseconds - action.long_press(x=x, y=y, duration=duration).release() - else: - action.press(x=x, y=y).release() - ma.add(action) - - ma.perform() - return self + raise NotImplementedError() - # convenience method added to Appium (NOT Selenium 3) - def swipe(self, start_x, start_y, end_x, end_y, duration=None): - """Swipe from one point to another point, for an optional duration. - :Args: - - start_x - x-coordinate at which to start - - start_y - y-coordinate at which to end - - end_x - x-coordinate at which to stop - - end_y - y-coordinate at which to stop - - duration - (optional) time to take the swipe, in ms. - - :Usage: - driver.swipe(100, 100, 100, 400) - """ - # `swipe` is something like press-wait-move_to-release, which the server - # will translate into the correct action - action = TouchAction(self) - action \ - .press(x=start_x, y=start_y) \ - .wait(ms=duration) \ - .move_to(x=end_x, y=end_y) \ - .release() - action.perform() - return self - - # convenience method added to Appium (NOT Selenium 3) - def flick(self, start_x, start_y, end_x, end_y): - """Flick from one point to another point. +def _get_remote_connection_and_client_config( + command_executor: Union[str, AppiumConnection], client_config: Optional[AppiumClientConfig] = None +) -> tuple[AppiumConnection, Optional[AppiumClientConfig]]: + """Return the pair of command executor and client config. + If the given command executor is a custom one, returned client config will + be None since the custom command executor has its own client config already. + The custom command executor's one will be prior than the given client config. + """ + if not isinstance(command_executor, str): + # client config already defined in the custom command executor + # will be prior than the given one. + return (command_executor, None) + + # command_executor is str + + # Do not keep None to avoid warnings in Selenium + # which can prevent with ClientConfig instance usage. + new_client_config = AppiumClientConfig(remote_server_addr=command_executor) if client_config is None else client_config + return (AppiumConnection(client_config=new_client_config), new_client_config) + + +class WebDriver( + webdriver.Remote, + ActionHelpers, + Activities, + Applications, + Clipboard, + Context, + Common, + DeviceTime, + Display, + ExecuteDriver, + ExecuteMobileCommand, + Gsm, + HardwareActions, + ImagesComparison, + Keyboard, + Location, + LogEvent, + Logs, + Network, + Performance, + Power, + RemoteFS, + ScreenRecord, + Session, + Settings, + Sms, + SystemBars, +): + def __init__( + self, + command_executor: Union[str, AppiumConnection] = 'http://127.0.0.1:4723', + extensions: Optional[List[Type['ExtensionBase']]] = None, + options: Union[AppiumOptions, List[AppiumOptions], None] = None, + client_config: Optional[AppiumClientConfig] = None, + ): + command_executor, client_config = _get_remote_connection_and_client_config( + command_executor=command_executor, client_config=client_config + ) + super().__init__( + command_executor=command_executor, + options=options, # type: ignore[arg-type] + locator_converter=AppiumLocatorConverter(), + web_element_cls=MobileWebElement, + client_config=client_config, + ) + + # to explicitly set type after the initialization + self.command_executor: RemoteConnection + + self._add_commands() - :Args: - - start_x - x-coordinate at which to start - - start_y - y-coordinate at which to end - - end_x - x-coordinate at which to stop - - end_y - y-coordinate at which to stop + self.error_handler = MobileErrorHandler() - :Usage: - driver.flick(100, 100, 100, 400) - """ - action = TouchAction(self) - action \ - .press(x=start_x, y=start_y) \ - .move_to(x=end_x, y=end_y) \ - .release() - action.perform() - return self + if client_config and client_config.direct_connection: + self._update_command_executor(keep_alive=client_config.keep_alive) + + self._absent_extensions: Set[str] = set() + + self._extensions = extensions or [] + for extension in self._extensions: + instance = extension(self.execute) + method_name = instance.method_name() + if hasattr(WebDriver, method_name): + logger.debug(f"Overriding the method '{method_name}'") + + # add a new method named 'instance.method_name()' and call it + setattr(WebDriver, method_name, getattr(instance, method_name)) + method, url_cmd = instance.add_command() + self.command_executor.add_command(method_name, method.upper(), url_cmd) + + if TYPE_CHECKING: + + def find_element(self, by: str, value: Union[str, Dict, None] = None) -> 'MobileWebElement': # type: ignore[override] + ... + + def find_elements(self, by: str, value: Union[str, Dict, None] = None) -> List['MobileWebElement']: # type: ignore[override] + ... + + def delete_extensions(self) -> None: + """Delete extensions added in the class with 'setattr'""" + for extension in self._extensions: + instance = extension(self.execute) + method_name = instance.method_name() + if hasattr(WebDriver, method_name): + delattr(WebDriver, method_name) + + def _update_command_executor(self, keep_alive: bool) -> None: + """Update command executor following directConnect feature""" + direct_protocol = 'directConnectProtocol' + direct_host = 'directConnectHost' + direct_port = 'directConnectPort' + direct_path = 'directConnectPath' + + assert self.caps, 'Driver capabilities must be defined' + if not {direct_protocol, direct_host, direct_port, direct_path}.issubset(set(self.caps)): + message = 'Direct connect capabilities from server were:\n' + for key in [direct_protocol, direct_host, direct_port, direct_path]: + message += f"{key}: '{self.caps.get(key, '')}' " + logger.debug(message) + return + + protocol = self.caps[direct_protocol] + hostname = self.caps[direct_host] + port = self.caps[direct_port] + path = self.caps[direct_path] + executor = f'{protocol}://{hostname}:{port}{path}' + + logger.debug('Updated request endpoint to %s', executor) + # Override command executor. + if isinstance(self.command_executor, AppiumConnection): # type: ignore + self.command_executor = AppiumConnection(executor, keep_alive=keep_alive) + else: + self.command_executor = RemoteConnection(executor, keep_alive=keep_alive) + self._add_commands() - # convenience method added to Appium (NOT Selenium 3) - def pinch(self, element=None, percent=200, steps=50): - """Pinch on an element a certain amount + # https://github.com/SeleniumHQ/selenium/blob/06fdf2966df6bca47c0ae45e8201cd30db9b9a49/py/selenium/webdriver/remote/webdriver.py#L277 + # noinspection PyAttributeOutsideInit + def start_session(self, capabilities: Union[Dict, AppiumOptions], browser_profile: Optional[str] = None) -> None: + """Creates a new session with the desired capabilities. - :Args: - - element - the element to pinch - - percent - (optional) amount to pinch. Defaults to 200% - - steps - (optional) number of steps in the pinch action + Override for Appium - :Usage: - driver.pinch(element) + Args: + capabilities: Read https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md + for more details. + browser_profile: Browser profile """ - if element: - element = element.id - - opts = { - 'element': element, - 'percent': percent, - 'steps': steps, - } - self.execute_script('mobile: pinchClose', opts) - return self - - # convenience method added to Appium (NOT Selenium 3) - def zoom(self, element=None, percent=200, steps=50): - """Zooms in on an element a certain amount + if not isinstance(capabilities, (dict, AppiumOptions)): + raise InvalidArgumentException('Capabilities must be a dictionary or AppiumOptions instance') - :Args: - - element - the element to zoom - - percent - (optional) amount to zoom. Defaults to 200% - - steps - (optional) number of steps in the zoom action + w3c_caps = AppiumOptions.as_w3c(capabilities) if isinstance(capabilities, dict) else capabilities.to_w3c() + response = self.execute(RemoteCommand.NEW_SESSION, w3c_caps) + # https://w3c.github.io/webdriver/#new-session + if not isinstance(response, dict): + raise SessionNotCreatedException( + f'A valid W3C session creation response must be a dictionary. Got "{response}" instead' + ) + # Due to a W3C spec parsing misconception some servers + # pack the createSession response stuff into 'value' dictionary and + # some other put it to the top level of the response JSON nesting hierarchy + get_response_value: Callable[[str], Optional[Any]] = lambda key: ( + response.get(key) or (response['value'].get(key) if isinstance(response.get('value'), dict) else None) + ) + session_id = get_response_value('sessionId') + if not session_id: + raise SessionNotCreatedException( + f'A valid W3C session creation response must contain a non-empty "sessionId" entry. Got "{response}" instead' + ) + self.session_id = session_id + self.caps = get_response_value('capabilities') or {} - :Usage: - driver.zoom(element) + def get_status(self) -> Dict: """ - if element: - element = element.id - - opts = { - 'element': element, - 'percent': percent, - 'steps': steps, - } - self.execute_script('mobile: pinchOpen', opts) - return self + Get the Appium server status - def app_strings(self, language=None): - """Returns the application strings from the device for the specified - language. + Usage: + driver.get_status() - :Args: - - language - strings language code - """ - data = {} - if language != None: - data['language'] = language - return self.execute(Command.GET_APP_STRINGS, data)['value'] + Returns: + dict: The status information - def reset(self): - """Resets the current application on the device. """ - self.execute(Command.RESET) - return self - - def hide_keyboard(self, key_name=None, key=None, strategy=None): - """Hides the software keyboard on the device. In iOS, use `key_name` to press - a particular key, or `strategy`. In Android, no parameters are used. + return self.execute(Command.GET_STATUS)['value'] - :Args: - - key_name - key to press - - strategy - strategy for closing the keyboard (e.g., `tapOutside`) - """ - data = {} - if key_name is not None: - data['keyName'] = key_name - elif key is not None: - data['key'] = key - else: - # defaults to `tapOutside` strategy - strategy = 'tapOutside' - data['strategy'] = strategy - self.execute(Command.HIDE_KEYBOARD, data) - return self + def create_web_element(self, element_id: Union[int, str]) -> MobileWebElement: + """Creates a web element with the specified element_id. - # Needed for Selendroid - def keyevent(self, keycode, metastate=None): - """Sends a keycode to the device. Android only. Possible keycodes can be - found in http://developer.android.com/reference/android/view/KeyEvent.html. - - :Args: - - keycode - the keycode to be sent to the device - - metastate - meta information about the keycode being sent - """ - data = { - 'keycode': keycode, - } - if metastate is not None: - data['metastate'] = metastate - self.execute(Command.KEY_EVENT, data) - return self - - def press_keycode(self, keycode, metastate=None): - """Sends a keycode to the device. Android only. Possible keycodes can be - found in http://developer.android.com/reference/android/view/KeyEvent.html. - - :Args: - - keycode - the keycode to be sent to the device - - metastate - meta information about the keycode being sent - """ - data = { - 'keycode': keycode, - } - if metastate is not None: - data['metastate'] = metastate - self.execute(Command.PRESS_KEYCODE, data) - return self + Overrides method in Selenium WebDriver in order to always give them + Appium WebElement - def long_press_keycode(self, keycode, metastate=None): - """Sends a long press of keycode to the device. Android only. Possible keycodes can be - found in http://developer.android.com/reference/android/view/KeyEvent.html. + Args: + element_id: The element id to create a web element - :Args: - - keycode - the keycode to be sent to the device - - metastate - meta information about the keycode being sent + Returns: + `MobileWebElement` """ - data = { - 'keycode': keycode - } - if metastate != None: - data['metastate'] = metastate - self.execute(Command.LONG_PRESS_KEYCODE, data) - return self + return MobileWebElement(self, element_id) @property - def current_activity(self): - """Retrieves the current activity on the device. - """ - return self.execute(Command.GET_CURRENT_ACTIVITY)['value'] + def switch_to(self) -> MobileSwitchTo: + """Returns an object containing all options to switch focus into - def set_value(self, element, value): - """Set the value on an element in the application. + Override for appium - :Args: - - element - the element whose value will be set - - Value - the value to set on the element - """ - data = { - 'elementId': element.id, - 'value': [value], - } - self.execute(Command.SET_IMMEDIATE_VALUE, data) - return self + Returns: + `appium.webdriver.switch_to.MobileSwitchTo` - def pull_file(self, path): - """Retrieves the file at `path`. Returns the file's content encoded as - Base64. - - :Args: - - path - the path to the file on the device """ - data = { - 'path': path, - } - return self.execute(Command.PULL_FILE, data)['value'] - def pull_folder(self, path): - """Retrieves a folder at `path`. Returns the folder's contents zipped - and encoded as Base64. + return MobileSwitchTo(self) - :Args: - - path - the path to the folder on the device + # MJSONWP for Selenium v4 + @property # type: ignore[override] + def orientation(self) -> str: """ - data = { - 'path': path, - } - return self.execute(Command.PULL_FOLDER, data)['value'] - - def push_file(self, path, base64data): - """Puts the data, encoded as Base64, in the file specified as `path`. + Gets the current orientation of the device - :Args: - - path - the path on the device - - base64data - data, encoded as Base64, to be written to the file - """ - data = { - 'path': path, - 'data': base64data, - } - self.execute(Command.PUSH_FILE, data) - return self + Example: - def complex_find(self, selector): - """Performs a find for elements in the current application. + .. code-block:: python - :Args: - - selector - an array of selection criteria + orientation = driver.orientation """ - data = { - 'selector': selector, - } - return self.execute(Command.COMPLEX_FIND, data)['value'] + return self.execute(Command.GET_SCREEN_ORIENTATION)['value'] - def background_app(self, seconds): - """Puts the application in the background on the device for a certain - duration. - - :Args: - - seconds - the duration for the application to remain in the background + # MJSONWP for Selenium v4 + @orientation.setter + def orientation(self, value: str) -> None: """ - data = { - 'seconds': seconds, - } - self.execute(Command.BACKGROUND, data) - return self - - def is_app_installed(self, bundle_id): - """Checks whether the application specified by `bundle_id` is installed - on the device. - - :Args: - - bundle_id - the id of the application to query - """ - data = { - 'bundleId': bundle_id, - } - return self.execute(Command.IS_APP_INSTALLED, data)['value'] - - def install_app(self, app_path): - """Install the application found at `app_path` on the device. - - :Args: - - app_path - the local or remote path to the application to install - """ - data = { - 'appPath': app_path, - } - self.execute(Command.INSTALL_APP, data) - return self + Sets the current orientation of the device - def remove_app(self, app_id): - """Remove the specified application from the device. + Args: + - value: orientation to set it to. - :Args: - - app_id - the application id to be removed - """ - data = { - 'appId': app_id, - } - self.execute(Command.REMOVE_APP, data) - return self + Example: + .. code-block:: python - def launch_app(self): - """Start on the device the application specified in the desired capabilities. + driver.orientation = 'landscape' """ - self.execute(Command.LAUNCH_APP) - return self + allowed_values = ['LANDSCAPE', 'PORTRAIT'] + if value.upper() in allowed_values: + self.execute(Command.SET_SCREEN_ORIENTATION, {'orientation': value}) + else: + raise WebDriverException("You can only set the orientation to 'LANDSCAPE' and 'PORTRAIT'") - def close_app(self): - """Stop the running application, specified in the desired capabilities, on - the device. + def assert_extension_exists(self, ext_name: str) -> Self: """ - self.execute(Command.CLOSE_APP) - return self - - def end_test_coverage(self, intent, path): - """Ends the coverage collection and pull the coverage.ec file from the device. - Android only. - - See https://github.com/appium/appium/blob/master/docs/en/android_coverage.md + Verifies if the given extension is not present in the list of absent extensions + for the given driver instance. + This API is designed for private usage. - :Args: - - intent - description of operation to be performed - - path - path to coverage.ec file to be pulled from the device - """ - data = { - 'intent': intent, - 'path': path, - } - return self.execute(Command.END_TEST_COVERAGE, data)['value'] + Args: + ext_name: extension name - def lock(self, seconds): - """Lock the device for a certain period of time. iOS only. + Returns: + self instance for chaining - :Args: - - the duration to lock the device, in seconds + Raises: + UnknownMethodException: If the extension has been marked as absent once """ - data = { - 'seconds': seconds, - } - self.execute(Command.LOCK, data) + if ext_name in self._absent_extensions: + raise UnknownMethodException() return self - def shake(self): - """Shake the device. + def mark_extension_absence(self, ext_name: str) -> Self: """ - self.execute(Command.SHAKE) - return self + Marks the given extension as absent for the given driver instance. + This API is designed for private usage. - def open_notifications(self): - """Open notification shade in Android (API Level 18 and above) - """ - self.execute(Command.OPEN_NOTIFICATIONS, {}) - return self + Args: + ext_name: extension name - @property - def network_connection(self): - """Returns an integer bitmask specifying the network connection type. - Android only. - Possible values are available through the enumeration `appium.webdriver.ConnectionType` - """ - return self.execute(Command.GET_NETWORK_CONNECTION, {})['value'] - - def set_network_connection(self, connectionType): - """Sets the network connection type. Android only. - Possible values: - Value (Alias) | Data | Wifi | Airplane Mode - ------------------------------------------------- - 0 (None) | 0 | 0 | 0 - 1 (Airplane Mode) | 0 | 0 | 1 - 2 (Wifi only) | 0 | 1 | 0 - 4 (Data only) | 1 | 0 | 0 - 6 (All network on) | 1 | 1 | 0 - These are available through the enumeration `appium.webdriver.ConnectionType` - - :Args: - - connectionType - a member of the enum appium.webdriver.ConnectionType + Returns: + self instance for chaining """ - data = { - 'parameters': { - 'type': connectionType.value - } - } - return self.execute(Command.SET_NETWORK_CONNECTION, data)['value'] - - @property - def available_ime_engines(self): - """Get the available input methods for an Android device. Package and - activity are returned (e.g., ['com.android.inputmethod.latin/.LatinIME']) - Android only. - """ - return self.execute(Command.GET_AVAILABLE_IME_ENGINES, {})['value'] - - def is_ime_active(self): - """Checks whether the device has IME service active. Returns True/False. - Android only. - """ - return self.execute(Command.IS_IME_ACTIVE, {})['value'] - - def activate_ime_engine(self, engine): - """Activates the given IME engine on the device. - Android only. - - :Args: - - engine - the package and activity of the IME engine to activate (e.g., - 'com.android.inputmethod.latin/.LatinIME') - """ - data = { - 'engine': engine - } - self.execute(Command.ACTIVATE_IME_ENGINE, data) + logger.debug(f'Marking driver extension "{ext_name}" as absent for the current instance') + self._absent_extensions.add(ext_name) return self - def deactivate_ime_engine(self): - """Deactivates the currently active IME engine on the device. - Android only. - """ - self.execute(Command.DEACTIVATE_IME_ENGINE, {}) - return self - - @property - def active_ime_engine(self): - """Returns the activity and package of the currently active IME engine (e.g., - 'com.android.inputmethod.latin/.LatinIME'). - Android only. - """ - return self.execute(Command.GET_ACTIVE_IME_ENGINE, {})['value'] - - - def _addCommands(self): - self.command_executor._commands[Command.CONTEXTS] = \ - ('GET', '/session/$sessionId/contexts') - self.command_executor._commands[Command.GET_CURRENT_CONTEXT] = \ - ('GET', '/session/$sessionId/context') - self.command_executor._commands[Command.SWITCH_TO_CONTEXT] = \ - ('POST', '/session/$sessionId/context') - self.command_executor._commands[Command.TOUCH_ACTION] = \ - ('POST', '/session/$sessionId/touch/perform') - self.command_executor._commands[Command.MULTI_ACTION] = \ - ('POST', '/session/$sessionId/touch/multi/perform') - self.command_executor._commands[Command.GET_APP_STRINGS] = \ - ('POST', '/session/$sessionId/appium/app/strings') - # Needed for Selendroid - self.command_executor._commands[Command.KEY_EVENT] = \ - ('POST', '/session/$sessionId/appium/device/keyevent') - self.command_executor._commands[Command.PRESS_KEYCODE] = \ - ('POST', '/session/$sessionId/appium/device/press_keycode') - self.command_executor._commands[Command.LONG_PRESS_KEYCODE] = \ - ('POST', '/session/$sessionId/appium/device/long_press_keycode') - self.command_executor._commands[Command.GET_CURRENT_ACTIVITY] = \ - ('GET', '/session/$sessionId/appium/device/current_activity') - self.command_executor._commands[Command.SET_IMMEDIATE_VALUE] = \ - ('POST', '/session/$sessionId/appium/element/$elementId/value') - self.command_executor._commands[Command.PULL_FILE] = \ - ('POST', '/session/$sessionId/appium/device/pull_file') - self.command_executor._commands[Command.PULL_FOLDER] = \ - ('POST', '/session/$sessionId/appium/device/pull_folder') - self.command_executor._commands[Command.PUSH_FILE] = \ - ('POST', '/session/$sessionId/appium/device/push_file') - self.command_executor._commands[Command.COMPLEX_FIND] = \ - ('POST', '/session/$sessionId/appium/app/complex_find') - self.command_executor._commands[Command.BACKGROUND] = \ - ('POST', '/session/$sessionId/appium/app/background') - self.command_executor._commands[Command.IS_APP_INSTALLED] = \ - ('POST', '/session/$sessionId/appium/device/app_installed') - self.command_executor._commands[Command.INSTALL_APP] = \ - ('POST', '/session/$sessionId/appium/device/install_app') - self.command_executor._commands[Command.REMOVE_APP] = \ - ('POST', '/session/$sessionId/appium/device/remove_app') - self.command_executor._commands[Command.LAUNCH_APP] = \ - ('POST', '/session/$sessionId/appium/app/launch') - self.command_executor._commands[Command.CLOSE_APP] = \ - ('POST', '/session/$sessionId/appium/app/close') - self.command_executor._commands[Command.END_TEST_COVERAGE] = \ - ('POST', '/session/$sessionId/appium/app/end_test_coverage') - self.command_executor._commands[Command.LOCK] = \ - ('POST', '/session/$sessionId/appium/device/lock') - self.command_executor._commands[Command.SHAKE] = \ - ('POST', '/session/$sessionId/appium/device/shake') - self.command_executor._commands[Command.RESET] = \ - ('POST', '/session/$sessionId/appium/app/reset') - self.command_executor._commands[Command.HIDE_KEYBOARD] = \ - ('POST', '/session/$sessionId/appium/device/hide_keyboard') - self.command_executor._commands[Command.OPEN_NOTIFICATIONS] = \ - ('POST', '/session/$sessionId/appium/device/open_notifications') - self.command_executor._commands[Command.GET_NETWORK_CONNECTION] = \ - ('GET', '/session/$sessionId/network_connection') - self.command_executor._commands[Command.SET_NETWORK_CONNECTION] = \ - ('POST', '/session/$sessionId/network_connection') - self.command_executor._commands[Command.GET_AVAILABLE_IME_ENGINES] = \ - ('GET', '/session/$sessionId/ime/available_engines') - self.command_executor._commands[Command.IS_IME_ACTIVE] = \ - ('GET', '/session/$sessionId/ime/activated') - self.command_executor._commands[Command.ACTIVATE_IME_ENGINE] = \ - ('POST', '/session/$sessionId/ime/activate') - self.command_executor._commands[Command.DEACTIVATE_IME_ENGINE] = \ - ('POST', '/session/$sessionId/ime/deactivate') - self.command_executor._commands[Command.GET_ACTIVE_IME_ENGINE] = \ - ('GET', '/session/$sessionId/ime/active_engine') - self.command_executor._commands[Command.REPLACE_KEYS] = \ - ('POST', '/session/$sessionId/appium/element/$elementId/replace_value') - - -# monkeypatched method for WebElement -def set_value(self, value): - """Set the value on this element in the application - """ - data = { - 'elementId': self.id, - 'value': [value], - } - self._execute(Command.SET_IMMEDIATE_VALUE, data) - return self + def _add_commands(self) -> None: + # call the overridden command binders from all mixin classes except for + # appium.webdriver.webdriver.WebDriver and its sub-classes + # https://github.com/appium/python-client/issues/342 + for mixin_class in filter(lambda x: not issubclass(x, WebDriver), self.__class__.__mro__): + if hasattr(mixin_class, self._add_commands.__name__): + get_atter = getattr(mixin_class, self._add_commands.__name__, None) + if get_atter: + get_atter(self) + + self.command_executor.add_command(Command.GET_STATUS, 'GET', '/status') + + # TODO Move commands for element to webelement + self.command_executor.add_command(Command.CLEAR, 'POST', '/session/$sessionId/element/$id/clear') + self.command_executor.add_command( + Command.LOCATION_IN_VIEW, + 'GET', + '/session/$sessionId/element/$id/location_in_view', + ) + + # MJSONWP for Selenium v4 + self.command_executor.add_command(Command.IS_ELEMENT_DISPLAYED, 'GET', '/session/$sessionId/element/$id/displayed') + self.command_executor.add_command(Command.GET_CAPABILITIES, 'GET', '/session/$sessionId') + + self.command_executor.add_command(Command.GET_SCREEN_ORIENTATION, 'GET', '/session/$sessionId/orientation') + self.command_executor.add_command(Command.SET_SCREEN_ORIENTATION, 'POST', '/session/$sessionId/orientation') diff --git a/appium/webdriver/webelement.py b/appium/webdriver/webelement.py index 01b52a3b..61076ca3 100644 --- a/appium/webdriver/webelement.py +++ b/appium/webdriver/webelement.py @@ -12,94 +12,119 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .mobilecommand import MobileCommand as Command +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Union -from selenium.webdriver.common.by import By +from selenium.webdriver.common.utils import keys_to_typing +from selenium.webdriver.remote.command import Command as RemoteCommand from selenium.webdriver.remote.webelement import WebElement as SeleniumWebElement +from typing_extensions import Self + +from .mobilecommand import MobileCommand as Command class WebElement(SeleniumWebElement): - def find_element_by_ios_uiautomation(self, uia_string): - """Finds an element by uiautomation in iOS. + _execute: Callable + _id: str - :Args: - - uia_string - The element name in the iOS UIAutomation library + if TYPE_CHECKING: - :Usage: - driver.find_element_by_ios_uiautomation('.elements()[1].cells()[2]') - """ - return self.find_element(by=By.IOS_UIAUTOMATION, value=uia_string) + def find_element(self, by: str, value: Union[str, Dict, None] = None) -> Self: # type: ignore[override] + ... - def find_elements_by_ios_uiautomation(self, uia_string): - """Finds elements by uiautomation in iOS. + def find_elements(self, by: str, value: Union[str, Dict, None] = None) -> List[Self]: # type: ignore[override] + ... - :Args: - - uia_string - The element name in the iOS UIAutomation library + def get_attribute(self, name: str) -> Optional[Union[str, Dict]]: # type: ignore[override] + """Gets the given attribute or property of the element. - :Usage: - driver.find_elements_by_ios_uiautomation('.elements()[1].cells()[2]') - """ - return self.find_elements(by=By.IOS_UIAUTOMATION, value=uia_string) + Override for Appium + + This method will first try to return the value of a property with the + given name. If a property with that name doesn't exist, it returns the + value of the attribute with the same name. If there's no attribute with + that name, ``None`` is returned. + + Values which are considered truthy, that is equals "true" or "false", + are returned as booleans. All other non-``None`` values are returned + as strings. For attributes or properties which do not exist, ``None`` + is returned. + + Args: + name: Name of the attribute/property to retrieve. - def find_element_by_android_uiautomator(self, uia_string): - """Finds element by uiautomator in Android. + Usage: + # Check if the "active" CSS class is applied to an element. - :Args: - - uia_string - The element name in the Android UIAutomator library + is_active = "active" in target_element.get_attribute("class") - :Usage: - driver.find_element_by_android_uiautomator('.elements()[1].cells()[2]') + Returns: + The given attribute or property of the element """ - return self.find_element(by=By.ANDROID_UIAUTOMATOR, value=uia_string) - def find_elements_by_android_uiautomator(self, uia_string): - """Finds elements by uiautomator in Android. + resp = self._execute(RemoteCommand.GET_ELEMENT_ATTRIBUTE, {'name': name}) + attribute_value = resp.get('value') - :Args: - - uia_string - The element name in the Android UIAutomator library + if attribute_value is None: + return None - :Usage: - driver.find_elements_by_android_uiautomator('.elements()[1].cells()[2]') + if isinstance(attribute_value, dict): + return attribute_value + + # Convert to str along to the spec + if not isinstance(attribute_value, str): + attribute_value = str(attribute_value) + + if name != 'value' and attribute_value.lower() in ('true', 'false'): + return attribute_value.lower() + + return attribute_value + + def is_displayed(self) -> bool: + """Whether the element is visible to a user. + + Override for Appium """ - return self.find_elements(by=By.ANDROID_UIAUTOMATOR, value=uia_string) + return self._execute(Command.IS_ELEMENT_DISPLAYED)['value'] - def find_element_by_accessibility_id(self, id): - """Finds an element by accessibility id. + def clear(self) -> Self: # type: ignore[override] + """Clears text. - :Args: - - id - a string corresponding to a recursive element search using the - Id/Name that the native Accessibility options utilize + Override for Appium - :Usage: - driver.find_element_by_accessibility_id() + Returns: + `appium.webdriver.webelement.WebElement` """ - return self.find_element(by=By.ACCESSIBILITY_ID, value=id) - def find_elements_by_accessibility_id(self, id): - """Finds elements by accessibility id. + # NOTE: this method is overridden because the selenium client returned None instead of self. + # Appium python client would like to allow users to chain methods. + data = {'id': self.id} + self._execute(Command.CLEAR, data) + return self + + @property + def location_in_view(self) -> Dict[str, int]: + """Gets the location of an element relative to the view. - :Args: - - id - a string corresponding to a recursive element search using the - Id/Name that the native Accessibility options utilize + Usage: + | location = element.location_in_view + | x = location['x'] + | y = location['y'] - :Usage: - driver.find_elements_by_accessibility_id() + Returns: + dict: The location of an element relative to the view """ - return self.find_elements(by=By.ACCESSIBILITY_ID, value=id) + return self._execute(Command.LOCATION_IN_VIEW)['value'] - def set_text(self, keys=''): - """Sends text to the element. Previous text is removed. - Android only. + # Override + def send_keys(self, *value: str) -> Self: # type: ignore[override] + """Simulates typing into the element. - :Args: - - keys - the text to be sent to the element. + Args: + value: A string for typing. - :Usage: - element.set_text('some text') + Returns: + `appium.webdriver.webelement.WebElement` """ - data = { - 'elementId': self._id, - 'value': [keys] - } - self._execute(Command.REPLACE_KEYS, data) + keys = keys_to_typing(value) + self._execute(RemoteCommand.SEND_KEYS_TO_ELEMENT, {'text': ''.join(keys), 'value': keys}) return self diff --git a/docgen.py b/docgen.py deleted file mode 100644 index d0f0742c..00000000 --- a/docgen.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python - -# PyPi expects a reStructuredText (http://docutils.sourceforge.net/rst.html) -# document for its readme. This takes the Github one and makes the requisite -# file. Run when README.md is changed and those changes should be reflected -# on PyPi. - -import pandoc -import os - -pandoc.core.PANDOC_PATH = os.environ['PANDOC_HOME'] - -doc = pandoc.Document() -doc.markdown = open('README.md').read() -f = open('README.txt','w+') -f.write(doc.rst) -f.close() diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..3fb41af6 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= uv run sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..ed9b03f1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,14 @@ +### How to check generated doc + +```bash +$cd python-client/docs +$bash generate.sh +$cd python-client/docs/_build/html +$python -m http.server 1234 +``` + +Access to http://localhost:1234 on web browser + + +### How to deploy generated doc +Handled at https://github.com/ki4070ma/python-client-sphinx diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..236b9e5d --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,58 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +sys.path.insert(0, os.path.abspath('../appium')) + + +# -- Project information ----------------------------------------------------- + +project = 'Python client 1.1' +copyright = '2020-2025, Appium' +author = 'Appium' + +# The full version, including alpha/beta/rc tags +release = '1.1' + +language = 'en' + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.githubpages'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +add_module_names = False diff --git a/docs/generate.sh b/docs/generate.sh new file mode 100644 index 00000000..71131e83 --- /dev/null +++ b/docs/generate.sh @@ -0,0 +1,4 @@ +#!/bin/sh +rm -rf *rst _build +uv run sphinx-apidoc -F -H 'Appium python client' -o . ../appium/webdriver +make html diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..9c16ec4d --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,18 @@ +.. Appium python client documentation master file, created by + sphinx-quickstart on Mon Aug 11 09:34:52 2025. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Appium python client documentation +================================== + +Add your content using ``reStructuredText`` syntax. See the +`reStructuredText `_ +documentation for details. + + +.. toctree:: + :maxdepth: 4 + :caption: Contents: + + webdriver diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..6247f7e2 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/roadmap.md b/docs/roadmap.md deleted file mode 100644 index cf61486a..00000000 --- a/docs/roadmap.md +++ /dev/null @@ -1,36 +0,0 @@ -Appium Python Client Plan -========================= - -This library will be a simple extension of the official Python bindings, through -subclassing, to add the new methods. I would like to maintain the same package -structure, so that switching to the Appium library would be a matter of changing -the import. - -The official client allows for three ways to interact with the server: with the -`selenium` class, with the `webdriver.Remote` class, and with specific browser -classes, which subclass `webdriver.Remote` in `webdriver.*` classes. It seems -like we would not need to update the browser classes for our use case, and the -first is for RC, which we don't support. Thus we can subclass the official -`webdriver.Remote` classes and add the new methods. Otherwise we would need to -use composition, since we have to change the base class and subclasses. - -Usage will remain as it currently is, using the first two methods from above, -other than importing from Appium: - -```python -from appium import webdriver - -desired_caps = {} -# ... - -driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - -print driver.get_window_size() -elem = driver.find_element_by_name('Graphics') -elem.click() -driver.quit() -``` - -As Selenium catches up, the methods can be seemlessly removed from the Appium -client. Any methods outside of the spec can remain and be used without issue, -should the user choose. diff --git a/docs/webdriver.common.rst b/docs/webdriver.common.rst new file mode 100644 index 00000000..bdc71e98 --- /dev/null +++ b/docs/webdriver.common.rst @@ -0,0 +1,21 @@ +webdriver.common package +======================== + +Submodules +---------- + +webdriver.common.appiumby module +-------------------------------- + +.. automodule:: webdriver.common.appiumby + :members: + :show-inheritance: + :undoc-members: + +Module contents +--------------- + +.. automodule:: webdriver.common + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/webdriver.extensions.android.rst b/docs/webdriver.extensions.android.rst new file mode 100644 index 00000000..a1a54bfa --- /dev/null +++ b/docs/webdriver.extensions.android.rst @@ -0,0 +1,93 @@ +webdriver.extensions.android package +==================================== + +Submodules +---------- + +webdriver.extensions.android.activities module +---------------------------------------------- + +.. automodule:: webdriver.extensions.android.activities + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.android.common module +------------------------------------------ + +.. automodule:: webdriver.extensions.android.common + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.android.display module +------------------------------------------- + +.. automodule:: webdriver.extensions.android.display + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.android.gsm module +--------------------------------------- + +.. automodule:: webdriver.extensions.android.gsm + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.android.nativekey module +--------------------------------------------- + +.. automodule:: webdriver.extensions.android.nativekey + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.android.network module +------------------------------------------- + +.. automodule:: webdriver.extensions.android.network + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.android.performance module +----------------------------------------------- + +.. automodule:: webdriver.extensions.android.performance + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.android.power module +----------------------------------------- + +.. automodule:: webdriver.extensions.android.power + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.android.sms module +--------------------------------------- + +.. automodule:: webdriver.extensions.android.sms + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.android.system\_bars module +------------------------------------------------ + +.. automodule:: webdriver.extensions.android.system_bars + :members: + :show-inheritance: + :undoc-members: + +Module contents +--------------- + +.. automodule:: webdriver.extensions.android + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/webdriver.extensions.flutter_integration.rst b/docs/webdriver.extensions.flutter_integration.rst new file mode 100644 index 00000000..a7bb41df --- /dev/null +++ b/docs/webdriver.extensions.flutter_integration.rst @@ -0,0 +1,37 @@ +webdriver.extensions.flutter\_integration package +================================================= + +Submodules +---------- + +webdriver.extensions.flutter\_integration.flutter\_commands module +------------------------------------------------------------------ + +.. automodule:: webdriver.extensions.flutter_integration.flutter_commands + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.flutter\_integration.flutter\_finder module +---------------------------------------------------------------- + +.. automodule:: webdriver.extensions.flutter_integration.flutter_finder + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.flutter\_integration.scroll\_directions module +------------------------------------------------------------------- + +.. automodule:: webdriver.extensions.flutter_integration.scroll_directions + :members: + :show-inheritance: + :undoc-members: + +Module contents +--------------- + +.. automodule:: webdriver.extensions.flutter_integration + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/webdriver.extensions.rst b/docs/webdriver.extensions.rst new file mode 100644 index 00000000..631f09f1 --- /dev/null +++ b/docs/webdriver.extensions.rst @@ -0,0 +1,158 @@ +webdriver.extensions package +============================ + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + webdriver.extensions.android + webdriver.extensions.flutter_integration + +Submodules +---------- + +webdriver.extensions.action\_helpers module +------------------------------------------- + +.. automodule:: webdriver.extensions.action_helpers + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.applications module +---------------------------------------- + +.. automodule:: webdriver.extensions.applications + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.clipboard module +------------------------------------- + +.. automodule:: webdriver.extensions.clipboard + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.context module +----------------------------------- + +.. automodule:: webdriver.extensions.context + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.device\_time module +---------------------------------------- + +.. automodule:: webdriver.extensions.device_time + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.execute\_driver module +------------------------------------------- + +.. automodule:: webdriver.extensions.execute_driver + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.execute\_mobile\_command module +---------------------------------------------------- + +.. automodule:: webdriver.extensions.execute_mobile_command + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.hw\_actions module +--------------------------------------- + +.. automodule:: webdriver.extensions.hw_actions + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.images\_comparison module +---------------------------------------------- + +.. automodule:: webdriver.extensions.images_comparison + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.keyboard module +------------------------------------ + +.. automodule:: webdriver.extensions.keyboard + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.location module +------------------------------------ + +.. automodule:: webdriver.extensions.location + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.log\_event module +-------------------------------------- + +.. automodule:: webdriver.extensions.log_event + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.logs module +-------------------------------- + +.. automodule:: webdriver.extensions.logs + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.remote\_fs module +-------------------------------------- + +.. automodule:: webdriver.extensions.remote_fs + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.screen\_record module +------------------------------------------ + +.. automodule:: webdriver.extensions.screen_record + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.session module +----------------------------------- + +.. automodule:: webdriver.extensions.session + :members: + :show-inheritance: + :undoc-members: + +webdriver.extensions.settings module +------------------------------------ + +.. automodule:: webdriver.extensions.settings + :members: + :show-inheritance: + :undoc-members: + +Module contents +--------------- + +.. automodule:: webdriver.extensions + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/webdriver.rst b/docs/webdriver.rst new file mode 100644 index 00000000..f74a7db8 --- /dev/null +++ b/docs/webdriver.rst @@ -0,0 +1,126 @@ +webdriver package +================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + webdriver.common + webdriver.extensions + +Submodules +---------- + +webdriver.appium\_connection module +----------------------------------- + +.. automodule:: webdriver.appium_connection + :members: + :show-inheritance: + :undoc-members: + +webdriver.appium\_service module +-------------------------------- + +.. automodule:: webdriver.appium_service + :members: + :show-inheritance: + :undoc-members: + +webdriver.applicationstate module +--------------------------------- + +.. automodule:: webdriver.applicationstate + :members: + :show-inheritance: + :undoc-members: + +webdriver.client\_config module +------------------------------- + +.. automodule:: webdriver.client_config + :members: + :show-inheritance: + :undoc-members: + +webdriver.clipboard\_content\_type module +----------------------------------------- + +.. automodule:: webdriver.clipboard_content_type + :members: + :show-inheritance: + :undoc-members: + +webdriver.command\_method module +-------------------------------- + +.. automodule:: webdriver.command_method + :members: + :show-inheritance: + :undoc-members: + +webdriver.connectiontype module +------------------------------- + +.. automodule:: webdriver.connectiontype + :members: + :show-inheritance: + :undoc-members: + +webdriver.errorhandler module +----------------------------- + +.. automodule:: webdriver.errorhandler + :members: + :show-inheritance: + :undoc-members: + +webdriver.locator\_converter module +----------------------------------- + +.. automodule:: webdriver.locator_converter + :members: + :show-inheritance: + :undoc-members: + +webdriver.mobilecommand module +------------------------------ + +.. automodule:: webdriver.mobilecommand + :members: + :show-inheritance: + :undoc-members: + +webdriver.switch\_to module +--------------------------- + +.. automodule:: webdriver.switch_to + :members: + :show-inheritance: + :undoc-members: + +webdriver.webdriver module +-------------------------- + +.. automodule:: webdriver.webdriver + :members: + :show-inheritance: + :undoc-members: + +webdriver.webelement module +--------------------------- + +.. automodule:: webdriver.webelement + :members: + :show-inheritance: + :undoc-members: + +Module contents +--------------- + +.. automodule:: webdriver + :members: + :show-inheritance: + :undoc-members: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..2e3bf91e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,150 @@ +[project] +name = "Appium-Python-Client" +description = "Python client for Appium" +version = "5.2.6" +readme = "README.md" +license = "Apache-2.0" +license-files = ["LICENSE"] +authors = [ + {name = "Kazuaki Matsuo", email = "kazucocoa1117@gmail.com"}, + {name = "Isaac Murchie"}, +] +maintainers = [ + {name = "Kazuaki Matsuo"}, + {name = "Mykola Mokhnach"}, + {name = "Mori Atsushi"}, +] +keywords = ["appium", "selenium", "python client", "mobile automation"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "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.14", + "Environment :: Console", + "Topic :: Software Development :: Testing", +] +requires-python = ">=3.9" +dependencies = [ + "selenium>=4.26,<5.0", + "typing-extensions~=4.13", +] + +[project.urls] +Homepage = "http://appium.io/" +Repository = "https://github.com/appium/python-client" +Issues = "https://github.com/appium/python-client/issues" +Changelog = "https://github.com/appium/python-client/blob/master/CHANGELOG.md" + +[dependency-groups] +dev = [ + "httpretty~=1.1", + "mock~=5.2", + "mypy~=1.17", + "pre-commit~=4.2", + "pytest~=8.4", + "pytest-cov>=6.2,<8.0", + "pytest-xdist~=3.8", + "python-dateutil~=2.9", + "ruff~=0.12", + "types-python-dateutil~=2.9", + + # for release + "python-semantic-release>=10.3.1,<10.6.0", + + # for documentation + "sphinx>=4.0,<9.0", + "sphinx_rtd_theme~=3.0", + "sphinxcontrib-apidoc~=0.6", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.version] +source = "regex" +path = "appium/version.py" +pattern = "(?P\\d+\\.\\d+\\.\\d+)" + +[tool.hatch.build] +exclude = [ + "test/", + "script/", + "release.sh" +] + +[tool.hatch.build.targets.wheel] +packages = ["appium"] + +[tool.semantic_release] +assets = [] +build_command = """ + uv lock --upgrade-package "$PACKAGE_NAME" + git add uv.lock + uv build +""" +build_command_env = [] +commit_message = "{version}\n\nAutomatically generated by python-semantic-release" +commit_parser = "conventional" +logging_use_named_masks = false +major_on_zero = true +allow_zero_version = false +no_git_verify = false +tag_format = "v{version}" +version_toml = ["pyproject.toml:project.version"] + +[tool.semantic_release.branches.main] +match = "(main|master)" +prerelease_token = "rc" +prerelease = false + +[tool.semantic_release.changelog] +exclude_commit_patterns = [] +mode = "update" +insertion_flag = "" +template_dir = "templates" + +[tool.semantic_release.changelog.default_templates] +changelog_file = "CHANGELOG.md" +output_format = "md" +mask_initial_release = true + +[tool.semantic_release.changelog.environment] +block_start_string = "{%" +block_end_string = "%}" +variable_start_string = "{{" +variable_end_string = "}}" +comment_start_string = "{#" +comment_end_string = "#}" +trim_blocks = false +lstrip_blocks = false +newline_sequence = "\n" +keep_trailing_newline = false +extensions = [] +autoescape = false + +[tool.semantic_release.commit_author] +env = "GIT_COMMIT_AUTHOR" +default = "semantic-release " + +[tool.semantic_release.commit_parser_options] +minor_tags = ["feat"] +patch_tags = ["fix", "perf"] +other_allowed_tags = ["build", "chore", "ci", "docs", "style", "refactor", "test"] +allowed_tags = ["feat", "fix", "perf", "build", "chore", "ci", "docs", "style", "refactor", "test"] +default_bump_level = 0 +parse_squash_commits = true +ignore_merge_commits = true + +[tool.semantic_release.remote] +name = "origin" +type = "github" +ignore_token_for_push = false +insecure = false + +[tool.semantic_release.publish] +dist_glob_patterns = ["dist/*"] +upload_to_vcs_release = true diff --git a/readme_gen.py b/readme_gen.py deleted file mode 100644 index dd75b1e3..00000000 --- a/readme_gen.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python - -# PyPi expects a reStructuredText (http://docutils.sourceforge.net/rst.html) -# document for its readme. This takes the Github one and makes the requisite -# file. Run when README.md is changed and those changes should be reflected -# on PyPi. - -import pandoc -import os - -pandoc.core.PANDOC_PATH = os.environ['PANDOC_HOME'] - -doc = pandoc.Document() -doc.markdown = open('README.md').read() -f = open('README.txt', 'w+') -f.write(doc.rst) -f.close() diff --git a/script/__init__.py b/script/__init__.py new file mode 100644 index 00000000..cc173e9d --- /dev/null +++ b/script/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/setup.py b/setup.py index 98306823..f32eab92 100644 --- a/setup.py +++ b/setup.py @@ -12,43 +12,34 @@ # See the License for the specific language governing permissions and # limitations under the License. -from distutils.core import setup -from setuptools import setup +# FIXME: Remove this setup.py completely. +# Then, we should bump the major version since the package will not include setup.py. + +try: + # Python 3.11+ + import tomllib +except Exception: + # for older versions + import tomli as tomllib + +with open('pyproject.toml', 'rb') as f: + pyproject = tomllib.load(f) + project = pyproject['project'] + +from setuptools import find_packages, setup setup( - name='Appium-Python-Client', - version='0.9', - description='Python client for Appium 1.0', - keywords=[ - 'appium', - 'appium 1.0', - 'selenium', - 'selenium 3', - 'python client', - 'mobile automation' - ], - author='Isaac Murchie', - author_email='isaac@saucelabs.com', - url='http://appium.io/', - packages=[ - 'appium', - 'appium.common', - 'appium.webdriver', - 'appium.webdriver.common' - ], - license='Apache 2.0', - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Programming Language :: Python', - 'Environment :: Console', - 'Environment :: MacOS X', - 'Environment :: Win32 (MS Windows)', - 'Intended Audience :: Developers', - 'Intended Audience :: Other Audience', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Topic :: Software Development :: Quality Assurance', - 'Topic :: Software Development :: Testing' - ], - install_requires=['selenium>=2.41.0', 'enum34'] + name=project['name'], + version=project['version'], + description=project['description'], + keywords=project['keywords'], + author=project['authors'][0]['name'], + author_email=project['authors'][0]['email'], + maintainer=', '.join([maintainer['name'] for maintainer in project['maintainers']]), + url=project['urls']['Homepage'], + package_data={'appium': ['py.typed']}, + packages=find_packages(include=['appium*']), + license=project['license'], + classifiers=project['classifiers'], + install_requires=project['dependencies'], ) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..cc173e9d --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/test/apps/ApiDemos-debug.apk b/test/apps/ApiDemos-debug.apk deleted file mode 100644 index b7a0f57d..00000000 Binary files a/test/apps/ApiDemos-debug.apk and /dev/null differ diff --git a/test/apps/TestApp.app.zip b/test/apps/TestApp.app.zip deleted file mode 100644 index 19668e47..00000000 Binary files a/test/apps/TestApp.app.zip and /dev/null differ diff --git a/test/apps/UICatalog.app.zip b/test/apps/UICatalog.app.zip deleted file mode 100644 index 4fc5756c..00000000 Binary files a/test/apps/UICatalog.app.zip and /dev/null differ diff --git a/test/apps/selendroid-test-app.apk b/test/apps/selendroid-test-app.apk deleted file mode 100644 index 8c112426..00000000 Binary files a/test/apps/selendroid-test-app.apk and /dev/null differ diff --git a/test/functional/__init__.py b/test/functional/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/functional/android/__init__.py b/test/functional/android/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/functional/android/appium_service_tests.py b/test/functional/android/appium_service_tests.py new file mode 100644 index 00000000..409b8098 --- /dev/null +++ b/test/functional/android/appium_service_tests.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# 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. + +from typing import Generator + +import pytest + +from appium.webdriver.appium_service import AppiumService + + +@pytest.fixture +def appium_service() -> Generator[AppiumService, None, None]: + """Create and configure Appium service for testing.""" + service = AppiumService() + service.start( + args=[ + '--address', + '127.0.0.1', + '-p', + '4773', + '--base-path', + '/wd/hub', + ] + ) + + yield service + + service.stop() + + +def test_appium_service(appium_service: AppiumService) -> None: + """Test that Appium service is running and listening.""" + assert appium_service.is_running + assert appium_service.is_listening diff --git a/test/functional/android/appium_tests.py b/test/functional/android/appium_tests.py deleted file mode 100644 index de1713c6..00000000 --- a/test/functional/android/appium_tests.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# 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. - -import unittest -from zipfile import ZipFile -import json -import os -import random -from time import sleep - -from selenium.common.exceptions import NoSuchElementException - -from appium import webdriver -import desired_capabilities - - -# the emulator is sometimes slow and needs time to think -SLEEPY_TIME = 1 - - -class AppiumTests(unittest.TestCase): - def setUp(self): - desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def tearDown(self): - self.driver.quit() - - # remove zipped file from `test_pull_folder` - if os.path.isfile(self.zipfilename): - os.remove(self.zipfilename) - - def test_app_strings(self): - strings = self.driver.app_strings() - self.assertEqual(u'You can\'t wipe my data, you are a monkey!', strings[u'monkey_wipe_data']) - - def test_app_strings_with_language(self): - strings = self.driver.app_strings("en") - self.assertEqual(u'You can\'t wipe my data, you are a monkey!', strings[u'monkey_wipe_data']) - - def test_press_keycode(self): - # not sure how to test this. - self.driver.press_keycode(176) - - def test_long_press_keycode(self): - # not sure how to test this. - self.driver.long_press_keycode(176) - - def test_current_activity(self): - activity = self.driver.current_activity - self.assertEqual('.ApiDemos', activity) - - def test_pull_file(self): - data = self.driver.pull_file('data/local/tmp/strings.json') - strings = json.loads(data.decode('base64', 'strict')) - self.assertEqual('You can\'t wipe my data, you are a monkey!', strings[u'monkey_wipe_data']) - - def test_push_file(self): - path = 'data/local/tmp/test_push_file.txt' - data = 'This is the contents of the file to push to the device.' - self.driver.push_file(path, data.encode('base64')) - data_ret = self.driver.pull_file('data/local/tmp/test_push_file.txt').decode('base64') - self.assertEqual(data, data_ret) - - def test_pull_folder(self): - string_data = 'random string data %d' % random.randint(0, 1000) - path = '/data/local/tmp' - self.driver.push_file(path + '/1.txt', string_data.encode('base64')) - self.driver.push_file(path + '/2.txt', string_data.encode('base64')) - folder = self.driver.pull_folder(path) - - # python doesn't have any functionality for unzipping streams - # save temporary file, which will be deleted in `tearDown` - self.zipfilename = 'folder_%d.zip' % random.randint(0, 1000000) - file = open(self.zipfilename, "w") - file.write(folder.decode('base64', 'strict')) - file.close() - - with ZipFile(self.zipfilename, 'r') as myzip: - # should find these. otherwise it will raise a `KeyError` - myzip.read('1.txt') - myzip.read('2.txt') - - def test_complex_find(self): - # this only works with a three dimensional array like here. - el = self.driver.complex_find([[[2, 'Ani']]]) - self.assertIsNotNone(el) - - def test_background_app(self): - self.driver.background_app(1) - sleep(5) - el = self.driver.find_element_by_name('Animation') - self.assertIsNotNone(el) - - def test_is_app_installed(self): - self.assertFalse(self.driver.is_app_installed('sdfsdf')) - self.assertTrue(self.driver.is_app_installed('com.example.android.apis')) - - def test_install_app(self): - self.skipTest('This causes the server to crash. no idea why') - self.assertFalse(self.driver.is_app_installed('io.selendroid.testapp')) - self.driver.install_app('/Users/isaac/code/python-client/test/apps/selendroid-test-app.apk') - self.assertTrue(self.driver.is_app_installed('io.selendroid.testapp')) - - def test_remove_app(self): - self.assertTrue(self.driver.is_app_installed('com.example.android.apis')) - self.driver.remove_app('com.example.android.apis') - self.assertFalse(self.driver.is_app_installed('com.example.android.apis')) - - def test_close__and_launch_app(self): - el = self.driver.find_element_by_name('Animation') - self.assertIsNotNone(el) - - self.driver.close_app() - self.driver.launch_app() - - el = self.driver.find_element_by_name('Animation') - self.assertIsNotNone(el) - - def test_end_test_coverage(self): - self.skipTest('Not sure how to set this up to run') - self.driver.end_test_coverage(intent='android.intent.action.MAIN', path='') - sleep(5) - - def test_reset(self): - el = self.driver.find_element_by_name('App') - el.click() - - self.driver.reset() - sleep(5) - - el = self.driver.find_element_by_name('App') - self.assertIsNotNone(el) - - def test_open_notifications(self): - self.driver.find_element_by_android_uiautomator('new UiSelector().text("App")').click() - self.driver.find_element_by_android_uiautomator('new UiSelector().text("Notification")').click() - self.driver.find_element_by_android_uiautomator('new UiSelector().text("Status Bar")').click() - - self.driver.find_element_by_android_uiautomator('new UiSelector().text(":-|")').click() - - self.driver.open_notifications() - sleep(1) - self.assertRaises(NoSuchElementException, \ - self.driver.find_element_by_android_uiautomator, 'new UiSelector().text(":-|")') - - els = self.driver.find_elements_by_class_name('android.widget.TextView') - # sometimes numbers shift - title = False - body = False - for el in els: - text = el.text - if text == 'Mood ring': - title = True - elif text == 'I am ok': - body = True - self.assertTrue(title) - self.assertTrue(body) - - self.driver.keyevent(4) - sleep(1) - self.driver.find_element_by_android_uiautomator('new UiSelector().text(":-|")') - - def test_set_text(self): - self.driver.find_element_by_android_uiautomator('new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("Views").instance(0));').click() - self.driver.find_element_by_name('Controls').click() - self.driver.find_element_by_name('1. Light Theme').click() - - el = self.driver.find_element_by_class_name('android.widget.EditText') - el.send_keys('original text') - el.set_text('new text') - - self.assertEqual('new text', el.text) - - def test_send_keys(self): - self.driver.find_element_by_android_uiautomator('new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("Views").instance(0));').click() - self.driver.find_element_by_name('Controls').click() - self.driver.find_element_by_name('1. Light Theme').click() - - el = self.driver.find_element_by_class_name('android.widget.EditText') - el.send_keys('original text') - el.send_keys(' and new text') - - self.assertEqual('original text and new text', el.text) - - -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(AppiumTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/bidi_tests.py b/test/functional/android/bidi_tests.py new file mode 100644 index 00000000..146f29a5 --- /dev/null +++ b/test/functional/android/bidi_tests.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python + +# 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. + +from typing import TYPE_CHECKING, Generator + +import pytest +from selenium.webdriver.common.bidi.common import command_builder + +from appium import webdriver +from appium.webdriver.client_config import AppiumClientConfig +from test.functional.test_helper import is_ci +from test.helpers.constants import SERVER_URL_BASE + +from .options import make_options + +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + + +class AppiumLogEntry: + """Represents a log entry from Appium BiDi.""" + + event_class = 'log.entryAdded' + + def __init__(self, level, text, timestamp, source, type): + self.level = level + self.text = text + self.timestamp = timestamp + self.source = source + self.type = type + + @property + def json(self): + return dict(type=self.type, level=self.level, text=self.text, timestamp=self.timestamp, source=self.source) + + @classmethod + def from_json(cls, json: dict): + return cls( + level=json['level'], + text=json['text'], + timestamp=json['timestamp'], + source=json['source'], + type=json['type'], + ) + + +@pytest.fixture +def driver() -> Generator['WebDriver', None, None]: + """Create and configure Chrome driver with BiDi support for testing.""" + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE) + client_config.timeout = 600 + options = make_options() + options.web_socket_url = True + driver = webdriver.Remote(SERVER_URL_BASE, options=options, client_config=client_config) + + yield driver + + driver.quit() + + +@pytest.mark.skipif(is_ci(), reason='Flaky on CI') +def test_bidi_log(driver: 'WebDriver') -> None: + """Test BiDi logging functionality with Chrome driver.""" + log_entries = [] + bidi_log_param = {'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP']} + + driver.script.conn.execute(command_builder('session.subscribe', bidi_log_param)) + + def _log(entry: AppiumLogEntry): + # e.g. {'type': 'syslog', 'level': 'info', 'source': {'realm': ''}, 'text': '08-05 13:30:32.617 29677 29709 I appium : channel read: GET /session/d7c38859-8930-4eb0-960a-8f917c9e6a38/source', 'timestamp': 1754368241565} + log_entries.append(entry.json) + + try: + callback_id = driver.script.conn.add_callback(AppiumLogEntry, _log) + driver.page_source + assert len(log_entries) != 0 + driver.script.conn.remove_callback(AppiumLogEntry, callback_id) + finally: + driver.script.conn.execute(command_builder('session.unsubscribe', bidi_log_param)) diff --git a/test/functional/android/chrome_tests.py b/test/functional/android/chrome_tests.py index b0f00da6..52f99439 100644 --- a/test/functional/android/chrome_tests.py +++ b/test/functional/android/chrome_tests.py @@ -12,34 +12,39 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest +from typing import TYPE_CHECKING, Generator -from time import sleep +import pytest from appium import webdriver -import desired_capabilities +from appium.webdriver.client_config import AppiumClientConfig +from appium.webdriver.common.appiumby import AppiumBy +from test.helpers.constants import SERVER_URL_BASE +from .options import make_options -class ChromeTests(unittest.TestCase): - def setUp(self): - desired_caps = { - 'platformName': 'Android', - 'platformVersion': '4.2', - 'deviceName': 'Android Emulator', - 'browserName': 'Chrome' - } - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver - def tearDown(self): - self.driver.quit() - def test_find_single_element(self): - self.driver.get('http://10.0.2.2:4723/test/guinea-pig') - self.driver.find_element_by_link_text('i am a link').click() +@pytest.fixture +def driver() -> Generator['WebDriver', None, None]: + """Create and configure Chrome driver for testing.""" + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE) + client_config.timeout = 600 + options = make_options() + options.browser_name = 'Chrome' + driver = webdriver.Remote(SERVER_URL_BASE, options=options, client_config=client_config) - self.assertTrue('I am some other page content' in self.driver.page_source) + yield driver + driver.quit() -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(ChromeTests) - unittest.TextTestRunner(verbosity=2).run(suite) + +def test_find_single_element(driver: 'WebDriver') -> None: + """Test finding a single element in Chrome browser.""" + e = driver.find_element(by=AppiumBy.XPATH, value='//body') + assert e.text == '' + + # Chrome browser's default page + assert '' in driver.page_source diff --git a/test/functional/android/context_switching_tests.py b/test/functional/android/context_switching_tests.py deleted file mode 100644 index 234404db..00000000 --- a/test/functional/android/context_switching_tests.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python - -# 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. - -import unittest - -from appium import webdriver -from appium.common.exceptions import NoSuchContextException -import desired_capabilities - - -class ContextSwitchingTests(unittest.TestCase): - def setUp(self): - desired_caps = desired_capabilities.get_desired_capabilities('selendroid-test-app.apk') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def test_contexts_list(self): - self._enter_webview() - contexts = self.driver.contexts - self.assertEqual(2, len(contexts)) - - def test_move_to_correct_context(self): - self._enter_webview() - self.assertEqual('WEBVIEW_io.selendroid.testapp', self.driver.current_context) - - def test_actually_in_webview(self): - self._enter_webview() - self.driver.find_element_by_css_selector('input[type=submit]').click() - el = self.driver.find_element_by_xpath("//h1[contains(., 'This is my way')]") - self.assertIsNot(None, el) - - def test_move_back_to_native_context(self): - self._enter_webview() - self.driver.switch_to.context(None) - self.assertEqual('NATIVE_APP', self.driver.current_context) - - def test_set_invalid_context(self): - self.assertRaises(NoSuchContextException, self.driver.switch_to.context, 'invalid name') - - def tearDown(self): - self.driver.quit() - - def _enter_webview(self): - btn = self.driver.find_element_by_name('buttonStartWebviewCD') - btn.click() - self.driver.switch_to.context('WEBVIEW') - - -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(ContextSwitchingTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/find_by_accessibility_id_tests.py b/test/functional/android/find_by_accessibility_id_tests.py deleted file mode 100644 index 9b478caf..00000000 --- a/test/functional/android/find_by_accessibility_id_tests.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python - -# 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. - -import unittest - -from appium import webdriver -import desired_capabilities - - -class FindByAccessibilityIDTests(unittest.TestCase): - def setUp(self): - desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def tearDown(self): - self.driver.quit() - - def test_find_single_element(self): - el = self.driver.find_element_by_accessibility_id('Animation') - self.assertIsNotNone(el) - - def test_find_multiple_elements(self): - els = self.driver.find_elements_by_accessibility_id('Animation') - self.assertIsInstance(els, list) - - def test_element_find_single_element(self): - el = self.driver.find_element_by_class_name('android.widget.ListView') - - sub_el = el.find_element_by_accessibility_id('Animation') - self.assertIsNotNone(sub_el) - - def test_element_find_multiple_elements(self): - el = self.driver.find_element_by_class_name('android.widget.ListView') - - sub_els = el.find_elements_by_accessibility_id('Animation') - self.assertIsInstance(sub_els, list) - - -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(FindByAccessibilityIDTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/find_by_uiautomator_tests.py b/test/functional/android/find_by_uiautomator_tests.py deleted file mode 100644 index a96049a7..00000000 --- a/test/functional/android/find_by_uiautomator_tests.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python - -# 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. - -import unittest - -from appium import webdriver -import desired_capabilities - - -class FindByUIAutomatorTests(unittest.TestCase): - def setUp(self): - desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def tearDown(self): - self.driver.quit() - - def test_find_single_element(self): - el = self.driver.find_element_by_android_uiautomator('new UiSelector().text("Animation")') - self.assertIsNotNone(el) - - def test_find_multiple_elements(self): - els = self.driver.find_elements_by_android_uiautomator('new UiSelector().clickable(true)') - self.assertIsInstance(els, list) - - def test_element_find_single_element(self): - el = self.driver.find_element_by_class_name('android.widget.ListView') - - sub_el = el.find_element_by_android_uiautomator('new UiSelector().description("Animation")') - self.assertIsNotNone(sub_el) - - def test_element_find_multiple_elements(self): - el = self.driver.find_element_by_class_name('android.widget.ListView') - - sub_els = el.find_elements_by_android_uiautomator('new UiSelector().clickable(true)') - self.assertIsInstance(sub_els, list) - - def test_scroll_into_view(self): - el = self.driver.find_element_by_android_uiautomator('new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("Views").instance(0));') - el.click() - - -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(FindByUIAutomatorTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/ime_tests.py b/test/functional/android/ime_tests.py deleted file mode 100644 index 76457e06..00000000 --- a/test/functional/android/ime_tests.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# 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. - -import unittest -from time import sleep - -from selenium.common.exceptions import NoSuchElementException - -from appium import webdriver -import desired_capabilities - - -# the emulator is sometimes slow and needs time to think -SLEEPY_TIME = 1 - -LATIN_IME = u'com.android.inputmethod.latin/.LatinIME' - - -class IMETests(unittest.TestCase): - def setUp(self): - desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def tearDown(self): - self.driver.quit() - - - def test_available_ime_engines(self): - engines = self.driver.available_ime_engines - self.assertIsInstance(engines, list) - self.assertTrue(LATIN_IME in engines) - - def test_is_ime_active(self): - self.assertTrue(self.driver.is_ime_active()) - - def test_active_ime_engine(self): - self.assertIsInstance(self.driver.active_ime_engine, unicode) - - def test_activate_ime_engine(self): - engines = self.driver.available_ime_engines - active_engine = self.driver.active_ime_engine - - self.driver.activate_ime_engine(engines[-1]) - self.assertEqual(self.driver.active_ime_engine, engines[-1]) - - def test_deactivate_ime_engine(self): - engines = self.driver.available_ime_engines - self.driver.activate_ime_engine(engines[-1]) - - self.assertEqual(self.driver.active_ime_engine, engines[-1]) - - self.driver.deactivate_ime_engine() - sleep(1) - self.assertNotEqual(self.driver.active_ime_engine, engines[-1]) - - -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(IMETests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/multi_action_tests.py b/test/functional/android/multi_action_tests.py deleted file mode 100644 index 28752341..00000000 --- a/test/functional/android/multi_action_tests.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python - -# 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. - -import unittest -from time import sleep - -from appium import webdriver -from appium.webdriver.common.touch_action import TouchAction -from appium.webdriver.common.multi_action import MultiAction -import desired_capabilities - -# the emulator is sometimes slow and needs time to think -SLEEPY_TIME = 1 - - -class MultiActionTests(unittest.TestCase): - def setUp(self): - desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def tearDown(self): - self.driver.quit() - - def test_parallel_actions(self): - el1 = self.driver.find_element_by_name('Content') - el2 = self.driver.find_element_by_name('Animation') - self.driver.scroll(el1, el2) - - el = self.driver.find_element_by_name('Views') - action = TouchAction(self.driver) - action.tap(el).perform() - - el = self.driver.find_element_by_name('Expandable Lists') - # simulate a swipe/scroll - action.press(el).move_to(x=100, y=-1000).release().perform() - - el = self.driver.find_element_by_name('Splitting Touches across Views') - action.tap(el).perform() - - els = self.driver.find_elements_by_class_name('android.widget.ListView') - a1 = TouchAction() - a1.press(els[0]) \ - .move_to(x=10, y=0).move_to(x=10, y=-75).move_to(x=10, y=-600).release() - - a2 = TouchAction() - a2.press(els[1]) \ - .move_to(x=10, y=10).move_to(x=10, y=-300).move_to(x=10, y=-600).release() - - ma = MultiAction(self.driver, els[0]) - ma.add(a1, a2) - ma.perform() - - def test_actions_with_waits(self): - el1 = self.driver.find_element_by_name('Content') - el2 = self.driver.find_element_by_name('Animation') - self.driver.scroll(el1, el2) - - el = self.driver.find_element_by_name('Views') - action = TouchAction(self.driver) - action.tap(el).perform() - - el = self.driver.find_element_by_name('Expandable Lists') - # simulate a swipe/scroll - action.press(el).move_to(x=100, y=-1000).release().perform() - - el = self.driver.find_element_by_name('Splitting Touches across Views') - action.tap(el).perform() - - els = self.driver.find_elements_by_class_name('android.widget.ListView') - a1 = TouchAction() - a1.press(els[0]) \ - .move_to(x=10, y=0) \ - .move_to(x=10, y=-75) \ - .wait(1000) \ - .move_to(x=10, y=-600) \ - .release() - - a2 = TouchAction() - a2.press(els[1]) \ - .move_to(x=10, y=10) \ - .move_to(x=10, y=-300) \ - .wait(500) \ - .move_to(x=10, y=-600) \ - .release() - - ma = MultiAction(self.driver, els[0]) - ma.add(a1, a2) - ma.perform() - - def test_driver_multi_tap(self): - el = self.driver.find_element_by_name('Graphics') - action = TouchAction(self.driver) - action.tap(el).perform() - - els = self.driver.find_elements_by_class_name('android.widget.TextView') - self.driver.scroll(els[len(els) - 1], els[0]) - - els = self.driver.find_elements_by_class_name('android.widget.TextView') - if els[len(els) - 1].get_attribute('name') != 'Xfermodes': - self.driver.scroll(els[len(els) - 1], els[0]) - - el = self.driver.find_element_by_name('Touch Paint') - action.tap(el).perform() - - positions = [(100, 200), (100, 400)] - - # makes two dots in the paint program - # THE TEST MUST BE WATCHED TO CHECK IF IT WORKS - self.driver.tap(positions) - sleep(10) - - def test_driver_pinch(self): - el1 = self.driver.find_element_by_name('Content') - el2 = self.driver.find_element_by_name('Animation') - self.driver.scroll(el1, el2) - - el = self.driver.find_element_by_name('Views') - action = TouchAction(self.driver) - action.tap(el).perform() - - els = self.driver.find_elements_by_class_name('android.widget.TextView') - self.driver.scroll(els[len(els) - 1], els[0]) - - els = self.driver.find_elements_by_class_name('android.widget.TextView') - if els[len(els) - 1].get_attribute('name') != 'WebView': - self.driver.scroll(els[len(els) - 1], els[0]) - - el = self.driver.find_element_by_name('WebView') - action.tap(el).perform() - - sleep(SLEEPY_TIME) - el = self.driver.find_element_by_id('com.example.android.apis:id/wv1') - self.driver.pinch(element=el) - - def test_driver_zoom(self): - el1 = self.driver.find_element_by_name('Content') - el2 = self.driver.find_element_by_name('Animation') - self.driver.scroll(el1, el2) - - el = self.driver.find_element_by_name('Views') - action = TouchAction(self.driver) - action.tap(el).perform() - - els = self.driver.find_elements_by_class_name('android.widget.TextView') - self.driver.scroll(els[len(els) - 1], els[0]) - - els = self.driver.find_elements_by_class_name('android.widget.TextView') - if els[len(els) - 1].get_attribute('name') != 'WebView': - self.driver.scroll(els[len(els) - 1], els[0]) - - el = self.driver.find_element_by_name('WebView') - action.tap(el).perform() - - sleep(SLEEPY_TIME) - el = self.driver.find_element_by_id('com.example.android.apis:id/wv1') - self.driver.zoom(element=el) - - -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(MultiActionTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/network_connection_tests.py b/test/functional/android/network_connection_tests.py deleted file mode 100644 index ea993f2a..00000000 --- a/test/functional/android/network_connection_tests.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# 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. - -import unittest - - -from appium import webdriver -from appium.webdriver.connectiontype import ConnectionType -import desired_capabilities - - -# the emulator is sometimes slow and needs time to think -SLEEPY_TIME = 1 - - -class NetworkConnectionTests(unittest.TestCase): - def setUp(self): - desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def tearDown(self): - self.driver.quit() - - - def test_get_network_connection(self): - nc = self.driver.network_connection - self.assertIsInstance(nc, int) - - def test_set_network_connection(self): - nc = self.driver.set_network_connection(ConnectionType.DATA_ONLY) - self.assertIsInstance(nc, int) - self.assertEqual(nc, ConnectionType.DATA_ONLY.value) - - -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(NetworkConnectionTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/options.py b/test/functional/android/options.py new file mode 100644 index 00000000..9c8103a7 --- /dev/null +++ b/test/functional/android/options.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +# 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. + +import os +from typing import Optional + +from appium.options.android import UiAutomator2Options +from test.functional.test_helper import get_worker_info + + +def make_options(app: Optional[str] = None) -> UiAutomator2Options: + """Get UiAutomator2 options configured for Android testing with parallel execution support.""" + options = UiAutomator2Options() + + # Set basic Android capabilities + options.device_name = android_device_name() + options.platform_name = 'Android' + options.automation_name = 'UIAutomator2' + options.new_command_timeout = 240 + options.uiautomator2_server_install_timeout = 120000 + options.adb_exec_timeout = 120000 + + if app is not None: + options.app = app + + return options + + +def android_device_name() -> str: + """ + Get a unique device name for the current worker. + Uses the base device name and appends the port number for uniqueness. + """ + prefix = os.getenv('ANDROID_MODEL') or 'Android Emulator' + worker_info = get_worker_info() + + if worker_info.is_parallel: + # For parallel execution, we can use different device names or ports + # This is a simplified approach - in practice you might want to use different emulators + return f'{prefix} - Worker {worker_info.worker_id}' + + return prefix diff --git a/test/functional/android/selendroid_tests.py b/test/functional/android/selendroid_tests.py deleted file mode 100644 index ff32afb7..00000000 --- a/test/functional/android/selendroid_tests.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python - -# 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. - -import unittest - -from appium import webdriver -from appium.common.exceptions import NoSuchContextException -import desired_capabilities -from time import sleep - -from selenium.webdriver.common.touch_actions import TouchActions - - -class SelendroidTests(unittest.TestCase): - def setUp(self): - desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk') - desired_caps['automationName'] = 'Selendroid' - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def test_contexts_list(self): - el = self.driver.find_element_by_class_name('android.widget.ListView') - els = el.find_elements_by_class_name('android.widget.TextView') - - ta = TouchActions(self.driver).flick_element(el, 0, -300, 0) - ta.perform() - sleep(5) - - def tearDown(self): - self.driver.quit() - - def _enter_webview(self): - btn = self.driver.find_element_by_name('buttonStartWebviewCD') - btn.click() - self.driver.switch_to.context('WEBVIEW') - - -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(SelendroidTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/android/touch_action_tests.py b/test/functional/android/touch_action_tests.py deleted file mode 100644 index 3493c6b1..00000000 --- a/test/functional/android/touch_action_tests.py +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env python - -# 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. - -import unittest -from time import sleep - -from selenium.common.exceptions import NoSuchElementException - -from appium import webdriver -from appium.webdriver.common.touch_action import TouchAction -import desired_capabilities - -# the emulator is sometimes slow -SLEEPY_TIME = 2 - - -class TouchActionTests(unittest.TestCase): - def setUp(self): - desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def tearDown(self): - self.driver.quit() - - def test_tap(self): - el = self.driver.find_element_by_accessibility_id('Animation') - action = TouchAction(self.driver) - action.tap(el).perform() - el = self.driver.find_element_by_accessibility_id('Bouncing Balls') - self.assertIsNotNone(el) - - def test_tap_x_y(self): - el = self.driver.find_element_by_accessibility_id('Animation') - action = TouchAction(self.driver) - action.tap(el, 100, 10).perform() - - sleep(SLEEPY_TIME) - el = self.driver.find_element_by_accessibility_id('Bouncing Balls') - self.assertIsNotNone(el) - - def test_tap_twice(self): - el = self.driver.find_element_by_name('Text') - action = TouchAction(self.driver) - action.tap(el).perform() - sleep(SLEEPY_TIME) - - el = self.driver.find_element_by_name('LogTextBox') - action.tap(el).perform() - - el = self.driver.find_element_by_name('Add') - action.tap(el, count=2).perform() - - els = self.driver.find_elements_by_class_name('android.widget.TextView') - self.assertEqual('This is a test\nThis is a test\n', els[1].get_attribute("text")) - - def test_press_and_immediately_release(self): - el = self.driver.find_element_by_accessibility_id('Animation') - action = TouchAction(self.driver) - action.press(el).release().perform() - - sleep(SLEEPY_TIME) - el = self.driver.find_element_by_accessibility_id('Bouncing Balls') - self.assertIsNotNone(el) - - def test_press_and_immediately_release_x_y(self): - el = self.driver.find_element_by_accessibility_id('Animation') - action = TouchAction(self.driver) - action.press(el, 100, 10).release().perform() - - sleep(SLEEPY_TIME) - el = self.driver.find_element_by_accessibility_id('Bouncing Balls') - self.assertIsNotNone(el) - - def test_press_and_wait(self): - el1 = self.driver.find_element_by_name('Content') - el2 = self.driver.find_element_by_accessibility_id('Animation') - - action = TouchAction(self.driver) - action.press(el1).move_to(el2).perform() - - sleep(SLEEPY_TIME) - el = self.driver.find_element_by_accessibility_id('Views') - # self.assertIsNotNone(el) - action.tap(el).perform() - - sleep(SLEEPY_TIME) - el = self.driver.find_element_by_accessibility_id('Expandable Lists') - # self.assertIsNotNone(el) - action.tap(el).perform() - - sleep(SLEEPY_TIME) - el = self.driver.find_element_by_accessibility_id('1. Custom Adapter') - # self.assertIsNotNone(el) - action.tap(el).perform() - - sleep(SLEEPY_TIME) - el = self.driver.find_element_by_name('People Names') - # self.assertIsNotNone(el) - action.press(el).wait(2000).perform() - - sleep(SLEEPY_TIME) - # 'Sample menu' only comes up with a long press, not a press - el = self.driver.find_element_by_name('Sample menu') - self.assertIsNotNone(el) - - def test_press_and_moveto(self): - el1 = self.driver.find_element_by_accessibility_id('Content') - el2 = self.driver.find_element_by_accessibility_id('Animation') - - action = TouchAction(self.driver) - action.press(el1).move_to(el2).release().perform() - - el = self.driver.find_element_by_accessibility_id('Views') - self.assertIsNotNone(el) - - def test_press_and_moveto_x_y(self): - el1 = self.driver.find_element_by_accessibility_id('Content') - el2 = self.driver.find_element_by_accessibility_id('App') - - action = TouchAction(self.driver) - action.press(el1).move_to(el2, 100, 100).release().perform() - - el = self.driver.find_element_by_accessibility_id('Views') - self.assertIsNotNone(el) - - def test_long_press(self): - el1 = self.driver.find_element_by_name('Content') - el2 = self.driver.find_element_by_accessibility_id('Animation') - - action = TouchAction(self.driver) - action.press(el1).move_to(el2).perform() - - el = self.driver.find_element_by_accessibility_id('Views') - # self.assertIsNotNone(el) - action.tap(el).perform() - - el = self.driver.find_element_by_accessibility_id('Expandable Lists') - # self.assertIsNotNone(el) - action.tap(el).perform() - - el = self.driver.find_element_by_accessibility_id('1. Custom Adapter') - # self.assertIsNotNone(el) - action.tap(el).perform() - - el = self.driver.find_element_by_name('People Names') - # self.assertIsNotNone(el) - action.long_press(el).perform() - - # 'Sample menu' only comes up with a long press, not a tap - el = self.driver.find_element_by_name('Sample menu') - self.assertIsNotNone(el) - - def test_long_press_x_y(self): - el1 = self.driver.find_element_by_name('Content') - el2 = self.driver.find_element_by_accessibility_id('Animation') - - action = TouchAction(self.driver) - action.press(el1).move_to(el2).perform() - - el = self.driver.find_element_by_accessibility_id('Views') - # self.assertIsNotNone(el) - action.tap(el).perform() - - el = self.driver.find_element_by_accessibility_id('Expandable Lists') - # self.assertIsNotNone(el) - action.tap(el).perform() - - el = self.driver.find_element_by_accessibility_id('1. Custom Adapter') - # self.assertIsNotNone(el) - action.tap(el).perform() - - # the element "People Names" is located at 0:110 (top left corner) - action.long_press(x=10, y=120).perform() - - # 'Sample menu' only comes up with a long press, not a tap - el = self.driver.find_element_by_name('Sample menu') - self.assertIsNotNone(el) - - def test_drag_and_drop(self): - el1 = self.driver.find_element_by_name('Content') - el2 = self.driver.find_element_by_name('Animation') - self.driver.scroll(el1, el2) - - el = self.driver.find_element_by_name('Views') - action = TouchAction(self.driver) - action.tap(el).perform() - - el = self.driver.find_element_by_name('Drag and Drop') - action.tap(el).perform() - - dd3 = self.driver.find_element_by_id('com.example.android.apis:id/drag_dot_3') - dd2 = self.driver.find_element_by_id('com.example.android.apis:id/drag_dot_2') - - # dnd is stimulated by longpress-move_to-release - action.long_press(dd3).move_to(dd2).release().perform() - - el = self.driver.find_element_by_id('com.example.android.apis:id/drag_result_text') - self.assertEqual('Dropped!', el.get_attribute('text')) - - def test_driver_drag_and_drop(self): - el1 = self.driver.find_element_by_name('Content') - el2 = self.driver.find_element_by_name('Animation') - self.driver.scroll(el1, el2) - - el = self.driver.find_element_by_name('Views') - action = TouchAction(self.driver) - action.tap(el).perform() - - el = self.driver.find_element_by_name('Drag and Drop') - action.tap(el).perform() - - dd3 = self.driver.find_element_by_id('com.example.android.apis:id/drag_dot_3') - dd2 = self.driver.find_element_by_id('com.example.android.apis:id/drag_dot_2') - - self.driver.drag_and_drop(dd3, dd2) - - el = self.driver.find_element_by_id('com.example.android.apis:id/drag_result_text') - self.assertEqual('Dropped!', el.get_attribute('text')) - - def test_driver_swipe(self): - self.assertRaises(NoSuchElementException, self.driver.find_element_by_name, 'Views') - - self.driver.swipe(100, 500, 100, 100, 800) - el = self.driver.find_element_by_name('Views') - self.assertIsNotNone(el) - - -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(TouchActionTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/flutter_integration/__init__.py b/test/functional/flutter_integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/functional/flutter_integration/commands_test.py b/test/functional/flutter_integration/commands_test.py new file mode 100644 index 00000000..7afe4156 --- /dev/null +++ b/test/functional/flutter_integration/commands_test.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python + +# 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. + +import os +from typing import TYPE_CHECKING + +from appium.webdriver.common.appiumby import AppiumBy +from appium.webdriver.extensions.flutter_integration.flutter_finder import FlutterFinder +from appium.webdriver.extensions.flutter_integration.scroll_directions import ScrollDirection + +if TYPE_CHECKING: + from appium.webdriver.extensions.flutter_integration.flutter_commands import FlutterCommand + from appium.webdriver.webdriver import WebDriver + + +def _open_screen(driver: 'WebDriver', flutter_command: 'FlutterCommand', screen_name: str) -> None: + """Helper function to open a specific screen in the Flutter app.""" + driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Login').click() + element = flutter_command.scroll_till_visible(FlutterFinder.by_text(screen_name)) + element.click() + + +def test_wait_command(driver: 'WebDriver', flutter_command: 'FlutterCommand') -> None: + """Test Flutter wait commands for element visibility.""" + _open_screen(driver, flutter_command, 'Lazy Loading') + + message_field_finder = FlutterFinder.by_key('message_field') + toggle_button_finder = FlutterFinder.by_key('toggle_button') + + message_field = driver.find_element(*message_field_finder.as_args()) + toggle_button = driver.find_element(*toggle_button_finder.as_args()) + assert message_field.is_displayed() == True + assert message_field.text == 'Hello world' + + toggle_button.click() + flutter_command.wait_for_invisible(message_field_finder) + assert len(driver.find_elements(*message_field_finder.as_args())) == 0 + + toggle_button.click() + flutter_command.wait_for_visible(message_field) + assert len(driver.find_elements(*message_field_finder.as_args())) == 1 + + +def test_scroll_till_visible_command(driver: 'WebDriver', flutter_command: 'FlutterCommand') -> None: + """Test Flutter scroll till visible command.""" + _open_screen(driver, flutter_command, 'Vertical Swiping') + + java_text_finder = FlutterFinder.by_text('Java') + protractor_text_finder = FlutterFinder.by_text('Protractor') + + first_element = flutter_command.scroll_till_visible(java_text_finder) + assert first_element.get_attribute('displayed') == 'true' + + second_element = flutter_command.scroll_till_visible(protractor_text_finder) + assert second_element.get_attribute('displayed') == 'true' + assert first_element.get_attribute('displayed') == 'false' + + first_element = flutter_command.scroll_till_visible(java_text_finder, ScrollDirection.UP) + assert second_element.get_attribute('displayed') == 'false' + assert first_element.get_attribute('displayed') == 'true' + + +def test_scroll_till_visible_with_scroll_params_command(driver: 'WebDriver', flutter_command: 'FlutterCommand') -> None: + """Test Flutter scroll till visible command with custom scroll parameters.""" + _open_screen(driver, flutter_command, 'Vertical Swiping') + + scroll_params = { + 'scrollView': FlutterFinder.by_type('Scrollable').to_dict(), + 'delta': 30, + 'maxScrolls': 30, + 'settleBetweenScrollsTimeout': 5000, + 'dragDuration': 35, + } + first_element = flutter_command.scroll_till_visible( + FlutterFinder.by_text('Playwright'), scroll_direction=ScrollDirection.DOWN, **scroll_params + ) + assert first_element.get_attribute('displayed') == 'true' + + +def test_double_click_command(driver: 'WebDriver', flutter_command: 'FlutterCommand') -> None: + """Test Flutter double click command.""" + _open_screen(driver, flutter_command, 'Double Tap') + + double_tap_button = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'double_tap_button').find_element( + AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Double Tap' + ) + assert double_tap_button.text == 'Double Tap' + + flutter_command.perform_double_click(double_tap_button) + assert driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, 'Successful').text == 'Double Tap Successful' + + driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Ok').click() + flutter_command.perform_double_click(double_tap_button, (10, 2)) + assert driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, 'Successful').text == 'Double Tap Successful' + + driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Ok').click() + + +def test_long_press_command(driver: 'WebDriver', flutter_command: 'FlutterCommand') -> None: + """Test Flutter long press command.""" + _open_screen(driver, flutter_command, 'Long Press') + + long_press_button = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'long_press_button') + flutter_command.perform_long_press(long_press_button) + + success_pop_up = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'It was a long press') + assert success_pop_up.text == 'It was a long press' + assert success_pop_up.is_displayed() == True + + +def test_drag_and_drop_command(driver: 'WebDriver', flutter_command: 'FlutterCommand') -> None: + """Test Flutter drag and drop command.""" + _open_screen(driver, flutter_command, 'Drag & Drop') + + drag_element = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'drag_me') + drop_element = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'drop_zone') + flutter_command.perform_drag_and_drop(drag_element, drop_element) + assert driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'The box is dropped').is_displayed() == True + + +def test_camera_mocking(driver: 'WebDriver', flutter_command: 'FlutterCommand') -> None: + """Test Flutter camera mocking functionality.""" + _open_screen(driver, flutter_command, 'Image Picker') + + success_qr_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'file', 'success_qr.png') + second_qr_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'file', 'second_qr.png') + + image_id = flutter_command.inject_mock_image(success_qr_file_path) + flutter_command.inject_mock_image(second_qr_file_path) + driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'capture_image').click() + driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'PICK').click() + assert driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'SecondInjectedImage').is_displayed() == True + + flutter_command.activate_injected_image(image_id) + driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'capture_image').click() + driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'PICK').click() + assert driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Success!').is_displayed() == True diff --git a/test/functional/flutter_integration/conftest.py b/test/functional/flutter_integration/conftest.py new file mode 100644 index 00000000..128312e8 --- /dev/null +++ b/test/functional/flutter_integration/conftest.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +# 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. + +from typing import TYPE_CHECKING, Generator + +import pytest + +from appium import webdriver +from appium.webdriver.client_config import AppiumClientConfig +from appium.webdriver.extensions.flutter_integration.flutter_commands import FlutterCommand +from test.helpers.constants import SERVER_URL_BASE + +from .helper.options import make_options + +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + + +@pytest.fixture +def driver() -> Generator['WebDriver', None, None]: + """Create and configure Flutter driver for testing.""" + options = make_options() + + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE) + client_config.timeout = 600 + + driver = webdriver.Remote(options=options, client_config=client_config) + + yield driver + + driver.quit() + + +@pytest.fixture +def flutter_command(driver: 'WebDriver') -> FlutterCommand: + """Create FlutterCommand instance for the driver.""" + return FlutterCommand(driver) diff --git a/test/functional/flutter_integration/file/second_qr.png b/test/functional/flutter_integration/file/second_qr.png new file mode 100644 index 00000000..355548c3 Binary files /dev/null and b/test/functional/flutter_integration/file/second_qr.png differ diff --git a/test/functional/flutter_integration/file/success_qr.png b/test/functional/flutter_integration/file/success_qr.png new file mode 100644 index 00000000..8896d86f Binary files /dev/null and b/test/functional/flutter_integration/file/success_qr.png differ diff --git a/test/functional/flutter_integration/finder_test.py b/test/functional/flutter_integration/finder_test.py new file mode 100644 index 00000000..5357fc6f --- /dev/null +++ b/test/functional/flutter_integration/finder_test.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +# 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. + +from typing import TYPE_CHECKING + +from appium.webdriver.common.appiumby import AppiumBy +from appium.webdriver.extensions.flutter_integration.flutter_finder import FlutterFinder + +if TYPE_CHECKING: + from appium.webdriver.extensions.flutter_integration.flutter_commands import FlutterCommand + from appium.webdriver.webdriver import WebDriver + +LOGIN_BUTTON_FINDER = FlutterFinder.by_text('Login') + + +def test_by_flutter_key(driver: 'WebDriver') -> None: + """Test finding elements by Flutter key.""" + user_name_field_finder = FlutterFinder.by_key('username_text_field') + user_name_field = driver.find_element(*user_name_field_finder.as_args()) + assert user_name_field.text == 'admin' + + user_name_field.clear() + user_name_field = driver.find_element(*user_name_field_finder.as_args()).send_keys('admin123') + assert user_name_field.text == 'admin123' + + +def test_by_flutter_type(driver: 'WebDriver') -> None: + """Test finding elements by Flutter type.""" + login_button = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TYPE, 'ElevatedButton') + assert login_button.find_element(AppiumBy.FLUTTER_INTEGRATION_TYPE, 'Text').text == 'Login' + + +def test_by_flutter_text(driver: 'WebDriver') -> None: + """Test finding elements by Flutter text.""" + login_button = driver.find_element(*LOGIN_BUTTON_FINDER.as_args()) + assert login_button.text == 'Login' + + login_button.click() + slider = driver.find_elements(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Slider') + assert len(slider) == 1 + + +def test_by_flutter_text_containing(driver: 'WebDriver') -> None: + """Test finding elements by Flutter text containing.""" + login_button = driver.find_element(*LOGIN_BUTTON_FINDER.as_args()) + login_button.click() + vertical_swipe_label = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, 'Vertical') + assert vertical_swipe_label.text == 'Vertical Swiping' + + +def test_by_flutter_semantics_label(driver: 'WebDriver', flutter_command: 'FlutterCommand') -> None: + """Test finding elements by Flutter semantics label.""" + login_button = driver.find_element(*LOGIN_BUTTON_FINDER.as_args()) + login_button.click() + element = flutter_command.scroll_till_visible(FlutterFinder.by_text('Lazy Loading')) + element.click() + message_field = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_SEMANTICS_LABEL, 'message_field') + assert message_field.text == 'Hello world' diff --git a/test/functional/flutter_integration/helper/__init__.py b/test/functional/flutter_integration/helper/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/functional/flutter_integration/helper/options.py b/test/functional/flutter_integration/helper/options.py new file mode 100644 index 00000000..0a44f372 --- /dev/null +++ b/test/functional/flutter_integration/helper/options.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +# 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. + +import os +from typing import Any, Dict + +from appium.options.flutter_integration.base import FlutterOptions +from test.functional.test_helper import get_wda_port, get_worker_info + + +def is_platform_android() -> bool: + """Check if the current platform is Android.""" + return os.getenv('PLATFORM', 'android').lower() == 'android' + + +def get_flutter_system_port() -> int: + """ + Get a unique Flutter system port for the current worker. + Uses base port 9999 and increments by worker number. + """ + worker_info = get_worker_info() + return 9999 + (worker_info.worker_number or 0) + + +def make_options() -> FlutterOptions: + """Get Flutter options configured for testing with parallel execution support.""" + options = FlutterOptions() + + # Set Flutter-specific capabilities + options.flutter_system_port = get_flutter_system_port() + options.flutter_enable_mock_camera = True + options.flutter_element_wait_timeout = 10000 + options.flutter_server_launch_timeout = 120000 + + caps: Dict[str, Any] = ( + { + 'platformName': 'Android', + 'deviceName': device_name(), + 'newCommandTimeout': 120, + 'uiautomator2ServerInstallTimeout': 120000, + 'adbExecTimeout': 120000, + 'app': os.environ['FLUTTER_ANDROID_APP'], + 'autoGrantPermissions': True, + } + if is_platform_android() + else { + 'deviceName': device_name(), + 'platformName': 'iOS', + 'platformVersion': os.environ['IOS_VERSION'], + 'allowTouchIdEnroll': True, + 'wdaLaunchTimeout': 240000, + 'wdaLocalPort': get_wda_port(), + 'eventTimings': True, + 'app': os.environ['FLUTTER_IOS_APP'], + } + ) + + if local_prebuilt_wda := os.getenv('LOCAL_PREBUILT_WDA'): + caps.update( + { + 'usePreinstalledWDA': True, + 'prebuiltWDAPath': local_prebuilt_wda, + } + ) + + return options.load_capabilities(caps) + + +def device_name() -> str: + """ + Get a unique device name for the current worker. + Uses the base device name and appends the port number for uniqueness. + """ + if is_platform_android(): + prefix = 'Android Emulator' + else: + prefix = os.environ['IPHONE_MODEL'] + + worker_info = get_worker_info() + + if worker_info.is_parallel: + port = get_flutter_system_port() if is_platform_android() else get_wda_port() + return f'{prefix} - {port}' + + return prefix diff --git a/test/functional/ios/__init__.py b/test/functional/ios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/functional/ios/appium_tests.py b/test/functional/ios/appium_tests.py deleted file mode 100644 index f499c629..00000000 --- a/test/functional/ios/appium_tests.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python - -# 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. - -import unittest -from time import sleep - -from selenium.common.exceptions import NoSuchElementException - -from appium import webdriver -import desired_capabilities - - -class AppiumTests(unittest.TestCase): - def setUp(self): - desired_caps = desired_capabilities.get_desired_capabilities('UICatalog.app.zip') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def tearDown(self): - self.driver.quit() - - def test_lock(self): - el = self.driver.find_element_by_id('ButtonsExplain') - self.assertIsNotNone(el) - self.driver.lock(0) - self.assertRaises(NoSuchElementException, self.driver.find_element_by_id, 'ButtonsExplain') - sleep(10) - - # # this does not seem to ever unlock, so the assertion fails - # el = self.driver.find_element_by_id('ButtonsExplain') - # self.assertIsNotNone(el) - - def test_shake(self): - # what can we assert about this? - self.driver.shake() - - def test_hide_keyboard(self): - el = self.driver.find_element_by_name('TextFields, Uses of UITextField') - el.click() - - # get focus on text field, so keyboard comes up - el = self.driver.find_element_by_class_name('UIATextField') - el.set_value('Testing') - - el = self.driver.find_element_by_class_name('UIAKeyboard') - self.assertTrue(el.is_displayed()) - - self.driver.hide_keyboard(key_name='Done') - - self.assertFalse(el.is_displayed()) - - def test_hide_keyboard_presskey_strategy(self): - el = self.driver.find_element_by_name('TextFields, Uses of UITextField') - el.click() - - # get focus on text field, so keyboard comes up - el = self.driver.find_element_by_class_name('UIATextField') - el.set_value('Testing') - - el = self.driver.find_element_by_class_name('UIAKeyboard') - self.assertTrue(el.is_displayed()) - - self.driver.hide_keyboard(strategy='pressKey', key='Done') - - self.assertFalse(el.is_displayed()) - - def test_hide_keyboard_no_key_name(self): - el = self.driver.find_element_by_name('TextFields, Uses of UITextField') - el.click() - - # get focus on text field, so keyboard comes up - el = self.driver.find_element_by_class_name('UIATextField') - el.set_value('Testing') - - el = self.driver.find_element_by_class_name('UIAKeyboard') - self.assertTrue(el.is_displayed()) - - self.driver.hide_keyboard() - sleep(10) - - # currently fails. - self.assertFalse(el.is_displayed()) - - -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(AppiumTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/ios/find_by_uiautomation_tests.py b/test/functional/ios/find_by_uiautomation_tests.py deleted file mode 100644 index f1b003a3..00000000 --- a/test/functional/ios/find_by_uiautomation_tests.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python - -# 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. - -import unittest - -from appium import webdriver -import desired_capabilities - - -class FindByUIAutomationTests(unittest.TestCase): - def setUp(self): - desired_caps = desired_capabilities.get_desired_capabilities('UICatalog.app.zip') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def tearDown(self): - self.driver.quit() - - def test_find_single_element(self): - el = self.driver.find_element_by_ios_uiautomation('.elements()[0]') - self.assertEqual('UICatalog', el.get_attribute('name')) - - def test_find_multiple_elements(self): - els = self.driver.find_elements_by_ios_uiautomation('elements()') - self.assertEqual(3, len(els)) - - def test_element_find_single_element(self): - # get the list - el = self.driver.find_element_by_ios_uiautomation('.elements()[1]') - - # get the search bar button - sub_el = el.find_element_by_ios_uiautomation('.elements()[3]') - self.assertEqual('SearchBar, Use of UISearchBar', sub_el.get_attribute('name')) - - def test_element_find_multiple_elements(self): - # get the list - el = self.driver.find_element_by_ios_uiautomation('.elements()[1]') - - # get the buttons - sub_el = el.find_elements_by_ios_uiautomation('.elements()') - self.assertEqual(12, len(sub_el)) - - -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(FindByUIAutomationTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/ios/helper/__init__.py b/test/functional/ios/helper/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/functional/ios/helper/options.py b/test/functional/ios/helper/options.py new file mode 100644 index 00000000..a189b741 --- /dev/null +++ b/test/functional/ios/helper/options.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +# 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. + +import os +from typing import Optional + +from appium.options.ios import XCUITestOptions +from test.functional.test_helper import get_wda_port, get_worker_info + + +def make_options(app: Optional[str] = None) -> XCUITestOptions: + """Get XCUITest options configured for iOS testing with parallel execution support.""" + options = XCUITestOptions() + + # Set basic iOS capabilities + options.device_name = iphone_device_name() + options.platform_version = os.getenv('IOS_VERSION') or '17.4' + options.allow_touch_id_enroll = True + options.wda_local_port = get_wda_port() + options.simple_is_visible_check = True + + if app is not None: + options.app = app + + if local_prebuilt_wda := os.getenv('LOCAL_PREBUILT_WDA'): + options.use_preinstalled_wda = True + options.prebuilt_wda_path = local_prebuilt_wda + + return options + + +def iphone_device_name() -> str: + """ + Get a unique device name for the current worker. + Uses the base device name and appends the port number for uniqueness. + """ + prefix = os.getenv('IPHONE_MODEL') or 'iPhone 15 Plus' + worker_info = get_worker_info() + + if worker_info.is_parallel: + port = get_wda_port() + return f'{prefix} - {port}' + + return prefix diff --git a/test/functional/ios/multi_action_tests.py b/test/functional/ios/multi_action_tests.py deleted file mode 100644 index f408c484..00000000 --- a/test/functional/ios/multi_action_tests.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python - -# 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. - -import unittest -from time import sleep - -from appium import webdriver -import desired_capabilities - - -class MultiActionTests(unittest.TestCase): - def setUp(self): - desired_caps = desired_capabilities.get_desired_capabilities('TestApp.app.zip') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def tearDown(self): - self.driver.quit() - - # this test does not assert anything. - # it has to be watched in order to see if it works - def test_driver_pinch_zoom(self): - els = self.driver.find_elements_by_class_name('UIAButton') - els[5].click() - - sleep(1) - el = self.driver.find_element_by_name('OK') - el.click() - - sleep(1) - el = self.driver.find_element_by_xpath('//UIAApplication[1]/UIAWindow[1]/UIAMapView[1]') - self.driver.zoom(el) - - sleep(5) - self.driver.pinch(el) - sleep(5) - - -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(MultiActionTests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/functional/ios/safari_tests.py b/test/functional/ios/safari_tests.py index 5240aaef..624979f8 100644 --- a/test/functional/ios/safari_tests.py +++ b/test/functional/ios/safari_tests.py @@ -1,35 +1,79 @@ #!/usr/bin/env python -import unittest -from time import sleep +# 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. +import time +from typing import TYPE_CHECKING, Generator + +import pytest from appium import webdriver +from appium.webdriver.client_config import AppiumClientConfig +from test.helpers.constants import SERVER_URL_BASE + +from .helper.options import make_options + +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + + +@pytest.fixture +def driver() -> Generator['WebDriver', None, None]: + """Create and configure Safari driver for testing.""" + options = make_options() + options.bundle_id = 'com.apple.mobilesafari' + options.native_web_tap = True + options.safari_ignore_fraud_warning = True + options.webview_connect_timeout = 100000 + + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE) + client_config.timeout = 600 + driver = webdriver.Remote(options=options, client_config=client_config) + + # Fresh iOS 17.4 simulator may not show up the webview context with "safari" + # after a fresh simlator instance creation. + # Re-launch the process could be a workaround in my debugging. + driver.terminate_app('com.apple.mobilesafari') + driver.activate_app('com.apple.mobilesafari') + + yield driver + + driver.quit() + + +def test_context(driver: 'WebDriver') -> None: + """Test Safari context switching.""" + contexts = driver.contexts + assert 'NATIVE_APP' == contexts[0] + assert contexts[1].startswith('WEBVIEW_') + driver.switch_to.context(contexts[1]) + assert 'WEBVIEW_' in driver.current_context + + +def test_navigation(driver: 'WebDriver') -> None: + """Test Safari navigation to Google.""" + contexts = driver.contexts + for context in contexts: + if context.startswith('WEBVIEW_'): + driver.switch_to.context(context) + break + else: + pytest.fail('Could not set WEBVIEW context') + driver.get('http://google.com') + for _ in range(5): + time.sleep(0.5) + if 'Google' == driver.title: + return -class SafariTests(unittest.TestCase): - def setUp(self): - desired_caps = { - 'browserName': 'safari', - 'platformName': 'iOS', - 'platformVersion': '7.1', - 'deviceName': 'iPhone Simulator', - 'nativeWebTap': True, - 'safariIgnoreFraudWarning': True - } - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def tearDown(self): - self.driver.quit() - - def test_context(self): - self.assertEqual([u'NATIVE_APP', u'WEBVIEW_1'], self.driver.contexts) - self.assertEqual('WEBVIEW_1', self.driver.current_context) - - def test_get(self): - self.driver.get("http://google.com") - self.assertEqual('Google', self.driver.title) - -if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(SafariTests) - unittest.TextTestRunner(verbosity=2).run(suite) + pytest.fail('The title was wrong') diff --git a/test/functional/mac/__init__.py b/test/functional/mac/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/functional/mac/execute_script_test.py b/test/functional/mac/execute_script_test.py new file mode 100644 index 00000000..95144e3d --- /dev/null +++ b/test/functional/mac/execute_script_test.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +# 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. + +from appium.webdriver.common.appiumby import AppiumBy +from test.functional.mac.helper.test_helper import BaseTestCase +from test.functional.test_helper import wait_for_element + + +class TestExecuteScript(BaseTestCase): + def test_sending_custom_keys(self) -> None: + edit_field = wait_for_element(self.driver, AppiumBy.CLASS_NAME, 'XCUIElementTypeTextView') + flagsShift = 1 << 1 + self.driver.execute_script( + 'macos: keys', + { + 'keys': [ + { + 'key': 'h', + 'modifierFlags': flagsShift, + }, + { + 'key': 'i', + 'modifierFlags': flagsShift, + }, + ] + }, + ) + assert edit_field.text == 'HI' diff --git a/test/functional/mac/helper/__init__.py b/test/functional/mac/helper/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/functional/ios/desired_capabilities.py b/test/functional/mac/helper/desired_capabilities.py similarity index 64% rename from test/functional/ios/desired_capabilities.py rename to test/functional/mac/helper/desired_capabilities.py index 319849cc..648420ef 100644 --- a/test/functional/ios/desired_capabilities.py +++ b/test/functional/mac/helper/desired_capabilities.py @@ -12,19 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os +from typing import Any, Dict -# Returns abs path relative to this file and not cwd -PATH = lambda p: os.path.abspath( - os.path.join(os.path.dirname(__file__), p) -) - -def get_desired_capabilities(app): - desired_caps = { - 'deviceName': 'iPhone Simulator', - 'platformName': 'iOS', - 'app': PATH('../../apps/' + app), - } +def get_desired_capabilities() -> Dict[str, Any]: + desired_caps: Dict[str, Any] = {'platformName': 'mac', 'automationName': 'Mac2', 'bundleId': 'com.apple.TextEdit'} return desired_caps diff --git a/test/functional/mac/helper/test_helper.py b/test/functional/mac/helper/test_helper.py new file mode 100644 index 00000000..0b7463da --- /dev/null +++ b/test/functional/mac/helper/test_helper.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +# 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. + +from appium import webdriver +from appium.options.mac import Mac2Options +from appium.webdriver.client_config import AppiumClientConfig +from test.helpers.constants import SERVER_URL_BASE + +from .desired_capabilities import get_desired_capabilities + + +class BaseTestCase(object): + def setup_method(self) -> None: + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE) + client_config.timeout = 600 + self.driver = webdriver.Remote( + SERVER_URL_BASE, options=Mac2Options().load_capabilities(get_desired_capabilities()), client_config=client_config + ) + + def teardown_method(self, method) -> None: # type: ignore + if not hasattr(self, 'driver'): + return + + self.driver.quit() diff --git a/test/functional/mac/webelement_test.py b/test/functional/mac/webelement_test.py new file mode 100644 index 00000000..f2fc4687 --- /dev/null +++ b/test/functional/mac/webelement_test.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +# 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. + +from appium.webdriver.common.appiumby import AppiumBy +from test.functional.mac.helper.test_helper import BaseTestCase +from test.functional.test_helper import wait_for_element + + +class TestWebElement(BaseTestCase): + def test_clear_text_field(self) -> None: + edit_field = wait_for_element(self.driver, AppiumBy.CLASS_NAME, 'XCUIElementTypeTextView') + edit_field.send_keys('helloworld') + assert edit_field.text == 'helloworld' + edit_field.clear() + assert edit_field.text == '' diff --git a/test/functional/test_helper.py b/test/functional/test_helper.py new file mode 100644 index 00000000..90500ea0 --- /dev/null +++ b/test/functional/test_helper.py @@ -0,0 +1,142 @@ +import os +import socket +import time +from dataclasses import dataclass +from time import sleep +from typing import TYPE_CHECKING, Any, Callable, Optional + +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + from appium.webdriver.webelement import WebElement + + +class NoAvailablePortError(Exception): + pass + + +def get_available_from_port_range(from_port: int, to_port: int) -> int: + """Returns available local port number. + + Args: + from_port: The start port to search + to_port: The end port to search + + Returns: + int: available local port number which are found first + + """ + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + for port in range(from_port, to_port): + try: + if sock.connect_ex(('localhost', port)) != 0: + return port + finally: + sock.close() + + raise NoAvailablePortError(f'No available port between {from_port} and {to_port}') + + +def is_ci() -> bool: + """Returns if current execution is running on CI + + Returns: + `True` if current executions is on CI + """ + return os.getenv('CI', 'false') == 'true' + + +def wait_for_condition(method: Callable, timeout_sec: float = 5, interval_sec: float = 1) -> Any: + """Wait while `method` returns the built-in objects considered false + + https://docs.python.org/3/library/stdtypes.html#truth-value-testing + + Args: + method: The target method to be waited + timeout: The timeout to be waited (sec.) + interval_sec: The interval for wait (sec.) + + Returns: + Any: value which `method` returns + + Raises: + ValueError: When interval isn't more than 0 + + """ + if interval_sec < 0: + raise ValueError('interval_sec needs to be not less than 0') + + started = time.time() + while time.time() - started <= timeout_sec: + result = method() + if result: + break + sleep(interval_sec) + return result + + +def wait_for_element(driver: 'WebDriver', locator: str, value: str, timeout_sec: float = 10) -> 'WebElement': + """Wait until the element located + + Args: + driver: WebDriver instance + locator: Locator like WebDriver, Mobile JSON Wire Protocol + (e.g. `appium.webdriver.common.appiumby.AppiumBy.ACCESSIBILITY_ID`) + value: Query value to locator + timeout_sec: Maximum time to wait the element. If time is over, `TimeoutException` is thrown + + Raises: + `selenium.common.exceptions.TimeoutException` + + Returns: + The found WebElement + """ + return WebDriverWait(driver, timeout_sec).until(EC.presence_of_element_located((locator, value))) + + +@dataclass +class WorkerInfo: + """Information about the current test worker in parallel execution.""" + + worker_number: Optional[int] + total_workers: Optional[int] + + @property + def is_parallel(self) -> bool: + """Check if running in parallel mode.""" + return self.worker_number is not None and self.total_workers is not None + + +def get_worker_info() -> WorkerInfo: + """ + Get current worker number and total worker count from pytest-xdist environment variables. + + Returns: + WorkerInfo: Worker information or None values if not running in parallel + """ + worker_number = os.getenv('PYTEST_XDIST_WORKER') + worker_count = os.getenv('PYTEST_XDIST_WORKER_COUNT') + + if worker_number and worker_count: + # Extract number from worker string like 'gw0', 'gw1', etc. + try: + worker_num = int(worker_number.replace('gw', '')) + total_workers = int(worker_count) + return WorkerInfo(worker_number=worker_num, total_workers=total_workers) + except (ValueError, AttributeError): + pass + + return WorkerInfo(worker_number=None, total_workers=None) + + +def get_wda_port() -> int: + """ + Get a unique WDA port for the current worker. + Uses base port 8100 and increments by worker number. + """ + worker_info = get_worker_info() + return 8100 + (worker_info.worker_number or 0) diff --git a/test/helpers/__init__.py b/test/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/helpers/constants.py b/test/helpers/constants.py new file mode 100644 index 00000000..f2cff27d --- /dev/null +++ b/test/helpers/constants.py @@ -0,0 +1,3 @@ +import os + +SERVER_URL_BASE = f'http://{os.getenv("APPIUM_TEST_SERVER_HOST", "127.0.0.1")}:{os.getenv("APPIUM_TEST_SERVER_PORT", "4723")}' diff --git a/test/pytest.ini b/test/pytest.ini new file mode 100644 index 00000000..0c95eb57 --- /dev/null +++ b/test/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = -ra -q --cov=appium diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 00000000..cc173e9d --- /dev/null +++ b/test/unit/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/test/unit/helper/__init__.py b/test/unit/helper/__init__.py new file mode 100644 index 00000000..cc173e9d --- /dev/null +++ b/test/unit/helper/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/test/unit/helper/test_helper.py b/test/unit/helper/test_helper.py new file mode 100644 index 00000000..58e35817 --- /dev/null +++ b/test/unit/helper/test_helper.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python + +# 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. + +import json +from typing import TYPE_CHECKING, Any, Dict + +import httpretty + +from appium import webdriver +from appium.options.android import UiAutomator2Options +from appium.options.ios import XCUITestOptions +from test.helpers.constants import SERVER_URL_BASE + +if TYPE_CHECKING: + from httpretty.core import HTTPrettyRequestEmpty + + from appium.webdriver.webdriver import WebDriver + + +def appium_command(command: str) -> str: + """Return a command of Appium + + Returns: + str: A string of command URL + """ + return ( + f'{SERVER_URL_BASE}{command}' + if SERVER_URL_BASE.endswith('/') or command.startswith('/') + else f'{SERVER_URL_BASE}/{command}' + ) + + +def android_w3c_driver() -> 'WebDriver': + """Return a W3C driver which is generated by a mock response for Android + + Returns: + `webdriver.webdriver.WebDriver`: An instance of WebDriver + """ + + response_body_json = json.dumps( + { + 'sessionId': '1234567890', + 'capabilities': { + 'platform': 'LINUX', + 'desired': { + 'platformName': 'Android', + 'automationName': 'uiautomator2', + 'platformVersion': '7.1.1', + 'deviceName': 'Android Emulator', + 'app': '/test/apps/ApiDemos-debug.apk', + }, + 'platformName': 'Android', + 'automationName': 'uiautomator2', + 'platformVersion': '7.1.1', + 'deviceName': 'emulator-5554', + 'app': '/test/apps/ApiDemos-debug.apk', + 'deviceUDID': 'emulator-5554', + 'appPackage': 'io.appium.android.apis', + 'appWaitPackage': 'io.appium.android.apis', + 'appActivity': 'io.appium.android.apis.ApiDemos', + 'appWaitActivity': 'io.appium.android.apis.ApiDemos', + }, + } + ) + + httpretty.register_uri(httpretty.POST, appium_command('/session'), body=response_body_json) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + 'automationName': 'UIAutomator2', + } + + driver = webdriver.Remote(SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps)) + return driver + + +def ios_w3c_driver() -> 'WebDriver': + """Return a W3C driver which is generated by a mock response for iOS + + Returns: + `webdriver.webdriver.WebDriver`: An instance of WebDriver + """ + response_body_json = json.dumps( + { + 'sessionId': '1234567890', + 'capabilities': { + 'device': 'iphone', + 'browserName': 'UICatalog', + 'sdkVersion': '11.4', + 'CFBundleIdentifier': 'com.example.apple-samplecode.UICatalog', + }, + } + ) + + httpretty.register_uri(httpretty.POST, appium_command('/session'), body=response_body_json) + + desired_caps = { + 'platformName': 'iOS', + 'deviceName': 'iPhone Simulator', + 'app': 'path/to/app', + 'automationName': 'XCUITest', + } + + driver = webdriver.Remote(SERVER_URL_BASE, options=XCUITestOptions().load_capabilities(desired_caps)) + return driver + + +def ios_w3c_driver_with_extensions(extensions) -> 'WebDriver': + """Return a W3C driver which is generated by a mock response for iOS + + Returns: + `webdriver.webdriver.WebDriver`: An instance of WebDriver + """ + + response_body_json = json.dumps( + { + 'sessionId': '1234567890', + 'capabilities': { + 'device': 'iphone', + 'browserName': 'UICatalog', + 'sdkVersion': '11.4', + 'CFBundleIdentifier': 'com.example.apple-samplecode.UICatalog', + }, + } + ) + + httpretty.register_uri(httpretty.POST, appium_command('/session'), body=response_body_json) + + desired_caps = { + 'platformName': 'iOS', + 'deviceName': 'iPhone Simulator', + 'app': 'path/to/app', + 'automationName': 'XCUITest', + } + + driver = webdriver.Remote(SERVER_URL_BASE, options=XCUITestOptions().load_capabilities(desired_caps), extensions=extensions) + return driver + + +def flutter_w3c_driver() -> 'WebDriver': + """Return a W3C driver which is generated by a mock response for Flutter + + Returns: + `webdriver.webdriver.WebDriver`: An instance of WebDriver + """ + + response_body_json = json.dumps( + { + 'sessionId': '1234567890', + 'capabilities': { + 'platform': 'LINUX', + 'desired': { + 'platformName': 'Android', + 'autoGrantPermissions': True, + 'flutterSystemPort': 9999, + 'flutterElementWaitTimeout': 10000, + 'flutterEnableMockCamera': True, + 'flutterServerLaunchTimeout': 120000, + 'automationName': 'FlutterIntegration', + 'platformVersion': '7.1.1', + 'deviceName': 'Android Emulator', + 'app': '/test/apps/ApiDemos-debug.apk', + }, + 'platformName': 'Android', + 'automationName': 'FlutterIntegration', + 'platformVersion': '7.1.1', + 'deviceName': 'emulator-5554', + 'app': '/test/apps/ApiDemos-debug.apk', + 'deviceUDID': 'emulator-5554', + 'appPackage': 'io.appium.android.apis', + 'appWaitPackage': 'io.appium.android.apis', + }, + } + ) + + httpretty.register_uri(httpretty.POST, appium_command('/session'), body=response_body_json) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + 'automationName': 'FlutterIntegration', + } + + driver = webdriver.Remote(SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps)) + return driver + + +def get_httpretty_request_body(request: 'HTTPrettyRequestEmpty') -> Dict[str, Any]: + """Returns utf-8 decoded request body""" + return json.loads(request.body.decode('utf-8')) diff --git a/test/unit/multi_action_tests.py b/test/unit/multi_action_tests.py deleted file mode 100644 index 8d1d67ca..00000000 --- a/test/unit/multi_action_tests.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python - -# 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. - -import unittest - -from appium.webdriver.common.multi_action import MultiAction -from appium.webdriver.common.touch_action import TouchAction - - -class MultiActionTests(unittest.TestCase): - def setUp(self): - self._multi_action = MultiAction(DriverStub()) - - def test_json(self): - self.maxDiff = None - json = { - 'actions': [ - [ - {'action': 'press', 'options': {'x': None, 'y': None, 'element': 1}}, - {'action': 'moveTo', 'options': {'x': 10, 'y': 20}}, - {'action': 'release', 'options': {}} - ], - [ - {'action': 'press', 'options': {'x': 11, 'y': 30, 'element': 5}}, - {'action': 'moveTo', 'options': {'x': 12, 'y': -300}}, - {'action': 'release', 'options': {}} - ] - ], - 'elementId': 0 - } - t1 = TouchAction(DriverStub()).press(ElementStub(1)).move_to(x=10, y=20).release() - t2 = TouchAction(DriverStub()).press(ElementStub(5), 11, 30).move_to(x=12, y=-300).release() - self._multi_action.add(t1, t2) - self.assertEqual(json, self._multi_action.json_wire_gestures) - - -class DriverStub(object): - def execute(self, action, params): - print "driver.execute called" - - -class ElementStub(object): - def __init__(self, id): - self._id = id - - @property - def id(self): - return self._id - - -if __name__ == "__main__": - unittest.main() diff --git a/test/unit/touch_action_tests.py b/test/unit/touch_action_tests.py deleted file mode 100644 index a9cab446..00000000 --- a/test/unit/touch_action_tests.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python - -# 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. - -import unittest - -from appium.webdriver.common.touch_action import TouchAction - - -class TouchActionTests(unittest.TestCase): - def setUp(self): - self._touch_action = TouchAction(DriverStub()) - - def test_tap_json(self): - json = [ - {'action': 'tap', 'options': {'x': None, 'y': None, 'count': 1, 'element': 1}} - ] - self._touch_action.tap(ElementStub(1)) - self.assertEqual(json, self._touch_action.json_wire_gestures) - - def test_tap_x_y_json(self): - json = [ - {'action': 'tap', 'options': {'x': 3, 'y': 4, 'count': 1, 'element': 2}} - ] - self._touch_action.tap(ElementStub(2), 3, 4) - self.assertEqual(json, self._touch_action.json_wire_gestures) - - -class DriverStub(object): - def execute(self, action, params): - print "driver.execute called" - - -class ElementStub(object): - def __init__(self, id, x=None, y=None, count=None): - self._id = id - - @property - def id(self): - return self._id - - -if __name__ == "__main__": - unittest.main() diff --git a/test/unit/webdriver/app_test.py b/test/unit/webdriver/app_test.py new file mode 100644 index 00000000..34767535 --- /dev/null +++ b/test/unit/webdriver/app_test.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python + +# 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. +import httpretty +import pytest + +from appium.webdriver.applicationstate import ApplicationState +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_install_app(driver_func): + driver = driver_func() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + result = driver.install_app('path/to/app') + + assert { + 'args': [{'app': 'path/to/app', 'appPath': 'path/to/app'}], + 'script': 'mobile: installApp', + } == get_httpretty_request_body(httpretty.last_request()) + assert isinstance(result, WebDriver) + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_remove_app(driver_func): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + result = driver.remove_app('com.app.id') + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: removeApp', + } == get_httpretty_request_body(httpretty.last_request()) + assert isinstance(result, WebDriver) + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_app_installed(driver_func): + driver = driver_func() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') + result = driver.is_app_installed('com.app.id') + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: isAppInstalled', + } == get_httpretty_request_body(httpretty.last_request()) + assert result is True + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_terminate_app(driver_func): + driver = driver_func() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') + result = driver.terminate_app('com.app.id') + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: terminateApp', + } == get_httpretty_request_body(httpretty.last_request()) + assert result is True + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_activate_app(driver_func): + driver = driver_func() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + result = driver.activate_app('com.app.id') + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: activateApp', + } == get_httpretty_request_body(httpretty.last_request()) + assert isinstance(result, WebDriver) + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_background_app(driver_func): + driver = driver_func() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + result = driver.background_app(0) + + assert {'args': [{'seconds': 0}], 'script': 'mobile: backgroundApp'} == get_httpretty_request_body(httpretty.last_request()) + assert isinstance(result, WebDriver) + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_query_app_state(driver_func): + driver = driver_func() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": 3}') + result = driver.query_app_state('com.app.id') + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: queryAppState', + } == get_httpretty_request_body(httpretty.last_request()) + assert result is ApplicationState.RUNNING_IN_BACKGROUND + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_app_strings(driver_func): + driver = driver_func() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', + ) + result = driver.app_strings() + + assert {'args': [{}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body(httpretty.last_request()) + assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_app_strings_with_lang(driver_func): + driver = driver_func() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', + ) + result = driver.app_strings('en') + + assert {'args': [{'language': 'en'}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body( + httpretty.last_request() + ) + assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_app_strings_with_lang_and_file(driver_func): + driver = driver_func() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', + ) + result = driver.app_strings('en', 'some_file') + + assert { + 'args': [{'language': 'en', 'stringFile': 'some_file'}], + 'script': 'mobile: getAppStrings', + } == get_httpretty_request_body(httpretty.last_request()) + assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result diff --git a/test/unit/webdriver/appium_connection_test.py b/test/unit/webdriver/appium_connection_test.py new file mode 100644 index 00000000..c59303e7 --- /dev/null +++ b/test/unit/webdriver/appium_connection_test.py @@ -0,0 +1,39 @@ +import unittest +from urllib import parse + +from appium.webdriver import appium_connection + + +class AppiumConnectionTest(unittest.TestCase): + def test_get_remote_connection_headers(self): + headers = appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http://127.0.0.1:4723/session') + ) + self.assertIsNotNone(headers.get('X-Idempotency-Key')) + + headers = appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http://127.0.0.1:4723/session/session_id') + ) + self.assertIsNone(headers.get('X-Idempotency-Key')) + + appium_connection.AppiumConnection.extra_headers = {'custom': 'header'} + + headers = appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http://127.0.0.1:4723/session') + ) + self.assertIsNotNone(headers.get('X-Idempotency-Key')) + self.assertEqual(headers.get('custom'), 'header') + + headers = appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http://127.0.0.1:4723/session/session_id') + ) + self.assertIsNone(headers.get('X-Idempotency-Key')) + self.assertEqual(headers.get('custom'), 'header') + + def test_remove_headers_case_insensitive(self): + for h in ['X-Idempotency-Key', 'X-idempotency-Key', 'x-idempotency-key']: + appium_connection.AppiumConnection.extra_headers = {h: 'value'} + appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http://127.0.0.1:4723/session/session_id') + ) + self.assertEqual(appium_connection.AppiumConnection.extra_headers, {}) diff --git a/test/unit/webdriver/appium_service_test.py b/test/unit/webdriver/appium_service_test.py new file mode 100644 index 00000000..763a6a37 --- /dev/null +++ b/test/unit/webdriver/appium_service_test.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +# 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. + +from appium.webdriver.appium_service import AppiumService + + +class TestAppiumService(object): + def test_get_instance(self): + assert AppiumService() diff --git a/test/unit/webdriver/context_test.py b/test/unit/webdriver/context_test.py new file mode 100644 index 00000000..80c2c0f7 --- /dev/null +++ b/test/unit/webdriver/context_test.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebDriverContext(object): + @httpretty.activate + def test_current_contexts(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890/context'), body='{"value": "NATIVE_APP"}') + assert driver.current_context == 'NATIVE_APP' + + @httpretty.activate + def test_get_contexts(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.GET, appium_command('/session/1234567890/contexts'), body='{"value": ["NATIVE_APP", "CHROMIUM"]}' + ) + + assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts + + @httpretty.activate + def test_switch_to_context(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/context'), body='{"value": null}') + + driver.switch_to.context(None) + + assert {'name': None} == get_httpretty_request_body(httpretty.last_request()) + + @httpretty.activate + def test_switch_to_context_native_app(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/context'), body='{"value": null}') + + driver.switch_to.context('NATIVE_APP') + + assert {'name': 'NATIVE_APP'} == get_httpretty_request_body(httpretty.last_request()) diff --git a/test/unit/webdriver/device/activities_test.py b/test/unit/webdriver/device/activities_test.py new file mode 100644 index 00000000..2374ba6c --- /dev/null +++ b/test/unit/webdriver/device/activities_test.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from test.unit.helper.test_helper import android_w3c_driver, appium_command + + +class TestWebDriverActivities(object): + @httpretty.activate + def test_current_activity(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/appium/device/current_activity'), + body='{"value": ".ExampleActivity"}', + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": ".ExampleActivity"}', + ) + assert driver.current_activity == '.ExampleActivity' + + @httpretty.activate + def test_wait_activity(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/appium/device/current_activity'), + body='{"value": ".ExampleActivity"}', + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": ".ExampleActivity"}', + ) + assert driver.wait_activity('.ExampleActivity', 1) is True diff --git a/test/unit/webdriver/device/clipboard_test.py b/test/unit/webdriver/device/clipboard_test.py new file mode 100644 index 00000000..7e21797a --- /dev/null +++ b/test/unit/webdriver/device/clipboard_test.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from appium.webdriver.clipboard_content_type import ClipboardContentType +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver + + +class TestWebDriverClipboard(object): + @httpretty.activate + def test_set_clipboard_with_url(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/appium/device/set_clipboard'), body='{"value": ""}' + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": ""}', + ) + driver.set_clipboard(bytes(str('http://appium.io/'), 'UTF-8'), ClipboardContentType.URL, 'label for android') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['args'][0]['content'] == 'aHR0cDovL2FwcGl1bS5pby8=' + assert d['args'][0]['contentType'] == 'url' + assert d['args'][0]['label'] == 'label for android' + + @httpretty.activate + def test_set_clipboard_text(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/appium/device/set_clipboard'), body='{"value": ""}' + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": ""}', + ) + driver.set_clipboard_text('hello') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['args'][0]['content'] == 'aGVsbG8=' + assert d['args'][0]['contentType'] == 'plaintext' diff --git a/test/unit/webdriver/device/common_test.py b/test/unit/webdriver/device/common_test.py new file mode 100644 index 00000000..43b069a0 --- /dev/null +++ b/test/unit/webdriver/device/common_test.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebDriverCommon(object): + @httpretty.activate + def test_open_notifications(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.open_notifications(), WebDriver) + assert {'args': [], 'script': 'mobile: openNotifications'} == get_httpretty_request_body(httpretty.last_request()) + + @httpretty.activate + def test_current_package(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/appium/device/current_package'), + body='{"value": ".ExamplePackage"}', + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": ".ExamplePackage"}', + ) + assert driver.current_package == '.ExamplePackage' diff --git a/test/unit/webdriver/device/device_time_test.py b/test/unit/webdriver/device/device_time_test.py new file mode 100644 index 00000000..8c6f5e0b --- /dev/null +++ b/test/unit/webdriver/device/device_time_test.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebDriverDeviceTime(object): + @httpretty.activate + def test_device_time(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/appium/device/system_time'), + body='{"value": "2019-01-05T14:46:44+09:00"}', + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": "2019-01-05T14:46:44+09:00"}', + ) + assert driver.device_time == '2019-01-05T14:46:44+09:00' + + @httpretty.activate + def test_get_device_time(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/appium/device/system_time'), + body='{"value": "2019-01-05T14:46:44+09:00"}', + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": "2019-01-05T14:46:44+09:00"}', + ) + assert driver.get_device_time() == '2019-01-05T14:46:44+09:00' + + @httpretty.activate + def test_get_formatted_device_time(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/system_time'), + body='{"value": "2019-01-08"}', + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": "2019-01-08"}', + ) + assert driver.get_device_time('YYYY-MM-DD') == '2019-01-08' + + d = get_httpretty_request_body(httpretty.last_request()) + assert d.get('format', d['args'][0]['format']) == 'YYYY-MM-DD' diff --git a/test/unit/webdriver/device/display_test.py b/test/unit/webdriver/device/display_test.py new file mode 100644 index 00000000..bce0b4e4 --- /dev/null +++ b/test/unit/webdriver/device/display_test.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from test.unit.helper.test_helper import android_w3c_driver, appium_command + + +class TestWebDriverDisplay(object): + @httpretty.activate + def test_get_display_density(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.GET, appium_command('/session/1234567890/appium/device/display_density'), body='{"value": 560}' + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": 560}') + assert driver.get_display_density() == 560 diff --git a/test/unit/webdriver/device/fingerprint_test.py b/test/unit/webdriver/device/fingerprint_test.py new file mode 100644 index 00000000..e6facf86 --- /dev/null +++ b/test/unit/webdriver/device/fingerprint_test.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebDriverFingerprint(object): + @httpretty.activate + def test_finger_print(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/finger_print'), + # body is None + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + # body is None + ) + + assert isinstance(driver.finger_print(1), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d.get('fingerprintId', d['args'][0]['fingerprintId']) == 1 diff --git a/test/unit/webdriver/device/gsm_test.py b/test/unit/webdriver/device/gsm_test.py new file mode 100644 index 00000000..cd776a94 --- /dev/null +++ b/test/unit/webdriver/device/gsm_test.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from appium.webdriver.extensions.android.gsm import GsmCallActions, GsmSignalStrength, GsmVoiceState +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebDriveGsm(object): + @httpretty.activate + def test_make_gsm_call(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/gsm_call'), + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + assert isinstance(driver.make_gsm_call('5551234567', GsmCallActions.CALL), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['args'][0]['phoneNumber'] == '5551234567' + assert d['args'][0]['action'] == GsmCallActions.CALL + + @httpretty.activate + def test_set_gsm_signal(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/gsm_signal'), + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + assert isinstance(driver.set_gsm_signal(GsmSignalStrength.GREAT), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['args'][0]['strength'] == GsmSignalStrength.GREAT + + @httpretty.activate + def test_set_gsm_voice(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/gsm_voice'), + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + assert isinstance(driver.set_gsm_voice(GsmVoiceState.ROAMING), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['args'][0]['state'] == GsmVoiceState.ROAMING diff --git a/test/unit/webdriver/device/keyboard_test.py b/test/unit/webdriver/device/keyboard_test.py new file mode 100644 index 00000000..526f1826 --- /dev/null +++ b/test/unit/webdriver/device/keyboard_test.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver + + +class TestWebDriverKeyboardAndroid(object): + @httpretty.activate + def test_hide_keyboard(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/device/hide_keyboard')) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.hide_keyboard(), WebDriver) + + @httpretty.activate + def test_press_keycode(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/appium/device/press_keycode'), body='{"value": "86"}' + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": "86"}') + driver.press_keycode(86) + d = get_httpretty_request_body((httpretty.last_request())) + assert d.get('keycode', d['args'][0]['keycode']) == 86 + + @httpretty.activate + def test_long_press_keycode(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/long_press_keycode'), + body='{"value": "86"}', + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": "86"}') + driver.long_press_keycode(86) + d = get_httpretty_request_body((httpretty.last_request())) + assert d.get('keycode', d['args'][0]['keycode']) == 86 + + @httpretty.activate + def test_keyevent(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/appium/device/keyevent'), body='{keycode: 86}' + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": "86"}') + assert isinstance(driver.keyevent(86), WebDriver) + + @httpretty.activate + def test_press_keycode_with_flags(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/press_keycode'), + body='{keycode: 86, metastate: 2097153, flags: 44}', + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + # metastate is META_SHIFT_ON and META_NUM_LOCK_ON + # flags is CANCELFLAG_CANCELEDED, FLAG_KEEP_TOUCH_MODE, FLAG_FROM_SYSTEM + assert isinstance( + driver.press_keycode(86, metastate=0x00000001 | 0x00200000, flags=0x20 | 0x00000004 | 0x00000008), + WebDriver, + ) + + @httpretty.activate + def test_long_press_keycode_with_flags(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/long_press_keycode'), + body='{keycode: 86, metastate: 2097153, flags: 44}', + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + # metastate is META_SHIFT_ON and META_NUM_LOCK_ON + # flags is CANCELFLAG_CANCELEDED, FLAG_KEEP_TOUCH_MODE, FLAG_FROM_SYSTEM + assert isinstance( + driver.long_press_keycode(86, metastate=0x00000001 | 0x00200000, flags=0x20 | 0x00000004 | 0x00000008), + WebDriver, + ) + + +class TestWebDriverKeyboardIOS(object): + @httpretty.activate + def test_hide_keyboard(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.hide_keyboard(), WebDriver) + assert {'args': [{}], 'script': 'mobile: hideKeyboard'} == get_httpretty_request_body(httpretty.last_request()) + + @httpretty.activate + def test_hide_keyboard_with_key(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.hide_keyboard(key_name='Done'), WebDriver) + assert {'args': [{'keys': ['Done']}], 'script': 'mobile: hideKeyboard'} == get_httpretty_request_body( + httpretty.last_request() + ) + + @httpretty.activate + def test_hide_keyboard_with_key_and_strategy(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.hide_keyboard(strategy='pressKey', key='Done'), WebDriver) + # only 'keys' works + assert {'args': [{'keys': ['Done']}], 'script': 'mobile: hideKeyboard'} == get_httpretty_request_body( + httpretty.last_request() + ) + + @httpretty.activate + def test_is_keyboard_shown(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + driver.is_keyboard_shown(), WebDriver + assert {'script': 'mobile: isKeyboardShown', 'args': []} == get_httpretty_request_body(httpretty.last_request()) + + @httpretty.activate + def test_press_button(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + driver.press_button('Home') + assert {'script': 'mobile: pressButton', 'args': [{'name': 'Home'}]} == get_httpretty_request_body( + httpretty.last_request() + ) diff --git a/test/unit/webdriver/device/location_test.py b/test/unit/webdriver/device/location_test.py new file mode 100644 index 00000000..fb0112bc --- /dev/null +++ b/test/unit/webdriver/device/location_test.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + +FLT_EPSILON = 1e-9 + + +class TestWebDriverLocation(object): + @httpretty.activate + def test_toggle_location_services(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/device/toggle_location_services')) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.toggle_location_services(), WebDriver) + + @httpretty.activate + def test_set_location_float(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/location')) + assert isinstance(driver.set_location(11.1, 22.2, 33.3, 23.2, 12), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert abs(d['location']['latitude'] - 11.1) <= FLT_EPSILON + assert abs(d['location']['longitude'] - 22.2) <= FLT_EPSILON + assert abs(d['location']['altitude'] - 33.3) <= FLT_EPSILON + assert abs(d['location']['speed'] - 23.2) <= FLT_EPSILON + assert abs(d['location']['satellites'] - 12) <= FLT_EPSILON + + @httpretty.activate + def test_set_location_str(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/location')) + assert isinstance(driver.set_location('11.1', '22.2', '33.3', '23.2', '12'), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['location']['latitude'] == '11.1' + assert d['location']['longitude'] == '22.2' + assert d['location']['altitude'] == '33.3' + assert d['location']['speed'] == '23.2' + assert d['location']['satellites'] == '12' + + @httpretty.activate + def test_set_location_without_altitude(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/location')) + assert isinstance(driver.set_location(11.1, 22.2, speed=23.2), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert abs(d['location']['latitude'] - 11.1) <= FLT_EPSILON + assert abs(d['location']['longitude'] - 22.2) <= FLT_EPSILON + assert abs(d['location']['speed'] - 23.2) <= FLT_EPSILON + assert d['location'].get('altitude') is None + + @httpretty.activate + def test_set_location_without_speed(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/location')) + assert isinstance(driver.set_location(11.1, 22.2, 33.3), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert abs(d['location']['latitude'] - 11.1) <= FLT_EPSILON + assert abs(d['location']['longitude'] - 22.2) <= FLT_EPSILON + assert abs(d['location']['altitude'] - 33.3) <= FLT_EPSILON + assert d['location'].get('speed') is None + + @httpretty.activate + def test_location(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/location'), + body='{"value": {"latitude": 11.1, "longitude": 22.2, "altitude": 33.3}}', + ) + val = driver.location + assert abs(val['latitude'] - 11.1) <= FLT_EPSILON + assert abs(val['longitude'] - 22.2) <= FLT_EPSILON + assert abs(val['altitude'] - 33.3) <= FLT_EPSILON diff --git a/test/unit/webdriver/device/lock_test.py b/test/unit/webdriver/device/lock_test.py new file mode 100644 index 00000000..b5469854 --- /dev/null +++ b/test/unit/webdriver/device/lock_test.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver + + +class TestWebDriverLockAndroid(object): + @httpretty.activate + def test_lock(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}') + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + driver.lock(1) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d.get('seconds', d['args'][0]['seconds']) == 1 + + @httpretty.activate + def test_lock_no_args(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}') + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + driver.lock() + + @httpretty.activate + def test_islocked_false(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/appium/device/is_locked'), body='{"value": false}' + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": false}') + assert driver.is_locked() is False + + @httpretty.activate + def test_islocked_true(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/appium/device/is_locked'), body='{"value": true}' + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') + assert driver.is_locked() is True + + @httpretty.activate + def test_unlock(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/unlock'), + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.unlock(), WebDriver) + + +class TestWebDriverLockIOS(object): + @httpretty.activate + def test_lock(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}') + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + driver.lock(1) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d.get('seconds', d['args'][0]['seconds']) == 1 + + @httpretty.activate + def test_lock_no_args(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}') + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + driver.lock() + + @httpretty.activate + def test_islocked_false(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/appium/device/is_locked'), body='{"value": false}' + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": false}') + assert driver.is_locked() is False + + @httpretty.activate + def test_islocked_true(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/appium/device/is_locked'), body='{"value": true}' + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') + assert driver.is_locked() is True + + @httpretty.activate + def test_unlock(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/unlock'), + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.unlock(), WebDriver) + + @httpretty.activate + def test_touch_id(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.touch_id(True), WebDriver) + assert { + 'script': 'mobile: sendBiometricMatch', + 'args': [{'match': True, 'type': 'touchId'}], + } == get_httpretty_request_body(httpretty.last_request()) + + @httpretty.activate + def test_enroll_biometric(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.toggle_touch_id_enrollment(), WebDriver) + assert {'script': 'mobile: enrollBiometric', 'args': [{'isEnabled': True}]} == get_httpretty_request_body( + httpretty.last_request() + ) diff --git a/test/unit/webdriver/device/power_test.py b/test/unit/webdriver/device/power_test.py new file mode 100644 index 00000000..2d87a8d4 --- /dev/null +++ b/test/unit/webdriver/device/power_test.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from appium.webdriver.extensions.android.power import Power +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebDriverPower(object): + @httpretty.activate + def test_set_power_capacity(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/power_capacity'), + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + assert isinstance(driver.set_power_capacity(50), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['args'][0]['percent'] == 50 + + @httpretty.activate + def test_set_power_ac(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/power_ac'), + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + assert isinstance(driver.set_power_ac(Power.AC_ON), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['args'][0]['state'] == Power.AC_ON diff --git a/test/unit/webdriver/device/remote_fs_test.py b/test/unit/webdriver/device/remote_fs_test.py new file mode 100644 index 00000000..4de3a85f --- /dev/null +++ b/test/unit/webdriver/device/remote_fs_test.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +# 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. + +import base64 + +import httpretty +import pytest +from selenium.common.exceptions import InvalidArgumentException + +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebDriverRemoteFs(object): + @httpretty.activate + def test_push_file(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/push_file'), + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + dest_path = '/path/to/file.txt' + data = base64.b64encode(bytes('HelloWorld', 'utf-8')).decode('utf-8') + + assert isinstance(driver.push_file(dest_path, data), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d.get('path', d['args'][0]['remotePath']) == dest_path + assert d.get('data', d['args'][0]['payload']) == str(data) + + @httpretty.activate + def test_push_file_invalid_arg_exception_without_src_path_and_base64data(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/push_file'), + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + dest_path = '/path/to/file.txt' + + with pytest.raises(InvalidArgumentException): + driver.push_file(dest_path) + + @httpretty.activate + def test_push_file_invalid_arg_exception_with_src_file_not_found(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/push_file'), + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/push_file'), + ) + dest_path = '/dest_path/to/file.txt' + src_path = '/src_path/to/file.txt' + + with pytest.raises(InvalidArgumentException): + driver.push_file(dest_path, source_path=src_path) + + @httpretty.activate + def test_pull_file(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/pull_file'), + body='{"value": "SGVsbG9Xb3JsZA=="}', + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": "SGVsbG9Xb3JsZA=="}', + ) + dest_path = '/path/to/file.txt' + + assert driver.pull_file(dest_path) == str(base64.b64encode(bytes('HelloWorld', 'utf-8')).decode('utf-8')) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d.get('path', d['args'][0]['remotePath']) == dest_path + + @httpretty.activate + def test_pull_folder(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/pull_folder'), + body='{"value": "base64EncodedZippedFolderData"}', + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": "base64EncodedZippedFolderData"}', + ) + dest_path = '/path/to/file.txt' + + assert driver.pull_folder(dest_path) == 'base64EncodedZippedFolderData' + + d = get_httpretty_request_body(httpretty.last_request()) + assert d.get('path', d['args'][0]['remotePath']) == dest_path diff --git a/test/unit/webdriver/device/shake_test.py b/test/unit/webdriver/device/shake_test.py new file mode 100644 index 00000000..0371852f --- /dev/null +++ b/test/unit/webdriver/device/shake_test.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +# 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. + + +import httpretty + +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import android_w3c_driver, appium_command + + +class TestWebDriverShake(object): + @httpretty.activate + def test_shake(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/shake'), + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + assert isinstance(driver.shake(), WebDriver) diff --git a/test/unit/webdriver/device/sms_test.py b/test/unit/webdriver/device/sms_test.py new file mode 100644 index 00000000..1323f50c --- /dev/null +++ b/test/unit/webdriver/device/sms_test.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebDriverSms(object): + @httpretty.activate + def test_send_sms(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/send_sms'), + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + assert isinstance(driver.send_sms('555-123-4567', 'Hey lol'), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['args'][0]['phoneNumber'] == '555-123-4567' + assert d['args'][0]['message'] == 'Hey lol' diff --git a/test/unit/webdriver/device/system_bars_test.py b/test/unit/webdriver/device/system_bars_test.py new file mode 100644 index 00000000..79e7c680 --- /dev/null +++ b/test/unit/webdriver/device/system_bars_test.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from test.unit.helper.test_helper import android_w3c_driver, appium_command + + +class TestWebDriverSystemBars(object): + @httpretty.activate + def test_get_system_bars(self): + driver = android_w3c_driver() + body = """{"value": + {"statusBar": + {"visible": true, "x": 0, "y": 0, "width": 1080, "height": 1920}, + "navigationBar": + {"visible": true, "x": 0, "y": 0, "width": 1080, "height": 126}}}""" + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/appium/device/system_bars'), + body=body, + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body=body, + ) + d = driver.get_system_bars() + + assert d['statusBar']['visible'] is True + assert d['statusBar']['x'] == 0 + assert d['statusBar']['y'] == 0 + assert d['statusBar']['width'] == 1080 + assert d['statusBar']['height'] == 1920 + + assert d['navigationBar']['visible'] is True + assert d['navigationBar']['x'] == 0 + assert d['navigationBar']['y'] == 0 + assert d['navigationBar']['width'] == 1080 + assert d['navigationBar']['height'] == 126 diff --git a/test/unit/webdriver/execute_driver_test.py b/test/unit/webdriver/execute_driver_test.py new file mode 100644 index 00000000..250b8cc8 --- /dev/null +++ b/test/unit/webdriver/execute_driver_test.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python + +# 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. + +import textwrap + +import httpretty + +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebDriverExecuteDriver(object): + @httpretty.activate + def test_batch(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/execute_driver'), + body='{"value": {"result":[' + '{"element-6066-11e4-a52e-4f735466cecf":"39000000-0000-0000-D39A-000000000000",' + '"ELEMENT":"39000000-0000-0000-D39A-000000000000"},' + '{"y":237,"x":18,"width":67,"height":24}],"logs":{' + '"error":[],"warn":["warning message"],"log":[]}}}', + ) + + script = """ + console.warn('warning message'); + const element = await driver.findElement('accessibility id', 'Buttons'); + const rect = await driver.getElementRect(element.ELEMENT); + return [element, rect]; + """ + response = driver.execute_driver(script=textwrap.dedent(script)) + # Python client convert an element item as WebElement in the result + assert response.result[0].id == '39000000-0000-0000-D39A-000000000000' + assert response.result[1]['y'] == 237 + assert response.logs['warn'] == ['warning message'] + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['script'] == textwrap.dedent(script) + assert d['type'] == 'webdriverio' + assert 'timeout' not in d + + @httpretty.activate + def test_batch_with_timeout(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/execute_driver'), + body='{"value": {"result":[' + '{"element-6066-11e4-a52e-4f735466cecf":"39000000-0000-0000-D39A-000000000000",' + '"ELEMENT":"39000000-0000-0000-D39A-000000000000"},' + '{"y":237,"x":18,"width":67,"height":24}],"logs":{' + '"error":[],"warn":["warning message"],"log":[]}}}', + ) + + script = """ + console.warn('warning message'); + const element = await driver.findElement('accessibility id', 'Buttons'); + const rect = await driver.getElementRect(element.ELEMENT); + return [element, rect]; + """ + response = driver.execute_driver(script=textwrap.dedent(script), timeout_ms=10000) + assert response.result[0].id == '39000000-0000-0000-D39A-000000000000' + assert response.result[1]['y'] == 237 + assert response.logs['error'] == [] + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['script'] == textwrap.dedent(script) + assert d['type'] == 'webdriverio' + assert d['timeout'] == 10000 diff --git a/test/unit/webdriver/flutter_integration/file/success_qr.png b/test/unit/webdriver/flutter_integration/file/success_qr.png new file mode 100644 index 00000000..8896d86f Binary files /dev/null and b/test/unit/webdriver/flutter_integration/file/success_qr.png differ diff --git a/test/unit/webdriver/flutter_integration/flutter_actions_test.py b/test/unit/webdriver/flutter_integration/flutter_actions_test.py new file mode 100644 index 00000000..50a42e59 --- /dev/null +++ b/test/unit/webdriver/flutter_integration/flutter_actions_test.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python + +# 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. + +import os + +import httpretty + +from appium.common.helper import encode_file_to_base64 +from appium.webdriver.extensions.flutter_integration.flutter_commands import FlutterCommand +from appium.webdriver.extensions.flutter_integration.flutter_finder import FlutterFinder +from appium.webdriver.extensions.flutter_integration.scroll_directions import ScrollDirection +from appium.webdriver.webelement import WebElement as MobileWebElement +from test.unit.helper.test_helper import appium_command, flutter_w3c_driver, get_httpretty_request_body + + +class TestFlutterActions(object): + @httpretty.activate + def test_double_click(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + element = MobileWebElement(driver, 'element_id') + flutter.perform_double_click(element, (10, 20)) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: doubleClick' + assert list(arguments['origin'].values())[0] == 'element_id' + assert arguments['offset'] == {'x': 10, 'y': 20} + + @httpretty.activate + def test_drag_and_drop(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + drag_element = MobileWebElement(driver, 'element_id1') + drop_element = MobileWebElement(driver, 'element_id2') + flutter.perform_drag_and_drop(drag_element, drop_element) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: dragAndDrop' + assert list(arguments['source'].values())[0] == 'element_id1' + assert list(arguments['target'].values())[0] == 'element_id2' + + @httpretty.activate + def test_scroll_till_visible(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + finder = FlutterFinder.by_key('message_field') + flutter.scroll_till_visible(finder) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + expected_arguments = { + 'finder': {'using': '-flutter key', 'value': 'message_field'}, + 'scrollDirection': 'down', + } + assert request_body['script'] == 'flutter: scrollTillVisible' + assert arguments == expected_arguments + + @httpretty.activate + def test_scroll_till_visible_with_kwargs(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + finder = FlutterFinder.by_key('message_field') + scroll_params = { + 'scrollView': FlutterFinder.by_type('Scrollable').to_dict(), + 'delta': 30, + 'maxScrolls': 30, + 'settleBetweenScrollsTimeout': 5000, + 'dragDuration': 35, + } + flutter.scroll_till_visible(finder, ScrollDirection.UP, **scroll_params) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: scrollTillVisible' + expected_arguments = { + 'finder': {'using': '-flutter key', 'value': 'message_field'}, + 'scrollView': {'using': '-flutter type', 'value': 'Scrollable'}, + 'scrollDirection': 'up', + 'dragDuration': 35, + 'settleBetweenScrollsTimeout': 5000, + 'maxScrolls': 30, + 'delta': 30, + } + assert arguments == expected_arguments + + @httpretty.activate + def test_inject_mock_image_with_file(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + success_qr_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'file', 'success_qr.png') + base64_encoded_image = encode_file_to_base64(success_qr_file_path) + flutter.inject_mock_image(success_qr_file_path) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: injectImage' + assert arguments == {'base64Image': base64_encoded_image} + + @httpretty.activate + def test_activate_injected_image(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + flutter.activate_injected_image('213476478') + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: activateInjectedImage' + assert arguments == {'imageId': '213476478'} diff --git a/test/unit/webdriver/flutter_integration/flutter_integration_driver_test.py b/test/unit/webdriver/flutter_integration/flutter_integration_driver_test.py new file mode 100644 index 00000000..0e97d81c --- /dev/null +++ b/test/unit/webdriver/flutter_integration/flutter_integration_driver_test.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +# 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. + +import json + +import httpretty + +from appium import webdriver +from appium.options.flutter_integration.base import FlutterOptions +from test.helpers.constants import SERVER_URL_BASE + + +class TestFlutterIntegrationDriver: + @httpretty.activate + def test_create_session(self): + # Set flutter options + flutterOptions = FlutterOptions() + flutterOptions.flutter_system_port = 9999 + flutterOptions.flutter_enable_mock_camera = True + flutterOptions.flutter_element_wait_timeout = 10000 + flutterOptions.flutter_server_launch_timeout = 120000 + + httpretty.register_uri( + httpretty.POST, + f'{SERVER_URL_BASE}/session', + body='{ "value": {"sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}} }', + ) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + } + driver = webdriver.Remote(SERVER_URL_BASE, options=flutterOptions.load_capabilities(desired_caps)) + + request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8')) + assert request_json.get('capabilities') is not None + assert request_json['capabilities']['alwaysMatch'] == { + 'platformName': 'Android', + 'appium:deviceName': 'Android Emulator', + 'appium:app': 'path/to/app', + 'appium:automationName': 'FlutterIntegration', + 'appium:flutterSystemPort': 9999, + 'appium:flutterEnableMockCamera': True, + 'appium:flutterElementWaitTimeout': 10000, + 'appium:flutterServerLaunchTimeout': 120000, + } + assert request_json.get('desiredCapabilities') is None + assert driver.session_id == 'session-id' diff --git a/test/unit/webdriver/flutter_integration/flutter_render_tree_test.py b/test/unit/webdriver/flutter_integration/flutter_render_tree_test.py new file mode 100644 index 00000000..1d11b5f8 --- /dev/null +++ b/test/unit/webdriver/flutter_integration/flutter_render_tree_test.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python + +# 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. + +import json + +import httpretty + +from appium.webdriver.extensions.flutter_integration.flutter_commands import ( + FlutterCommand, +) +from test.unit.helper.test_helper import ( + appium_command, + flutter_w3c_driver, + get_httpretty_request_body, +) + + +class TestFlutterRenderTree: + @httpretty.activate + def test_get_render_tree_with_all_filters(self): + expected_body = [{'': 'LoginButton'}] + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body=json.dumps({'value': expected_body}), + ) + + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + result = flutter.get_render_tree(widget_type='ElevatedButton', key='LoginButton', text='Login') + + request_body = get_httpretty_request_body(httpretty.last_request()) + assert request_body['script'] == 'flutter: renderTree' + assert request_body['args'] == [ + { + 'widgetType': 'ElevatedButton', + 'key': 'LoginButton', + 'text': 'Login', + } + ] + + assert result == expected_body + + @httpretty.activate + def test_get_render_tree_with_partial_filters(self): + expected_body = [{'': 'LoginScreen'}] + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body=json.dumps({'value': expected_body}), + ) + + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + result = flutter.get_render_tree(widget_type='LoginScreen') + + request_body = get_httpretty_request_body(httpretty.last_request()) + assert request_body['script'] == 'flutter: renderTree' + assert request_body['args'] == [{'widgetType': 'LoginScreen'}] + + assert result == expected_body + + @httpretty.activate + def test_get_render_tree_with_no_filters(self): + expected_body = [{'': 'RootWidget'}] + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body=json.dumps({'value': expected_body}), + ) + + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + result = flutter.get_render_tree() + + request_body = get_httpretty_request_body(httpretty.last_request()) + assert request_body['script'] == 'flutter: renderTree' + assert request_body['args'] == [{}] + + assert result == expected_body diff --git a/test/unit/webdriver/flutter_integration/flutter_search_context_test.py b/test/unit/webdriver/flutter_integration/flutter_search_context_test.py new file mode 100644 index 00000000..36e37843 --- /dev/null +++ b/test/unit/webdriver/flutter_integration/flutter_search_context_test.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from appium.webdriver.common.appiumby import AppiumBy +from test.unit.helper.test_helper import appium_command, flutter_w3c_driver, get_httpretty_request_body + + +class TestFlutterSearchContext(object): + @httpretty.activate + def test_find_element_by_flutter_key(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'Flutter UI Key') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter key' + assert d['value'] == 'Flutter UI Key' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_flutter_key(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = driver.find_elements(AppiumBy.FLUTTER_INTEGRATION_KEY, 'Flutter UI Key') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter key' + assert d['value'] == 'Flutter UI Key' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' + + @httpretty.activate + def test_find_element_by_flutter_text(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Flutter UI Text') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter text' + assert d['value'] == 'Flutter UI Text' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_flutter_text(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = driver.find_elements(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Flutter UI Text') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter text' + assert d['value'] == 'Flutter UI Text' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' + + @httpretty.activate + def test_find_element_by_flutter_semantics_label(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_SEMANTICS_LABEL, 'Flutter UI Semantics Label') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter semantics label' + assert d['value'] == 'Flutter UI Semantics Label' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_flutter_semantics_label(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = driver.find_elements(AppiumBy.FLUTTER_INTEGRATION_SEMANTICS_LABEL, 'Flutter UI Semantics Label') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter semantics label' + assert d['value'] == 'Flutter UI Semantics Label' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' + + @httpretty.activate + def test_find_element_by_flutter_type(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TYPE, 'Flutter UI Type') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter type' + assert d['value'] == 'Flutter UI Type' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_flutter_type(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = driver.find_elements(AppiumBy.FLUTTER_INTEGRATION_TYPE, 'Flutter UI Type') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter type' + assert d['value'] == 'Flutter UI Type' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' + + @httpretty.activate + def test_find_element_by_flutter_text_containing(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, 'Flutter UI Partial Text') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter text containing' + assert d['value'] == 'Flutter UI Partial Text' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_flutter_text_containing(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = driver.find_elements( + AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, + 'Flutter UI Partial Text', + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter text containing' + assert d['value'] == 'Flutter UI Partial Text' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' diff --git a/test/unit/webdriver/flutter_integration/flutter_waits_test.py b/test/unit/webdriver/flutter_integration/flutter_waits_test.py new file mode 100644 index 00000000..62f9f7c1 --- /dev/null +++ b/test/unit/webdriver/flutter_integration/flutter_waits_test.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from appium.webdriver.extensions.flutter_integration.flutter_commands import FlutterCommand +from appium.webdriver.extensions.flutter_integration.flutter_finder import FlutterFinder +from appium.webdriver.webelement import WebElement as MobileWebElement +from test.unit.helper.test_helper import appium_command, flutter_w3c_driver, get_httpretty_request_body + + +class TestFlutterWaits(object): + @httpretty.activate + def test_wait_for_visible_with_finder(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + finder = FlutterFinder.by_key('message_field') + flutter.wait_for_visible(finder, 5) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: waitForVisible' + expected_arguments = { + 'locator': {'using': '-flutter key', 'value': 'message_field'}, + 'timeout': 5, + } + assert arguments == expected_arguments + + @httpretty.activate + def test_wait_for_visible_with_webelement(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + element = MobileWebElement(driver, 'element_id') + flutter.wait_for_visible(element, 5) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: waitForVisible' + assert list(arguments['element'].values())[0] == 'element_id' + assert arguments['timeout'] == 5 + + @httpretty.activate + def test_wait_for_invisible_with_finder(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + message_field_finder = FlutterFinder.by_key('message_field') + flutter.wait_for_invisible(message_field_finder, 5) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: waitForAbsent' + expected_arguments = { + 'locator': {'using': '-flutter key', 'value': 'message_field'}, + 'timeout': 5, + } + assert arguments == expected_arguments + + @httpretty.activate + def test_wait_for_invisible_with_webelement(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + element = MobileWebElement(driver, 'element_id') + flutter.wait_for_invisible(element, 5) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: waitForAbsent' + assert list(arguments['element'].values())[0] == 'element_id' + assert arguments['timeout'] == 5 diff --git a/test/unit/webdriver/log_events_test.py b/test/unit/webdriver/log_events_test.py new file mode 100644 index 00000000..14d41442 --- /dev/null +++ b/test/unit/webdriver/log_events_test.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +# 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. + +import json + +import httpretty + +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import appium_command, get_httpretty_request_body, ios_w3c_driver + + +class TestWebDriverLogEvents(object): + @httpretty.activate + def test_get_events(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/events'), + body=json.dumps({'value': {'appium:funEvent': [12347]}}), + ) + events = driver.get_events() + assert events['appium:funEvent'] == [12347] + + d = get_httpretty_request_body(httpretty.last_request()) + assert 'type' not in d.keys() + + @httpretty.activate + def test_get_events_args(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/events'), + body=json.dumps({'value': {'appium:funEvent': [12347]}}), + ) + events_to_filter = ['appium:funEvent'] + events = driver.get_events(events_to_filter) + assert events['appium:funEvent'] == [12347] + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['type'] == events_to_filter + + @httpretty.activate + def test_log_event(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/log_event'), body='') + vendor_name = 'appium' + event_name = 'funEvent' + assert isinstance(driver.log_event(vendor_name, event_name), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['vendor'] == vendor_name + assert d['event'] == event_name diff --git a/test/unit/webdriver/log_test.py b/test/unit/webdriver/log_test.py new file mode 100644 index 00000000..f3b76d35 --- /dev/null +++ b/test/unit/webdriver/log_test.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +# 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. + +import json + +import httpretty + +from test.unit.helper.test_helper import appium_command, get_httpretty_request_body, ios_w3c_driver + + +@httpretty.activate +def test_get_log_types(): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/se/log/types'), + body=json.dumps({'value': ['syslog']}), + ) + log_types = driver.log_types + assert log_types == ['syslog'] + + +@httpretty.activate +def test_get_log(): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/se/log'), + body=json.dumps({'value': ['logs as array']}), + ) + log_types = driver.get_log('syslog') + assert log_types == ['logs as array'] + + d = get_httpretty_request_body(httpretty.last_request()) + assert {'type': 'syslog'} == d diff --git a/test/unit/webdriver/nativekey_test.py b/test/unit/webdriver/nativekey_test.py new file mode 100644 index 00000000..733f6159 --- /dev/null +++ b/test/unit/webdriver/nativekey_test.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +# 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. + + +from appium.webdriver.extensions.android.nativekey import AndroidKey + + +class TestAndroidKey: + def test_has_some_codes(self): + assert AndroidKey.ENTER == 66 + assert AndroidKey.BACK == 4 + assert AndroidKey.CAMERA == 27 + assert AndroidKey.SPACE == 62 + + def test_is_gamepad_key(self): + assert AndroidKey.is_gamepad_button(AndroidKey.BUTTON_8) + assert not AndroidKey.is_gamepad_button(250) + + def test_is_confirm_key(self): + assert AndroidKey.is_confirm_key(AndroidKey.SPACE) + assert not AndroidKey.is_confirm_key(21) + + def test_is_media_key(self): + assert AndroidKey.is_media_key(AndroidKey.MEDIA_PAUSE) + assert not AndroidKey.is_media_key(11) + + def test_is_system_key(self): + assert AndroidKey.is_system_key(AndroidKey.HEADSETHOOK) + assert not AndroidKey.is_system_key(21) + + def test_is_wake_key(self): + assert AndroidKey.is_wake_key(AndroidKey.MENU) + assert not AndroidKey.is_wake_key(32) diff --git a/test/unit/webdriver/network_test.py b/test/unit/webdriver/network_test.py new file mode 100644 index 00000000..a9cf7ea9 --- /dev/null +++ b/test/unit/webdriver/network_test.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python + +# 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. + + +import httpretty + +from appium.webdriver.extensions.android.network import NetSpeed +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebDriverNetwork(object): + @httpretty.activate + def test_network_connection(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890/network_connection'), body='{"value": 2}') + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"wifi": true, "data": false, "airplaneMode": false}}', + ) + assert driver.network_connection == 2 + + @httpretty.activate + def test_set_network_connection(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/network_connection'), body='{"value": ""}') + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"wifi": true, "data": false, "airplaneMode": false}}', + ) + driver.set_network_connection(2) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['args'][0]['wifi'] is True + + @httpretty.activate + def test_set_network_speed(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/network_speed'), + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + assert isinstance(driver.set_network_speed(NetSpeed.LTE), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['args'][0]['speed'] == NetSpeed.LTE + + @httpretty.activate + def test_toggle_wifi(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/toggle_wifi'), + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"wifi": true, "data": false, "airplaneMode": false}}', + ) + assert isinstance(driver.toggle_wifi(), WebDriver) diff --git a/test/unit/webdriver/performance_test.py b/test/unit/webdriver/performance_test.py new file mode 100644 index 00000000..d4534c90 --- /dev/null +++ b/test/unit/webdriver/performance_test.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebDriverPerformance(object): + @httpretty.activate + def test_get_performance_data(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/getPerformanceData'), + body='{"value": [["user", "kernel"], ["2.5", "1.3"]]}', + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": [["user", "kernel"], ["2.5", "1.3"]]}', + ) + assert driver.get_performance_data('my.app.package', 'cpuinfo', 5) == [['user', 'kernel'], ['2.5', '1.3']] + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['args'][0]['packageName'] == 'my.app.package' + assert d['args'][0]['dataType'] == 'cpuinfo' + + @httpretty.activate + def test_get_performance_data_types(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/performanceData/types'), + body='{"value": ["cpuinfo", "memoryinfo", "batteryinfo", "networkinfo"]}', + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": ["cpuinfo", "memoryinfo", "batteryinfo", "networkinfo"]}', + ) + assert driver.get_performance_data_types() == ['cpuinfo', 'memoryinfo', 'batteryinfo', 'networkinfo'] diff --git a/test/unit/webdriver/screen_record_test.py b/test/unit/webdriver/screen_record_test.py new file mode 100644 index 00000000..da470ceb --- /dev/null +++ b/test/unit/webdriver/screen_record_test.py @@ -0,0 +1,74 @@ +# +# 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. + +import httpretty + +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver + + +class TestWebDriverScreenRecordAndroid(object): + @httpretty.activate + def test_start_recording_screen(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/start_recording_screen'), + ) + assert driver.start_recording_screen(user='userA', password='12345') is None + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['options']['user'] == 'userA' + assert d['options']['pass'] == '12345' + assert 'password' not in d['options'].keys() + + @httpretty.activate + def test_stop_recording_screen(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/stop_recording_screen'), + body='{"value": "b64_video_data"}', + ) + assert driver.stop_recording_screen(user='userA', password='12345') == 'b64_video_data' + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['options']['user'] == 'userA' + assert d['options']['pass'] == '12345' + assert 'password' not in d['options'].keys() + + +class TestWebDriverScreenRecordIOS(object): + @httpretty.activate + def test_start_recording_screen(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/start_recording_screen'), + ) + assert driver.start_recording_screen(user='userA', password='12345') is None + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['options']['user'] == 'userA' + assert d['options']['pass'] == '12345' + assert 'password' not in d['options'].keys() + + @httpretty.activate + def test_stop_recording_screen(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/stop_recording_screen'), + body='{"value": "b64_video_data"}', + ) + assert driver.stop_recording_screen(user='userA', password='12345') == 'b64_video_data' + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['options']['user'] == 'userA' + assert d['options']['pass'] == '12345' + assert 'password' not in d['options'].keys() diff --git a/test/unit/webdriver/search_context/android_test.py b/test/unit/webdriver/search_context/android_test.py new file mode 100644 index 00000000..879d78f5 --- /dev/null +++ b/test/unit/webdriver/search_context/android_test.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python + +# 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. + +import json + +import httpretty + +from appium.webdriver.common.appiumby import AppiumBy +from appium.webdriver.webelement import WebElement as MobileWebElement +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebDriverAndroidSearchContext(object): + @httpretty.activate + def test_find_element_by_id(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element( + by=AppiumBy.ID, + value='id data', + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == 'id' + assert d['value'] == 'id data' + assert isinstance(el, MobileWebElement) + + @httpretty.activate + def test_find_elements_by_id(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "element-id2"}]}', + ) + els = driver.find_elements( + by=AppiumBy.ID, + value='id data', + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == 'id' + assert d['value'] == 'id data' + assert isinstance(els[0], MobileWebElement) + + @httpretty.activate + def test_find_child_element_by_id(self): + driver = android_w3c_driver() + element = MobileWebElement(driver, 'element_id') + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element/element_id/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "child-element-id"}}', + ) + el = element.find_element( + by=AppiumBy.ID, + value='id data', + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == 'id' + assert d['value'] == 'id data' + assert isinstance(el, MobileWebElement) + + @httpretty.activate + def test_find_element_by_android_data_matcher(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element( + by=AppiumBy.ANDROID_DATA_MATCHER, + value=json.dumps({'name': 'title', 'args': ['title', 'Animation'], 'class': 'class name'}), + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-android datamatcher' + value_dict = json.loads(d['value']) + assert value_dict['args'] == ['title', 'Animation'] + assert value_dict['name'] == 'title' + assert value_dict['class'] == 'class name' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_android_data_matcher(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "element-id2"}]}', + ) + els = driver.find_elements( + by=AppiumBy.ANDROID_DATA_MATCHER, value=json.dumps({'name': 'title', 'args': ['title', 'Animation']}) + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-android datamatcher' + value_dict = json.loads(d['value']) + assert value_dict['args'] == ['title', 'Animation'] + assert value_dict['name'] == 'title' + assert els[0].id == 'element-id1' + assert els[1].id == 'element-id2' + + @httpretty.activate + def test_find_elements_by_android_data_matcher_no_value(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/elements'), body='{"value": []}') + els = driver.find_elements(by=AppiumBy.ANDROID_DATA_MATCHER, value='{}') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-android datamatcher' + assert d['value'] == '{}' + assert len(els) == 0 + + @httpretty.activate + def test_find_child_element_by_android_data_matcher(self): + driver = android_w3c_driver() + element = MobileWebElement(driver, 'element_id') + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element/element_id/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "child-element-id"}}', + ) + el = element.find_element( + by=AppiumBy.ANDROID_DATA_MATCHER, + value=json.dumps({'name': 'title', 'args': ['title', 'Animation'], 'class': 'class name'}), + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-android datamatcher' + value_dict = json.loads(d['value']) + assert value_dict['args'] == ['title', 'Animation'] + assert value_dict['name'] == 'title' + assert value_dict['class'] == 'class name' + assert el.id == 'child-element-id' + + @httpretty.activate + def test_find_child_elements_by_android_data_matcher(self): + driver = android_w3c_driver() + element = MobileWebElement(driver, 'element_id') + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element/element_id/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = element.find_elements( + by=AppiumBy.ANDROID_DATA_MATCHER, value=json.dumps({'name': 'title', 'args': ['title', 'Animation']}) + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-android datamatcher' + value_dict = json.loads(d['value']) + assert value_dict['args'] == ['title', 'Animation'] + assert value_dict['name'] == 'title' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' + + @httpretty.activate + def test_find_child_elements_by_android_data_matcher_no_value(self): + driver = android_w3c_driver() + element = MobileWebElement(driver, 'element_id') + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/element/element_id/elements'), body='{"value": []}' + ) + els = element.find_elements(by=AppiumBy.ANDROID_DATA_MATCHER, value='{}') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-android datamatcher' + assert d['value'] == '{}' + assert len(els) == 0 diff --git a/test/unit/webdriver/search_context/ios_test.py b/test/unit/webdriver/search_context/ios_test.py new file mode 100644 index 00000000..d6b288a4 --- /dev/null +++ b/test/unit/webdriver/search_context/ios_test.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from appium.webdriver.common.appiumby import AppiumBy +from appium.webdriver.webelement import WebElement as MobileWebElement +from test.unit.helper.test_helper import appium_command, get_httpretty_request_body, ios_w3c_driver + + +class TestWebDriverIOSSearchContext(object): + @httpretty.activate + def test_find_element_by_ios_predicate(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(by=AppiumBy.IOS_PREDICATE, value='wdName == "UIKitCatalog"') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-ios predicate string' + assert d['value'] == 'wdName == "UIKitCatalog"' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_ios_predicate(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "element-id2"}]}', + ) + els = driver.find_elements(by=AppiumBy.IOS_PREDICATE, value='wdName == "UIKitCatalog"') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-ios predicate string' + assert d['value'] == 'wdName == "UIKitCatalog"' + assert els[0].id == 'element-id1' + assert els[1].id == 'element-id2' + + @httpretty.activate + def test_find_child_elements_by_ios_predicate(self): + driver = ios_w3c_driver() + element = MobileWebElement(driver, 'element_id') + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element/element_id/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = element.find_elements(by=AppiumBy.IOS_PREDICATE, value='wdName == "UIKitCatalog"') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-ios predicate string' + assert d['value'] == 'wdName == "UIKitCatalog"' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' + + @httpretty.activate + def test_find_element_by_ios_class_chain(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(by=AppiumBy.IOS_CLASS_CHAIN, value='**/XCUIElementTypeStaticText') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-ios class chain' + assert d['value'] == '**/XCUIElementTypeStaticText' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_ios_class_chain(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "element-id2"}]}', + ) + els = driver.find_elements(by=AppiumBy.IOS_CLASS_CHAIN, value='**/XCUIElementTypeStaticText') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-ios class chain' + assert d['value'] == '**/XCUIElementTypeStaticText' + assert els[0].id == 'element-id1' + assert els[1].id == 'element-id2' + + @httpretty.activate + def test_find_child_elements_by_ios_class_chain(self): + driver = ios_w3c_driver() + element = MobileWebElement(driver, 'element_id') + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element/element_id/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = element.find_elements(by=AppiumBy.IOS_CLASS_CHAIN, value='**/XCUIElementTypeStaticText') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-ios class chain' + assert d['value'] == '**/XCUIElementTypeStaticText' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' diff --git a/test/unit/webdriver/settings_test.py b/test/unit/webdriver/settings_test.py new file mode 100644 index 00000000..de9a0e00 --- /dev/null +++ b/test/unit/webdriver/settings_test.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +# 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. + +import httpretty + +from appium.webdriver.webdriver import WebDriver +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebDriverSettings(object): + @httpretty.activate + def test_get_settings_bool(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.GET, appium_command('/session/1234567890/appium/settings'), body='{"value": {"sample": true}}' + ) + assert driver.get_settings()['sample'] is True + + @httpretty.activate + def test_update_settings_bool(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/settings'), + ) + assert isinstance(driver.update_settings({'sample': True}), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['settings']['sample'] is True + + @httpretty.activate + def test_get_settings_string(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.GET, appium_command('/session/1234567890/appium/settings'), body='{"value": {"sample": "string"}}' + ) + assert driver.get_settings()['sample'] == 'string' + + @httpretty.activate + def test_update_settings_string(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/settings'), + ) + assert isinstance(driver.update_settings({'sample': 'string'}), WebDriver) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['settings']['sample'] == 'string' diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py new file mode 100644 index 00000000..443885d5 --- /dev/null +++ b/test/unit/webdriver/webdriver_test.py @@ -0,0 +1,534 @@ +#!/usr/bin/env python + +# 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. + +import json + +import httpretty +import urllib3 +from mock import patch + +from appium import webdriver +from appium.options.android import UiAutomator2Options +from appium.webdriver.appium_connection import AppiumConnection +from appium.webdriver.client_config import AppiumClientConfig +from appium.webdriver.webdriver import ExtensionBase, WebDriver, _get_remote_connection_and_client_config +from test.helpers.constants import SERVER_URL_BASE +from test.unit.helper.test_helper import ( + android_w3c_driver, + appium_command, + get_httpretty_request_body, + ios_w3c_driver, + ios_w3c_driver_with_extensions, +) + + +class TestWebDriverWebDriver: + @httpretty.activate + def test_create_session(self): + httpretty.register_uri( + httpretty.POST, + f'{SERVER_URL_BASE}/session', + body='{ "value": {"sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}} }', + ) + + desired_caps = { + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + } + driver = webdriver.Remote(SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps)) + + # This tests counts the same request twice on Azure only for now (around 20th May, 2021). Local running works. + # Should investigate the cause. + # assert len(httpretty.HTTPretty.latest_requests) == 1 + + request = httpretty.HTTPretty.latest_requests[0] + assert request.headers['content-type'] == 'application/json;charset=UTF-8' + assert request.headers['user-agent'].startswith('appium/') + assert '(selenium/' in request.headers['user-agent'] + + request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8')) + assert request_json.get('capabilities') is not None + assert request_json['capabilities']['alwaysMatch'] == { + 'platformName': 'Android', + 'appium:deviceName': 'Android Emulator', + 'appium:app': 'path/to/app', + 'appium:automationName': 'UIAutomator2', + } + assert request_json.get('desiredCapabilities') is None + + assert driver.session_id == 'session-id' + + @httpretty.activate + def test_create_session_change_session_id(self): + httpretty.register_uri( + httpretty.POST, + f'{SERVER_URL_BASE}/session', + body='{ "sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"} }', + ) + + httpretty.register_uri( + httpretty.GET, + f'{SERVER_URL_BASE}/session/another-session-id/title', + body='{ "value": "title on another session id"}', + ) + + options = UiAutomator2Options().set_capability('deviceName', 'Android Emulator').set_capability('app', 'path/to/app') + driver = webdriver.Remote(SERVER_URL_BASE, options=options) + + # current session + assert driver.session_id == 'session-id' + + # call against another session id + driver.session_id = 'another-session-id' + assert driver.title == 'title on another session id' + assert driver.session_id == 'another-session-id' + + @httpretty.activate + def test_create_session_register_uridirect(self): + httpretty.register_uri( + httpretty.POST, + f'{SERVER_URL_BASE}/session', + body=json.dumps( + { + 'sessionId': 'session-id', + 'capabilities': { + 'deviceName': 'Android Emulator', + 'directConnectProtocol': 'http', + 'directConnectHost': 'localhost2', + 'directConnectPort': 4800, + 'directConnectPath': '/special/path/wd/hub', + }, + } + ), + ) + + httpretty.register_uri( + httpretty.GET, + 'http://localhost2:4800/special/path/wd/hub/session/session-id/contexts', + body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}), + ) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + 'automationName': 'UIAutomator2', + } + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True) + driver = webdriver.Remote( + SERVER_URL_BASE, + options=UiAutomator2Options().load_capabilities(desired_caps), + client_config=client_config, + ) + + assert 'http://localhost2:4800/special/path/wd/hub' == driver.command_executor._client_config.remote_server_addr + assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts + assert isinstance(driver.command_executor, AppiumConnection) + + @httpretty.activate + def test_create_session_register_uridirect_no_direct_connect_path(self): + httpretty.register_uri( + httpretty.POST, + f'{SERVER_URL_BASE}/session', + body=json.dumps( + { + 'sessionId': 'session-id', + 'capabilities': { + 'deviceName': 'Android Emulator', + 'directConnectProtocol': 'http', + 'directConnectHost': 'localhost2', + 'directConnectPort': 4800, + }, + } + ), + ) + + httpretty.register_uri( + httpretty.GET, + f'{SERVER_URL_BASE}/session/session-id/contexts', + body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}), + ) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + 'automationName': 'UIAutomator2', + } + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True) + driver = webdriver.Remote( + SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps), client_config=client_config + ) + + assert SERVER_URL_BASE == driver.command_executor._client_config.remote_server_addr + assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts + assert isinstance(driver.command_executor, AppiumConnection) + + @httpretty.activate + def test_create_session_remote_server_addr_treatment_with_appiumclientconfig(self): + # remote server add in AppiumRemoteCong will be prior than the string of 'command_executor' + # as same as Selenium behavior. + httpretty.register_uri( + httpretty.POST, + f'{SERVER_URL_BASE}/session', + body=json.dumps( + { + 'sessionId': 'session-id', + 'capabilities': { + 'deviceName': 'Android Emulator', + }, + } + ), + ) + + httpretty.register_uri( + httpretty.GET, + f'{SERVER_URL_BASE}/session/session-id/contexts', + body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}), + ) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + 'automationName': 'UIAutomator2', + } + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True) + driver = webdriver.Remote( + 'http://localhost:8080/something/path', + options=UiAutomator2Options().load_capabilities(desired_caps), + client_config=client_config, + ) + + assert SERVER_URL_BASE == driver.command_executor._client_config.remote_server_addr + assert isinstance(driver.command_executor, AppiumConnection) + + @httpretty.activate + def test_get_events(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890'), + body=json.dumps({'value': {'events': {'simStarted': [1234567890]}}}), + ) + events = driver.events + assert events['simStarted'] == [1234567890] + + @httpretty.activate + def test_get_events_catches_missing_events(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890'), body=json.dumps({'value': {}})) + events = driver.events + assert events == {} + httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890'), body=json.dumps({})) + events = driver.events + assert events == {} + + @httpretty.activate + @patch('appium.webdriver.webdriver.logger.warning') + def test_session_catches_error(self, mock_warning): + def exceptionCallback(request, uri, headers): + raise Exception() + + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890'), body=exceptionCallback) + events = driver.events + assert events == {} + + @httpretty.activate + def test_add_command(self): + class CustomURLCommand(ExtensionBase): + def method_name(self): + return 'test_command' + + def test_command(self): + return self.execute()['value'] + + def add_command(self): + return 'get', '/session/$sessionId/path/to/custom/url' + + driver = ios_w3c_driver_with_extensions([CustomURLCommand]) + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/path/to/custom/url'), + body=json.dumps({'value': {}}), + ) + result = driver.test_command() + + assert result == {} + driver.delete_extensions() + + @httpretty.activate + def test_add_command_body(self): + class CustomURLCommand(ExtensionBase): + def method_name(self): + return 'test_command' + + def test_command(self, argument): + return self.execute(argument)['value'] + + def add_command(self): + return 'post', '/session/$sessionId/path/to/custom/url' + + driver = ios_w3c_driver_with_extensions([CustomURLCommand]) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/path/to/custom/url'), + body=json.dumps({'value': {}}), + ) + result = driver.test_command({'dummy': 'test argument'}) + assert result == {} + + d = get_httpretty_request_body(httpretty.last_request()) + + assert d['dummy'] == 'test argument' + driver.delete_extensions() + + @httpretty.activate + def test_add_command_with_element_id(self): + class CustomURLCommand(ExtensionBase): + def method_name(self): + return 'test_command' + + def test_command(self, element_id): + return self.execute({'id': element_id})['value'] + + def add_command(self): + return 'GET', '/session/$sessionId/path/to/custom/$id/url' + + driver = ios_w3c_driver_with_extensions([CustomURLCommand]) + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/path/to/custom/element_id/url'), + body=json.dumps({'value': {}}), + ) + result = driver.test_command('element_id') + assert result == {} + driver.delete_extensions() + + @httpretty.activate + def test_create_session_with_custom_connection(self): + httpretty.register_uri( + httpretty.POST, + f'{SERVER_URL_BASE}/session', + body='{ "value": {"sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}} }', + ) + + desired_caps = { + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + } + + class CustomAppiumConnection(AppiumConnection): + # To explicitly check if the given executor is used + pass + + init_args_for_pool_manager = {'retries': urllib3.util.retry.Retry(total=3, connect=3, read=False)} + custom_appium_connection = CustomAppiumConnection( + remote_server_addr=SERVER_URL_BASE, init_args_for_pool_manager=init_args_for_pool_manager + ) + + driver = webdriver.Remote(custom_appium_connection, options=UiAutomator2Options().load_capabilities(desired_caps)) + + request = httpretty.HTTPretty.latest_requests[0] + assert request.headers['content-type'] == 'application/json;charset=UTF-8' + assert request.headers['user-agent'].startswith('appium/') + assert '(selenium/' in request.headers['user-agent'] + + request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8')) + assert request_json.get('capabilities') is not None + assert request_json['capabilities']['alwaysMatch'] == { + 'platformName': 'Android', + 'appium:deviceName': 'Android Emulator', + 'appium:app': 'path/to/app', + 'appium:automationName': 'UIAutomator2', + } + assert request_json.get('desiredCapabilities') is None + assert driver.session_id == 'session-id' + + assert isinstance(driver.command_executor, CustomAppiumConnection) + + @httpretty.activate + def test_create_session_with_custom_connection_with_keepalive(self): + httpretty.register_uri( + httpretty.POST, + f'{SERVER_URL_BASE}/session', + body='{ "value": {"sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}} }', + ) + + desired_caps = { + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + } + + class CustomAppiumConnection(AppiumConnection): + # To explicitly check if the given executor is used + pass + + init_args_for_pool_manager = {'retries': urllib3.util.retry.Retry(total=3, connect=3, read=False)} + custom_appium_connection = CustomAppiumConnection( + # keep alive has different route to set init args for the pool manager + keep_alive=True, + remote_server_addr=SERVER_URL_BASE, + init_args_for_pool_manager=init_args_for_pool_manager, + ) + + driver = webdriver.Remote(custom_appium_connection, options=UiAutomator2Options().load_capabilities(desired_caps)) + + request = httpretty.HTTPretty.latest_requests[0] + assert request.headers['content-type'] == 'application/json;charset=UTF-8' + assert request.headers['user-agent'].startswith('appium/') + assert '(selenium/' in request.headers['user-agent'] + + request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8')) + assert request_json.get('capabilities') is not None + assert request_json['capabilities']['alwaysMatch'] == { + 'platformName': 'Android', + 'appium:deviceName': 'Android Emulator', + 'appium:app': 'path/to/app', + 'appium:automationName': 'UIAutomator2', + } + assert request_json.get('desiredCapabilities') is None + assert driver.session_id == 'session-id' + + assert isinstance(driver.command_executor, CustomAppiumConnection) + + @httpretty.activate + def test_extention_command_check(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') + assert ( + driver.execute_script( + 'mobile: startActivity', + {'component': 'io.appium.android.apis/.accessibility.AccessibilityNodeProviderActivity'}, + ) + is True + ) + assert { + 'args': [{'component': 'io.appium.android.apis/.accessibility.AccessibilityNodeProviderActivity'}], + 'script': 'mobile: startActivity', + } == get_httpretty_request_body(httpretty.last_request()) + + def test_get_client_config_and_connection_with_empty_config(self): + command_executor, client_config = _get_remote_connection_and_client_config( + command_executor='http://127.0.0.1:4723', client_config=None + ) + + assert isinstance(command_executor, AppiumConnection) + assert command_executor._client_config == client_config + assert isinstance(client_config, AppiumClientConfig) + assert client_config.remote_server_addr == 'http://127.0.0.1:4723' + + def test_get_client_config_and_connection(self): + command_executor, client_config = _get_remote_connection_and_client_config( + command_executor='http://127.0.0.1:4723', + client_config=AppiumClientConfig(remote_server_addr='http://127.0.0.1:4723/wd/hub'), + ) + + assert isinstance(command_executor, AppiumConnection) + # the client config in the command_executor is the given client config. + assert command_executor._client_config == client_config + assert isinstance(client_config, AppiumClientConfig) + assert client_config.remote_server_addr == 'http://127.0.0.1:4723/wd/hub' + + def test_get_client_config_and_connection_custom_appium_connection(self): + c_config = AppiumClientConfig(remote_server_addr='http://127.0.0.1:4723') + appium_connection = AppiumConnection(client_config=c_config) + + command_executor, client_config = _get_remote_connection_and_client_config( + command_executor=appium_connection, client_config=AppiumClientConfig(remote_server_addr='http://127.0.0.1:4723') + ) + + assert isinstance(command_executor, AppiumConnection) + # client config already defined in the command_executor will be used. + assert command_executor._client_config != client_config + assert client_config is None + + +class SubWebDriver(WebDriver): + def __init__(self, command_executor, options=None): + super().__init__( + command_executor=command_executor, + options=options, + ) + + +class SubSubWebDriver(SubWebDriver): + def __init__(self, command_executor, options=None): + super().__init__( + command_executor=command_executor, + options=options, + ) + + +class TestSubModuleWebDriver(object): + def android_w3c_driver(self, driver_class): + response_body_json = json.dumps( + { + 'sessionId': '1234567890', + 'capabilities': { + 'platform': 'LINUX', + 'desired': { + 'platformName': 'Android', + 'automationName': 'uiautomator2', + 'platformVersion': '7.1.1', + 'deviceName': 'Android Emulator', + 'app': '/test/apps/ApiDemos-debug.apk', + }, + 'platformName': 'Android', + 'automationName': 'uiautomator2', + 'platformVersion': '7.1.1', + 'deviceName': 'emulator-5554', + 'app': '/test/apps/ApiDemos-debug.apk', + 'deviceUDID': 'emulator-5554', + 'appPackage': 'io.appium.android.apis', + 'appWaitPackage': 'io.appium.android.apis', + 'appActivity': 'io.appium.android.apis.ApiDemos', + 'appWaitActivity': 'io.appium.android.apis.ApiDemos', + }, + } + ) + + httpretty.register_uri(httpretty.POST, appium_command('/session'), body=response_body_json) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + 'automationName': 'UIAutomator2', + } + + driver = driver_class(SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps)) + return driver + + @httpretty.activate + def test_clipboard_with_subclass(self): + driver = self.android_w3c_driver(SubWebDriver) + httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890/context'), body='{"value": "NATIVE"}') + assert driver.current_context == 'NATIVE' + + @httpretty.activate + def test_clipboard_with_subsubclass(self): + driver = self.android_w3c_driver(SubSubWebDriver) + httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890/context'), body='{"value": "NATIVE"}') + assert driver.current_context == 'NATIVE' + + @httpretty.activate + def test_compare_commands(self): + driver_base = android_w3c_driver() + driver_sub = self.android_w3c_driver(SubWebDriver) + driver_subsub = self.android_w3c_driver(SubSubWebDriver) + + assert len(driver_base.command_executor._commands) == len(driver_sub.command_executor._commands) + assert len(driver_base.command_executor._commands) == len(driver_subsub.command_executor._commands) diff --git a/test/unit/webdriver/webelement_test.py b/test/unit/webdriver/webelement_test.py new file mode 100644 index 00000000..23268c3a --- /dev/null +++ b/test/unit/webdriver/webelement_test.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python + +# 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. + +import json +import tempfile + +import httpretty + +from appium.webdriver.webelement import WebElement as MobileWebElement +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestWebElement(object): + @httpretty.activate + def test_status(self): + driver = android_w3c_driver() + response = {'ready': True, 'message': {'build': {'version': '2.0.0', 'revision': None}}} + httpretty.register_uri( + httpretty.GET, + appium_command('/status'), + body=json.dumps({'value': response}), + ) + s = driver.get_status() + + assert s == response + + @httpretty.activate + def test_send_key(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/element/element_id/value')) + + element = MobileWebElement(driver, 'element_id') + element.send_keys('happy testing') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['text'] == ''.join(d['value']) + + @httpretty.activate + def test_send_key_with_file(self): + driver = android_w3c_driver() + # Should not send this file + tmp_f = tempfile.NamedTemporaryFile() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/element/element_id/value')) + + try: + element = MobileWebElement(driver, 'element_id') + element.send_keys(tmp_f.name) + finally: + tmp_f.close() + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['text'] == ''.join(d['value']) + + @httpretty.activate + def test_clear(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/element/element_id/clear')) + + element = MobileWebElement(driver, 'element_id') + element.clear() + + @httpretty.activate + def test_get_attribute_with_dict(self): + driver = android_w3c_driver() + rect_dict = {'y': 200, 'x': 100, 'width': 300, 'height': 56} + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/element/element_id/attribute/rect'), + body=json.dumps({'value': rect_dict}), + ) + + element = MobileWebElement(driver, 'element_id') + ef = element.get_attribute('rect') + + httpretty.last_request() + + assert isinstance(ef, dict) + assert ef == rect_dict + + @httpretty.activate + def test_element_location_in_view(self): + driver = android_w3c_driver() + location_in_view = {'y': 200, 'x': 100} + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/element/element_id/location_in_view'), + body=json.dumps({'value': location_in_view}), + ) + + element = MobileWebElement(driver, 'element_id') + loc = element.location_in_view + + httpretty.last_request() + + assert loc == location_in_view diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..edea8255 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2314 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "appium-python-client" +version = "5.2.6" +source = { editable = "." } +dependencies = [ + { name = "selenium", version = "4.36.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "selenium", version = "4.40.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] + +[package.dev-dependencies] +dev = [ + { name = "httpretty" }, + { name = "mock" }, + { name = "mypy" }, + { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, + { name = "python-dateutil" }, + { name = "python-semantic-release" }, + { name = "ruff" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-rtd-theme" }, + { name = "sphinxcontrib-apidoc" }, + { name = "types-python-dateutil" }, +] + +[package.metadata] +requires-dist = [ + { name = "selenium", specifier = ">=4.26,<5.0" }, + { name = "typing-extensions", specifier = "~=4.13" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "httpretty", specifier = "~=1.1" }, + { name = "mock", specifier = "~=5.2" }, + { name = "mypy", specifier = "~=1.17" }, + { name = "pre-commit", specifier = "~=4.2" }, + { name = "pytest", specifier = "~=8.4" }, + { name = "pytest-cov", specifier = ">=6.2,<8.0" }, + { name = "pytest-xdist", specifier = "~=3.8" }, + { name = "python-dateutil", specifier = "~=2.9" }, + { name = "python-semantic-release", specifier = ">=10.3.1,<10.6.0" }, + { name = "ruff", specifier = "~=0.12" }, + { name = "sphinx", specifier = ">=4.0,<9.0" }, + { name = "sphinx-rtd-theme", specifier = "~=3.0" }, + { name = "sphinxcontrib-apidoc", specifier = "~=0.6" }, + { name = "types-python-dateutil", specifier = "~=2.9" }, +] + +[[package]] +name = "async-generator" +version = "1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/b6/6fa6b3b598a03cba5e80f829e0dadbb49d7645f523d209b2fb7ea0bbb02a/async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144", size = 29870, upload-time = "2018-08-01T03:36:21.69Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/52/39d20e03abd0ac9159c162ec24b93fbcaa111e8400308f2465432495ca2b/async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", size = 18857, upload-time = "2018-08-01T03:36:20.029Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click-option-group" +version = "0.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/ff/d291d66595b30b83d1cb9e314b2c9be7cfc7327d4a0d40a15da2416ea97b/click_option_group-0.5.9.tar.gz", hash = "sha256:f94ed2bc4cf69052e0f29592bd1e771a1789bd7bfc482dd0bc482134aff95823", size = 22222, upload-time = "2025-10-09T09:38:01.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/45/54bb2d8d4138964a94bef6e9afe48b0be4705ba66ac442ae7d8a8dc4ffef/click_option_group-0.5.9-py3-none-any.whl", hash = "sha256:ad2599248bd373e2e19bec5407967c3eec1d0d4fc4a5e77b08a0481e75991080", size = 11553, upload-time = "2025-10-09T09:38:00.066Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "dotty-dict" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/ab/88d67f02024700b48cd8232579ad1316aa9df2272c63049c27cc094229d6/dotty_dict-1.3.1.tar.gz", hash = "sha256:4b016e03b8ae265539757a53eba24b9bfda506fb94fbce0bee843c6f05541a15", size = 7699, upload-time = "2022-07-09T18:50:57.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/91/e0d457ee03ec33d79ee2cd8d212debb1bc21dfb99728ae35efdb5832dc22/dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f", size = 7014, upload-time = "2022-07-09T18:50:55.058Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.46" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpretty" +version = "1.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/19/850b7ed736319d0c4088581f4fc34f707ef14461947284026664641e16d4/httpretty-1.1.4.tar.gz", hash = "sha256:20de0e5dd5a18292d36d928cc3d6e52f8b2ac73daec40d41eb62dee154933b68", size = 442389, upload-time = "2021-08-16T19:35:31.4Z" } + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + +[[package]] +name = "identify" +version = "2.6.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "librt" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, + { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, + { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, + { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, + { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/2668bb01f568bc89ace53736df950845f8adfcacdf6da087d5cef12110cb/librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6", size = 56680, upload-time = "2026-01-14T12:56:02.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d4/dbb3edf2d0ec4ba08dcaf1865833d32737ad208962d4463c022cea6e9d3c/librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b", size = 58612, upload-time = "2026-01-14T12:56:03.616Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/64b029de4ac9901fcd47832c650a0fd050555a452bd455ce8deddddfbb9f/librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c", size = 163654, upload-time = "2026-01-14T12:56:04.975Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/95e2abb1b48eb8f8c7fc2ae945321a6b82777947eb544cc785c3f37165b2/librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5", size = 172477, upload-time = "2026-01-14T12:56:06.103Z" }, + { url = "https://files.pythonhosted.org/packages/7e/27/9bdf12e05b0eb089dd008d9c8aabc05748aad9d40458ade5e627c9538158/librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71", size = 186220, upload-time = "2026-01-14T12:56:09.958Z" }, + { url = "https://files.pythonhosted.org/packages/53/6a/c3774f4cc95e68ed444a39f2c8bd383fd18673db7d6b98cfa709f6634b93/librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e", size = 183841, upload-time = "2026-01-14T12:56:11.109Z" }, + { url = "https://files.pythonhosted.org/packages/58/6b/48702c61cf83e9c04ad5cec8cad7e5e22a2cde23a13db8ef341598897ddd/librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63", size = 179751, upload-time = "2026-01-14T12:56:12.278Z" }, + { url = "https://files.pythonhosted.org/packages/35/87/5f607fc73a131d4753f4db948833063c6aad18e18a4e6fbf64316c37ae65/librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94", size = 199319, upload-time = "2026-01-14T12:56:13.425Z" }, + { url = "https://files.pythonhosted.org/packages/6e/cc/b7c5ac28ae0f0645a9681248bae4ede665bba15d6f761c291853c5c5b78e/librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb", size = 43434, upload-time = "2026-01-14T12:56:14.781Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5d/dce0c92f786495adf2c1e6784d9c50a52fb7feb1cfb17af97a08281a6e82/librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be", size = 49801, upload-time = "2026-01-14T12:56:15.827Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mock" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/8c/14c2ae915e5f9dca5a22edd68b35be94400719ccfa068a03e0fb63d0f6f6/mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0", size = 92796, upload-time = "2025-03-03T12:31:42.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/d9/617e6af809bf3a1d468e0d58c3997b1dc219a9a9202e650d30c2fc85d481/mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f", size = 31617, upload-time = "2025-03-03T12:31:41.518Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "pbr" +version = "7.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/ab/1de9a4f730edde1bdbbc2b8d19f8fa326f036b4f18b2f72cfbea7dc53c26/pbr-7.0.3.tar.gz", hash = "sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29", size = 135625, upload-time = "2025-11-03T17:04:56.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/db/61efa0d08a99f897ef98256b03e563092d36cc38dc4ebe4a85020fe40b31/pbr-7.0.3-py2.py3-none-any.whl", hash = "sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b", size = 131898, upload-time = "2025-11-03T17:04:54.875Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "cfgv", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "identify", version = "2.6.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "nodeenv", marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "virtualenv", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "cfgv", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "identify", version = "2.6.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "nodeenv", marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "virtualenv", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, + { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, + { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, + { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-gitlab" +version = "6.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "requests-toolbelt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/bd/b30f1d3b303cb5d3c72e2d57a847d699e8573cbdfd67ece5f1795e49da1c/python_gitlab-6.5.0.tar.gz", hash = "sha256:97553652d94b02de343e9ca92782239aa2b5f6594c5482331a9490d9d5e8737d", size = 400591, upload-time = "2025-10-17T21:40:02.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/bd/b0d440685fbcafee462bed793a74aea88541887c4c30556a55ac64914b8d/python_gitlab-6.5.0-py3-none-any.whl", hash = "sha256:494e1e8e5edd15286eaf7c286f3a06652688f1ee20a49e2a0218ddc5cc475e32", size = 144419, upload-time = "2025-10-17T21:40:01.233Z" }, +] + +[[package]] +name = "python-semantic-release" +version = "10.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "click-option-group" }, + { name = "deprecated" }, + { name = "dotty-dict" }, + { name = "gitpython" }, + { name = "importlib-resources" }, + { name = "jinja2" }, + { name = "pydantic" }, + { name = "python-gitlab" }, + { name = "requests" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/3a/7332b822825ed0e902c6e950e0d1e90e8f666fd12eb27855d1c8b6677eff/python_semantic_release-10.5.3.tar.gz", hash = "sha256:de4da78635fa666e5774caaca2be32063cae72431eb75e2ac23b9f2dfd190785", size = 618034, upload-time = "2025-12-14T22:37:29.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/01/ada29a1215df601bded0a2efd3b6d53864a0a9e0a9ea52aeaebe14fd03fd/python_semantic_release-10.5.3-py3-none-any.whl", hash = "sha256:1be0e07c36fa1f1ec9da4f438c1f6bbd7bc10eb0d6ac0089b0643103708c2823", size = 152716, upload-time = "2025-12-14T22:37:28.089Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "roman-numerals-py" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "roman-numerals", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/de96fca640f4f656eb79bbee0e79aeec52e3e0e359f8a3e6a0d366378b64/roman_numerals_py-4.1.0.tar.gz", hash = "sha256:f5d7b2b4ca52dd855ef7ab8eb3590f428c0b1ea480736ce32b01fef2a5f8daf9", size = 4274, upload-time = "2025-12-17T18:25:41.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/2c/daca29684cbe9fd4bc711f8246da3c10adca1ccc4d24436b17572eb2590e/roman_numerals_py-4.1.0-py3-none-any.whl", hash = "sha256:553114c1167141c1283a51743759723ecd05604a1b6b507225e91dc1a6df0780", size = 4547, upload-time = "2025-12-17T18:25:40.136Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, + { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, + { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, +] + +[[package]] +name = "selenium" +version = "4.36.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.10'" }, + { name = "trio", version = "0.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "trio-websocket", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "urllib3", extra = ["socks"], marker = "python_full_version < '3.10'" }, + { name = "websocket-client", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/35/33d3d84e3399c9d00b489aeccfdc78115e149e45816fb8fe84274329e8a2/selenium-4.36.0.tar.gz", hash = "sha256:0eced83038736c3a013b824116df0b6dbb83e93721545f51b680451013416723", size = 913613, upload-time = "2025-10-02T15:24:37.483Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/9e/642a355e43a4ebf68bc4f00dd4ab264f635079c5dc7ed6d9991a0c2be3d7/selenium-4.36.0-py3-none-any.whl", hash = "sha256:525fdfe96b99c27d9a2c773c75aa7413f4c24bdb7b9749c1950aa3b5f79ed915", size = 9587029, upload-time = "2025-10-02T15:24:35.025Z" }, +] + +[[package]] +name = "selenium" +version = "4.40.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.10'" }, + { name = "trio", version = "0.32.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "trio-typing", marker = "python_full_version >= '3.10'" }, + { name = "trio-websocket", marker = "python_full_version >= '3.10'" }, + { name = "types-certifi", marker = "python_full_version >= '3.10'" }, + { name = "types-urllib3", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "urllib3", extra = ["socks"], marker = "python_full_version >= '3.10'" }, + { name = "websocket-client", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/ef/a5727fa7b33d20d296322adf851b76072d8d3513e1b151969d3228437faf/selenium-4.40.0.tar.gz", hash = "sha256:a88f5905d88ad0b84991c2386ea39e2bbde6d6c334be38df5842318ba98eaa8c", size = 930444, upload-time = "2026-01-18T23:12:31.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/74/eb9d6540aca1911106fa0877b8e9ef24171bc18857937a6b0ffe0586c623/selenium-4.40.0-py3-none-any.whl", hash = "sha256:c8823fc02e2c771d9ad9a0cf899cee7de1a57a6697e3d0b91f67566129f2b729", size = 9608184, upload-time = "2026-01-18T23:12:29.435Z" }, +] + +[[package]] +name = "setuptools" +version = "80.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/ff/f75651350db3cf2ef767371307eb163f3cc1ac03e16fdf3ac347607f7edb/setuptools-80.10.1.tar.gz", hash = "sha256:bf2e513eb8144c3298a3bd28ab1a5edb739131ec5c22e045ff93cd7f5319703a", size = 1229650, upload-time = "2026-01-21T09:42:03.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/76/f963c61683a39084aa575f98089253e1e852a4417cb8a3a8a422923a5246/setuptools-80.10.1-py3-none-any.whl", hash = "sha256:fc30c51cbcb8199a219c12cc9c281b5925a4978d212f84229c909636d9f6984e", size = 1099859, upload-time = "2026-01-21T09:42:00.688Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "sphinx" +version = "7.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "babel", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.10'" }, + { name = "imagesize", marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "requests", marker = "python_full_version < '3.10'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "babel", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version == '3.10.*'" }, + { name = "imagesize", marker = "python_full_version == '3.10.*'" }, + { name = "jinja2", marker = "python_full_version == '3.10.*'" }, + { name = "packaging", marker = "python_full_version == '3.10.*'" }, + { name = "pygments", marker = "python_full_version == '3.10.*'" }, + { name = "requests", marker = "python_full_version == '3.10.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, +] + +[[package]] +name = "sphinx" +version = "8.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "babel", marker = "python_full_version >= '3.11'" }, + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.11'" }, + { name = "imagesize", marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, +] + +[[package]] +name = "sphinxcontrib-apidoc" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pbr" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/8d/161842a1c199c1baed5f5bc0765a59a791324f962fea3b604f105c9493e7/sphinxcontrib_apidoc-0.6.0.tar.gz", hash = "sha256:329b9810d66988f48e127a6bd18cc8efbbd1cd20b8deb4691a35738af49ad88d", size = 16790, upload-time = "2025-05-08T12:55:44.463Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/ac/6b94ecdca590ad0596b83d78277f89d897539a6cfdc997d62f27651540bc/sphinxcontrib_apidoc-0.6.0-py3-none-any.whl", hash = "sha256:668592f933eee858f3bc0d0810d56d50dfa0a70f650a2faaaad501b9a3504633", size = 8765, upload-time = "2025-05-08T12:55:42.733Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + +[[package]] +name = "trio" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "attrs", marker = "python_full_version < '3.10'" }, + { name = "cffi", marker = "python_full_version < '3.10' and implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "outcome", marker = "python_full_version < '3.10'" }, + { name = "sniffio", marker = "python_full_version < '3.10'" }, + { name = "sortedcontainers", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/8f/c6e36dd11201e2a565977d8b13f0b027ba4593c1a80bed5185489178e257/trio-0.31.0.tar.gz", hash = "sha256:f71d551ccaa79d0cb73017a33ef3264fde8335728eb4c6391451fe5d253a9d5b", size = 605825, upload-time = "2025-09-09T15:17:15.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/5b/94237a3485620dbff9741df02ff6d8acaa5fdec67d81ab3f62e4d8511bf7/trio-0.31.0-py3-none-any.whl", hash = "sha256:b5d14cd6293d79298b49c3485ffd9c07e3ce03a6da8c7dfbe0cb3dd7dc9a4774", size = 512679, upload-time = "2025-09-09T15:17:13.821Z" }, +] + +[[package]] +name = "trio" +version = "0.32.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "cffi", marker = "python_full_version >= '3.10' and implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "outcome", marker = "python_full_version >= '3.10'" }, + { name = "sniffio", marker = "python_full_version >= '3.10'" }, + { name = "sortedcontainers", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/ce/0041ddd9160aac0031bcf5ab786c7640d795c797e67c438e15cfedf815c8/trio-0.32.0.tar.gz", hash = "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b", size = 605323, upload-time = "2025-10-31T07:18:17.466Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/bf/945d527ff706233636c73880b22c7c953f3faeb9d6c7e2e85bfbfd0134a0/trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5", size = 512030, upload-time = "2025-10-31T07:18:15.885Z" }, +] + +[[package]] +name = "trio-typing" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-generator", marker = "python_full_version >= '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "trio", version = "0.32.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/74/a87aafa40ec3a37089148b859892cbe2eef08d132c816d58a60459be5337/trio-typing-0.10.0.tar.gz", hash = "sha256:065ee684296d52a8ab0e2374666301aec36ee5747ac0e7a61f230250f8907ac3", size = 38747, upload-time = "2023-12-01T02:54:55.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/ff/9bd795273eb14fac7f6a59d16cc8c4d0948a619a1193d375437c7f50f3eb/trio_typing-0.10.0-py3-none-any.whl", hash = "sha256:6d0e7ec9d837a2fe03591031a172533fbf4a1a95baf369edebfc51d5a49f0264", size = 42224, upload-time = "2023-12-01T02:54:54.1Z" }, +] + +[[package]] +name = "trio-websocket" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "outcome" }, + { name = "trio", version = "0.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "trio", version = "0.32.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "wsproto", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "wsproto", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/3c/8b4358e81f2f2cfe71b66a267f023a91db20a817b9425dd964873796980a/trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", size = 33549, upload-time = "2025-02-25T05:16:58.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221, upload-time = "2025-02-25T05:16:57.545Z" }, +] + +[[package]] +name = "types-certifi" +version = "2021.10.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/68/943c3aeaf14624712a0357c4a67814dba5cea36d194f5c764dad7959a00c/types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", size = 2095, upload-time = "2022-06-09T15:19:05.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/63/2463d89481e811f007b0e1cd0a91e52e141b47f9de724d20db7b861dcfec/types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a", size = 2136, upload-time = "2022-06-09T15:19:03.127Z" }, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20260124" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/41/4f8eb1ce08688a9e3e23709ed07089ccdeaf95b93745bfb768c6da71197d/types_python_dateutil-2.9.0.20260124.tar.gz", hash = "sha256:7d2db9f860820c30e5b8152bfe78dbdf795f7d1c6176057424e8b3fdd1f581af", size = 16596, upload-time = "2026-01-24T03:18:42.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/c2/aa5e3f4103cc8b1dcf92432415dde75d70021d634ecfd95b2e913cf43e17/types_python_dateutil-2.9.0.20260124-py3-none-any.whl", hash = "sha256:f802977ae08bf2260142e7ca1ab9d4403772a254409f7bbdf652229997124951", size = 18266, upload-time = "2026-01-24T03:18:42.155Z" }, +] + +[[package]] +name = "types-urllib3" +version = "1.26.25.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/de/b9d7a68ad39092368fb21dd6194b362b98a1daeea5dcfef5e1adb5031c7e/types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f", size = 11239, upload-time = "2023-07-20T15:19:31.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/7b/3fc711b2efea5e85a7a0bbfe269ea944aa767bbba5ec52f9ee45d362ccf3/types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e", size = 15377, upload-time = "2023-07-20T15:19:30.379Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[package.optional-dependencies] +socks = [ + { name = "pysocks" }, +] + +[[package]] +name = "virtualenv" +version = "20.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "wrapt" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/0d/12d8c803ed2ce4e5e7d5b9f5f602721f9dfef82c95959f3ce97fa584bb5c/wrapt-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:64b103acdaa53b7caf409e8d45d39a8442fe6dcfec6ba3f3d141e0cc2b5b4dbd", size = 77481, upload-time = "2025-11-07T00:43:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/3e/4364ebe221ebf2a44d9fc8695a19324692f7dd2795e64bd59090856ebf12/wrapt-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:91bcc576260a274b169c3098e9a3519fb01f2989f6d3d386ef9cbf8653de1374", size = 60692, upload-time = "2025-11-07T00:43:13.697Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ff/ae2a210022b521f86a8ddcdd6058d137c051003812b0388a5e9a03d3fe10/wrapt-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab594f346517010050126fcd822697b25a7031d815bb4fbc238ccbe568216489", size = 61574, upload-time = "2025-11-07T00:43:14.967Z" }, + { url = "https://files.pythonhosted.org/packages/c6/93/5cf92edd99617095592af919cb81d4bff61c5dbbb70d3c92099425a8ec34/wrapt-2.0.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:36982b26f190f4d737f04a492a68accbfc6fa042c3f42326fdfbb6c5b7a20a31", size = 113688, upload-time = "2025-11-07T00:43:18.275Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0a/e38fc0cee1f146c9fb266d8ef96ca39fb14a9eef165383004019aa53f88a/wrapt-2.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23097ed8bc4c93b7bf36fa2113c6c733c976316ce0ee2c816f64ca06102034ef", size = 115698, upload-time = "2025-11-07T00:43:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/b0/85/bef44ea018b3925fb0bcbe9112715f665e4d5309bd945191da814c314fd1/wrapt-2.0.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bacfe6e001749a3b64db47bcf0341da757c95959f592823a93931a422395013", size = 112096, upload-time = "2025-11-07T00:43:16.5Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0b/733a2376e413117e497aa1a5b1b78e8f3a28c0e9537d26569f67d724c7c5/wrapt-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8ec3303e8a81932171f455f792f8df500fc1a09f20069e5c16bd7049ab4e8e38", size = 114878, upload-time = "2025-11-07T00:43:20.81Z" }, + { url = "https://files.pythonhosted.org/packages/da/03/d81dcb21bbf678fcda656495792b059f9d56677d119ca022169a12542bd0/wrapt-2.0.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3f373a4ab5dbc528a94334f9fe444395b23c2f5332adab9ff4ea82f5a9e33bc1", size = 111298, upload-time = "2025-11-07T00:43:22.229Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d5/5e623040e8056e1108b787020d56b9be93dbbf083bf2324d42cde80f3a19/wrapt-2.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f49027b0b9503bf6c8cdc297ca55006b80c2f5dd36cecc72c6835ab6e10e8a25", size = 113361, upload-time = "2025-11-07T00:43:24.301Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f3/de535ccecede6960e28c7b722e5744846258111d6c9f071aa7578ea37ad3/wrapt-2.0.1-cp310-cp310-win32.whl", hash = "sha256:8330b42d769965e96e01fa14034b28a2a7600fbf7e8f0cc90ebb36d492c993e4", size = 58035, upload-time = "2025-11-07T00:43:28.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/15/39d3ca5428a70032c2ec8b1f1c9d24c32e497e7ed81aed887a4998905fcc/wrapt-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:1218573502a8235bb8a7ecaed12736213b22dcde9feab115fa2989d42b5ded45", size = 60383, upload-time = "2025-11-07T00:43:25.804Z" }, + { url = "https://files.pythonhosted.org/packages/43/c2/dfd23754b7f7a4dce07e08f4309c4e10a40046a83e9ae1800f2e6b18d7c1/wrapt-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:eda8e4ecd662d48c28bb86be9e837c13e45c58b8300e43ba3c9b4fa9900302f7", size = 58894, upload-time = "2025-11-07T00:43:27.074Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/553997acf3939079dab022e37b67b1904b5b0cc235503226898ba573b10c/wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590", size = 77480, upload-time = "2025-11-07T00:43:30.573Z" }, + { url = "https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6", size = 60690, upload-time = "2025-11-07T00:43:31.594Z" }, + { url = "https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7", size = 61578, upload-time = "2025-11-07T00:43:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28", size = 114115, upload-time = "2025-11-07T00:43:35.605Z" }, + { url = "https://files.pythonhosted.org/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb", size = 116157, upload-time = "2025-11-07T00:43:37.058Z" }, + { url = "https://files.pythonhosted.org/packages/01/22/1c158fe763dbf0a119f985d945711d288994fe5514c0646ebe0eb18b016d/wrapt-2.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c", size = 112535, upload-time = "2025-11-07T00:43:34.138Z" }, + { url = "https://files.pythonhosted.org/packages/5c/28/4f16861af67d6de4eae9927799b559c20ebdd4fe432e89ea7fe6fcd9d709/wrapt-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16", size = 115404, upload-time = "2025-11-07T00:43:39.214Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8b/7960122e625fad908f189b59c4aae2d50916eb4098b0fb2819c5a177414f/wrapt-2.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2", size = 111802, upload-time = "2025-11-07T00:43:40.476Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/7881eee5ac31132a713ab19a22c9e5f1f7365c8b1df50abba5d45b781312/wrapt-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd", size = 113837, upload-time = "2025-11-07T00:43:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/45/00/9499a3d14e636d1f7089339f96c4409bbc7544d0889f12264efa25502ae8/wrapt-2.0.1-cp311-cp311-win32.whl", hash = "sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be", size = 58028, upload-time = "2025-11-07T00:43:47.369Z" }, + { url = "https://files.pythonhosted.org/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b", size = 60385, upload-time = "2025-11-07T00:43:44.34Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/32195e57a8209003587bbbad44d5922f13e0ced2a493bb46ca882c5b123d/wrapt-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf", size = 58893, upload-time = "2025-11-07T00:43:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" }, + { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" }, + { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, + { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, + { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, + { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, + { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, + { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, + { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" }, + { url = "https://files.pythonhosted.org/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" }, + { url = "https://files.pythonhosted.org/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" }, + { url = "https://files.pythonhosted.org/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" }, + { url = "https://files.pythonhosted.org/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0", size = 58701, upload-time = "2025-11-07T00:44:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c", size = 60947, upload-time = "2025-11-07T00:44:46.086Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e", size = 59359, upload-time = "2025-11-07T00:44:47.164Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" }, + { url = "https://files.pythonhosted.org/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c", size = 60609, upload-time = "2025-11-07T00:45:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395", size = 64038, upload-time = "2025-11-07T00:45:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1f/5af0ae22368ec69067a577f9e07a0dd2619a1f63aabc2851263679942667/wrapt-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:68424221a2dc00d634b54f92441914929c5ffb1c30b3b837343978343a3512a3", size = 77478, upload-time = "2025-11-07T00:45:16.65Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b7/fd6b563aada859baabc55db6aa71b8afb4a3ceb8bc33d1053e4c7b5e0109/wrapt-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd1a18f5a797fe740cb3d7a0e853a8ce6461cc62023b630caec80171a6b8097", size = 60687, upload-time = "2025-11-07T00:45:17.896Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8c/9ededfff478af396bcd081076986904bdca336d9664d247094150c877dcb/wrapt-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fb3a86e703868561c5cad155a15c36c716e1ab513b7065bd2ac8ed353c503333", size = 61563, upload-time = "2025-11-07T00:45:19.109Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/d795a1aa2b6ab20ca21157fe03cbfc6aa7e870a88ac3b4ea189e2f6c79f0/wrapt-2.0.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5dc1b852337c6792aa111ca8becff5bacf576bf4a0255b0f05eb749da6a1643e", size = 113395, upload-time = "2025-11-07T00:45:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/61/32/56cde2bbf95f2d5698a1850a765520aa86bc7ae0f95b8ec80b6f2e2049bb/wrapt-2.0.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c046781d422f0830de6329fa4b16796096f28a92c8aef3850674442cdcb87b7f", size = 115362, upload-time = "2025-11-07T00:45:22.809Z" }, + { url = "https://files.pythonhosted.org/packages/cf/53/8d3cc433847c219212c133a3e8305bd087b386ef44442ff39189e8fa62ac/wrapt-2.0.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f73f9f7a0ebd0db139253d27e5fc8d2866ceaeef19c30ab5d69dcbe35e1a6981", size = 111766, upload-time = "2025-11-07T00:45:20.294Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d3/14b50c2d0463c0dcef8f388cb1527ed7bbdf0972b9fd9976905f36c77ebf/wrapt-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b667189cf8efe008f55bbda321890bef628a67ab4147ebf90d182f2dadc78790", size = 114560, upload-time = "2025-11-07T00:45:24.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b8/4f731ff178f77ae55385586de9ff4b4261e872cf2ced4875e6c976fbcb8b/wrapt-2.0.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:a9a83618c4f0757557c077ef71d708ddd9847ed66b7cc63416632af70d3e2308", size = 110999, upload-time = "2025-11-07T00:45:25.596Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bb/5f1bb0f9ae9d12e19f1d71993d052082062603e83fe3e978377f918f054d/wrapt-2.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e9b121e9aeb15df416c2c960b8255a49d44b4038016ee17af03975992d03931", size = 113164, upload-time = "2025-11-07T00:45:26.8Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f6/f3a3c623d3065c7bf292ee0b73566236b562d5ed894891bd8e435762b618/wrapt-2.0.1-cp39-cp39-win32.whl", hash = "sha256:1f186e26ea0a55f809f232e92cc8556a0977e00183c3ebda039a807a42be1494", size = 58028, upload-time = "2025-11-07T00:45:30.943Z" }, + { url = "https://files.pythonhosted.org/packages/24/78/647c609dfa18063a7fcd5c23f762dd006be401cc9206314d29c9b0b12078/wrapt-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:bf4cb76f36be5de950ce13e22e7fdf462b35b04665a12b64f3ac5c1bbbcf3728", size = 60380, upload-time = "2025-11-07T00:45:28.341Z" }, + { url = "https://files.pythonhosted.org/packages/07/90/0c14b241d18d80ddf4c847a5f52071e126e8a6a9e5a8a7952add8ef0d766/wrapt-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:d6cc985b9c8b235bd933990cdbf0f891f8e010b65a3911f7a55179cd7b0fc57b", size = 58895, upload-time = "2025-11-07T00:45:29.527Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, +] + +[[package]] +name = "wsproto" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "h11", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" }, +] + +[[package]] +name = "wsproto" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "h11", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]