diff --git a/.gitchangelog.rc b/.gitchangelog.rc deleted file mode 100644 index 45092650..00000000 --- a/.gitchangelog.rc +++ /dev/null @@ -1,288 +0,0 @@ -# -*- coding: utf-8; mode: python -*- -## -## Format -## -## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...] -## -## Description -## -## ACTION is one of 'chg', 'fix', 'new' -## -## Is WHAT the change is about. -## -## 'chg' is for refactor, small improvement, cosmetic changes... -## 'fix' is for bug fixes -## 'new' is for new features, big improvement -## -## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc' -## -## Is WHO is concerned by the change. -## -## 'dev' is for developpers (API changes, refactors...) -## 'usr' is for final users (UI changes) -## 'pkg' is for packagers (packaging changes) -## 'test' is for testers (test only related changes) -## 'doc' is for doc guys (doc only changes) -## -## COMMIT_MSG is ... well ... the commit message itself. -## -## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic' -## -## They are preceded with a '!' or a '@' (prefer the former, as the -## latter is wrongly interpreted in github.) Commonly used tags are: -## -## 'refactor' is obviously for refactoring code only -## 'minor' is for a very meaningless change (a typo, adding a comment) -## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...) -## 'wip' is for partial functionality but complete subfunctionality. -## -## Example: -## -## new: usr: support of bazaar implemented -## chg: re-indentend some lines !cosmetic -## new: dev: updated code to be compatible with last version of killer lib. -## fix: pkg: updated year of licence coverage. -## new: test: added a bunch of test around user usability of feature X. -## fix: typo in spelling my name in comment. !minor -## -## Please note that multi-line commit message are supported, and only the -## first line will be considered as the "summary" of the commit message. So -## tags, and other rules only applies to the summary. The body of the commit -## message will be displayed in the changelog without reformatting. - - -## -## ``ignore_regexps`` is a line of regexps -## -## Any commit having its full commit message matching any regexp listed here -## will be ignored and won't be reported in the changelog. -## -ignore_regexps = [ - r'@minor', r'!minor', - r'@cosmetic', r'!cosmetic', - r'@refactor', r'!refactor', - r'@wip', r'!wip', - r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:', - r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:', - r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$', - r'^$', ## ignore commits with empty messages -] - - -## ``section_regexps`` is a list of 2-tuples associating a string label and a -## list of regexp -## -## Commit messages will be classified in sections thanks to this. Section -## titles are the label, and a commit is classified under this section if any -## of the regexps associated is matching. -## -## Please note that ``section_regexps`` will only classify commits and won't -## make any changes to the contents. So you'll probably want to go check -## ``subject_process`` (or ``body_process``) to do some changes to the subject, -## whenever you are tweaking this variable. -## -section_regexps = [ - ('New', [ - r'^([nN]ew|[fF]eat|[aA]dd)\s*(:|)\s*([^\n]*)$', - ]), - ('Fix', [ - r'^[fF]ix\s*(:|)\s*([^\n]*)$', - ]), - ('Test', [ - r'^([tT]est|[cC][iI])\s*(:|)\s*([^\n]*)$', - ]), - ('Other', None ## Match all lines - ), - -] - - -## ``body_process`` is a callable -## -## This callable will be given the original body and result will -## be used in the changelog. -## -## Available constructs are: -## -## - any python callable that take one txt argument and return txt argument. -## -## - ReSub(pattern, replacement): will apply regexp substitution. -## -## - Indent(chars=" "): will indent the text with the prefix -## Please remember that template engines gets also to modify the text and -## will usually indent themselves the text if needed. -## -## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns -## -## - noop: do nothing -## -## - ucfirst: ensure the first letter is uppercase. -## (usually used in the ``subject_process`` pipeline) -## -## - final_dot: ensure text finishes with a dot -## (usually used in the ``subject_process`` pipeline) -## -## - strip: remove any spaces before or after the content of the string -## -## - SetIfEmpty(msg="No commit message."): will set the text to -## whatever given ``msg`` if the current text is empty. -## -## Additionally, you can `pipe` the provided filters, for instance: -#body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ") -#body_process = Wrap(regexp=r'\n(?=\w+\s*:)') -#body_process = noop -body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip - - -## ``subject_process`` is a callable -## -## This callable will be given the original subject and result will -## be used in the changelog. -## -## Available constructs are those listed in ``body_process`` doc. -subject_process = (strip | - ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') | - SetIfEmpty("No commit message.") | ucfirst | final_dot) - - -## ``tag_filter_regexp`` is a regexp -## -## Tags that will be used for the changelog must match this regexp. -## -tag_filter_regexp = r'^[vV]{1}[0-9]+\.[0-9]+(\.[0-9]*)?$' - - -## ``unreleased_version_label`` is a string or a callable that outputs a string -## -## This label will be used as the changelog Title of the last set of changes -## between last valid tag and HEAD if any. -unreleased_version_label = "(unreleased)" - - -## ``output_engine`` is a callable -## -## This will change the output format of the generated changelog file -## -## Available choices are: -## -## - rest_py -## -## Legacy pure python engine, outputs ReSTructured text. -## This is the default. -## -## - mustache() -## -## Template name could be any of the available templates in -## ``templates/mustache/*.tpl``. -## Requires python package ``pystache``. -## Examples: -## - mustache("markdown") -## - mustache("restructuredtext") -## -## - makotemplate() -## -## Template name could be any of the available templates in -## ``templates/mako/*.tpl``. -## Requires python package ``mako``. -## Examples: -## - makotemplate("restructuredtext") -## -output_engine = rest_py -#output_engine = mustache("restructuredtext") -#output_engine = mustache("markdown") -#output_engine = makotemplate("restructuredtext") - - -## ``include_merge`` is a boolean -## -## This option tells git-log whether to include merge commits in the log. -## The default is to include them. -include_merge = False - - -## ``log_encoding`` is a string identifier -## -## This option tells gitchangelog what encoding is outputed by ``git log``. -## The default is to be clever about it: it checks ``git config`` for -## ``i18n.logOutputEncoding``, and if not found will default to git's own -## default: ``utf-8``. -log_encoding = 'utf-8' - - -## ``publish`` is a callable -## -## Sets what ``gitchangelog`` should do with the output generated by -## the output engine. ``publish`` is a callable taking one argument -## that is an interator on lines from the output engine. -## -## Some helper callable are provided: -## -## Available choices are: -## -## - stdout -## -## Outputs directly to standard output -## (This is the default) -## -## - FileInsertAtFirstRegexMatch(file, pattern, idx=lamda m: m.start()) -## -## Creates a callable that will parse given file for the given -## regex pattern and will insert the output in the file. -## ``idx`` is a callable that receive the matching object and -## must return a integer index point where to insert the -## the output in the file. Default is to return the position of -## the start of the matched string. -## -## - FileRegexSubst(file, pattern, replace, flags) -## -## Apply a replace inplace in the given file. Your regex pattern must -## take care of everything and might be more complex. Check the README -## for a complete copy-pastable example. -## -# publish = FileInsertIntoFirstRegexMatch( -# "CHANGELOG.rst", -# r'/(?P[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n/', -# idx=lambda m: m.start(1) -# ) -#publish = stdout - - -## ``revs`` is a list of callable or a list of string -## -## callable will be called to resolve as strings and allow dynamical -## computation of these. The result will be used as revisions for -## gitchangelog (as if directly stated on the command line). This allows -## to filter exaclty which commits will be read by gitchangelog. -## -## To get a full documentation on the format of these strings, please -## refer to the ``git rev-list`` arguments. There are many examples. -## -## Using callables is especially useful, for instance, if you -## are using gitchangelog to generate incrementally your changelog. -## -## Some helpers are provided, you can use them:: -## -## - FileFirstRegexMatch(file, pattern): will return a callable that will -## return the first string match for the given pattern in the given file. -## If you use named sub-patterns in your regex pattern, it'll output only -## the string matching the regex pattern named "rev". -## -## - Caret(rev): will return the rev prefixed by a "^", which is a -## way to remove the given revision and all its ancestor. -## -## Please note that if you provide a rev-list on the command line, it'll -## replace this value (which will then be ignored). -## -## If empty, then ``gitchangelog`` will act as it had to generate a full -## changelog. -## -## The default is to use all commits to make the changelog. -#revs = ["^1.0.3", ] -#revs = [ -# Caret( -# FileFirstRegexMatch( -# "CHANGELOG.rst", -# r"(?P[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n")), -# "HEAD" -#] -revs = [] 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/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 911a3616..9ad0aa1d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,7 @@ __pycache__ # Virtual Environments venv* +.venv .tox -Pipfile.lock - .coverage diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36f5975b..17b86e2a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,10 @@ -- repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.4 - hooks: - - id: autopep8 - args: ["-a", "-i"] -- repo: https://github.com/pre-commit/mirrors-isort - rev: v4.3.21 - hooks: - - id: isort - args: ["-rc", "."] -- repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.782' - hooks: - - id: mypy - entry: mypy appium/ test/ - pass_filenames: false +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/.pylintrc b/.pylintrc deleted file mode 100644 index 1d5306af..00000000 --- a/.pylintrc +++ /dev/null @@ -1,553 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. -jobs=1 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - invalid-unicode-literal, - raw-checker-failed, - bad-inline-option, - locally-disabled, - locally-enabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - empty-docstring, - missing-docstring, - too-few-public-methods, - invalid-name, - duplicate-code - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio).You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=optparse.Values,sys.exit - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,io,builtins - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=128 - -# Maximum number of lines in a module -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[BASIC] - -# Naming style matching correct argument names -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style -#argument-rgx= - -# Naming style matching correct attribute names -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style -#class-attribute-rgx= - -# Naming style matching correct class names -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming-style -#class-rgx= - -# Naming style matching correct constant names -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma -good-names=i, - j, - k, - ex, - Run, - _ - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# Naming style matching correct inline iteration names -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style -#inlinevar-rgx= - -# Naming style matching correct method names -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style -#method-rgx= - -# Naming style matching correct module names -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style -#variable-rgx= - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=6 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of statements in function / method body -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=yes - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub, - TERMIOS, - Bastion, - rexec - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception 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/.travis.yml b/.travis.yml deleted file mode 100644 index 826c21be..00000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: python -dist: xenial - -python: - - "3.7" - - "3.8" - - "3.9-dev" - -install: - - pip install tox-travis - -script: - - tox 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.rst b/CHANGELOG.rst deleted file mode 100644 index e8edf193..00000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,2238 +0,0 @@ -Changelog -========= - - -v1.0.2 (2020-07-15) -------------------- -- Bump 1.0.2. [Kazuaki Matsuo] -- Chore: Add the workaround to avoid service freezes on Windows (#552) - [Mykola Mokhnach] -- Chore: add checking package file count comparison in release script - (#547) [Kazuaki Matsuo] - - * chore: Add file count in release script - - * use f string for Python 3 :P - - * handle exit in method -- Update changelog for 1.0.1. [Kazuaki Matsuo] - - -v1.0.1 (2020-05-18) -------------------- - -Fix -~~~ -- Broken package (#545) [Kazuaki Matsuo] - - * add appium/webdriver/py.typed in find_packages - - * fix - -Other -~~~~~ -- Bump 1.0.1. [Kazuaki Matsuo] -- Update changelog for 1.0.0. [Kazuaki Matsuo] - - -v1.0.0 (2020-05-16) -------------------- - -New -~~~ -- Feat: Added Makefile (#530) [Mori Atsushi] - - * Created setup.cfg - - * Updated lib ver for pre-commit - - * Fix ci.sh to set failure even when one command failed - - * Fix pylint error - - * Add help to Makefile - - * Update README - - * Add check-all command -- Feat: Merge python3 branch to master (#526) [Hannes Hauer, Hannes - Hauer * chore: Update readme and - gitchangelog section role (#524) (#525) * chore: tweak changelog - filter * address stoping Python 2 support * 2 instead of 2.0... - * tweak readme * Revert some unexpected changes * review - comments * Changed bound for TypeVar * Fix crashing ci * - Remove beta Co-authored-by: dependabot-preview[bot] - <27856297+dependabot-preview[bot]@users.noreply.github.com>, Kazuaki - Matsuo, Kazuaki Matsuo, Mori Atsushi, Mykola Mokhnach, Mykola - Mokhnach, Nrupesh Patel, Nrupesh Patel, Venkatesh, Venkatesh] - - * Drop py2 support (#478) - - * Drop py2 support - - * Support 3.7+ - - * Add explicit type declarations (#482) - - * Fixed mypy warning: touch_action.py - - * Fixed mypy warning: multi_action.py - - * Fixed mypy warning: extensions/android - - * Fixed mypy warning: extensions/search_context - - * Updated - - * Revert some changes to run unit test - - * Review comments - - * Updates - - * Updates - - * Add mypy check to ci.sh - - * Add mypy to Pipfile - - * Updates - - * Update README - - * Revert unexpected changes - - * Updates Dict - - * Revert unexpected changes - - * Updates - - * Review comments - - * Review comments - - * tweak - - * Restore and modify changes - - * Fix wrong return type - - * Add comments - - * Revert unexpected changes - - * Fix mypy error - - * updates - - * Add mypy to pre-commit (#485) - - * chore: Applied some py3 formats (#486) - - * Removed unused import - - * Removed unnecessary codes - - * Applied f'' format instead ''.format() - - * Fixes - - * tweak - - * chore: Fix mypy errors under test folder (#487) - - * Fix mypy errors under test folder - - * Add mypy check for test folder to pre-commit - - * Add mypy check to ci - - * chore: Remove unittest dependency (#488) - - * Removed unnecessary codes from calling super - - * Removed unittest dependency - - * Upgrade the dependencies to the latest - - * Removed unused args - - * Review comments - - * Update mock requirement from ~=3.0 to ~=4.0 (#502) - - Updates the requirements on [mock](https://github.com/testing-cabal/mock) to permit the latest version. - - [Release notes](https://github.com/testing-cabal/mock/releases) - - [Changelog](https://github.com/testing-cabal/mock/blob/master/CHANGELOG.rst) - - [Commits](https://github.com/testing-cabal/mock/compare/3.0.0...4.0.0) - - Signed-off-by: dependabot-preview[bot] - - * Add 'from' to except (#503) - - * Update pre-commit requirement from ~=1.21 to ~=2.1 (#506) - - Updates the requirements on [pre-commit](https://github.com/pre-commit/pre-commit) to permit the latest version. - - [Release notes](https://github.com/pre-commit/pre-commit/releases) - - [Changelog](https://github.com/pre-commit/pre-commit/blob/master/CHANGELOG.md) - - [Commits](https://github.com/pre-commit/pre-commit/compare/v1.21.0...v2.1.0) - - Signed-off-by: dependabot-preview[bot] - - * doc: Add script to generate sphinx doc (#508) - - * Add quickstart template files - - * Update conf file - - * Update - - * Update settings - - * Change project name - - * Add script to generate docs - - * Changed header title - - * Add new line to usage section - - * Add py.typed file(PEP561) - - * Replace \n with new line - - * tweak - - * Use sphinx format for tables - - * Rebase python3 branch with master (#522) - - * Update pytest-cov requirement from ~=2.6 to ~=2.8 (#489) - - Updates the requirements on [pytest-cov](https://github.com/pytest-dev/pytest-cov) to permit the latest version. - - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.6.0...v2.8.1) - - Signed-off-by: dependabot-preview[bot] - - * Update autopep8 requirement from ~=1.4 to ~=1.5 (#490) - - Updates the requirements on [autopep8](https://github.com/hhatto/autopep8) to permit the latest version. - - [Release notes](https://github.com/hhatto/autopep8/releases) - - [Commits](https://github.com/hhatto/autopep8/compare/v1.4...v1.5) - - Signed-off-by: dependabot-preview[bot] - - * Update tox-travis requirement from ~=0.11 to ~=0.12 (#491) - - Updates the requirements on [tox-travis](https://github.com/tox-dev/tox-travis) to permit the latest version. - - [Release notes](https://github.com/tox-dev/tox-travis/releases) - - [Changelog](https://github.com/tox-dev/tox-travis/blob/master/HISTORY.rst) - - [Commits](https://github.com/tox-dev/tox-travis/compare/0.11...0.12) - - Signed-off-by: dependabot-preview[bot] - - * Update tox requirement from ~=3.6 to ~=3.14 (#494) - - Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. - - [Release notes](https://github.com/tox-dev/tox/releases) - - [Changelog](https://github.com/tox-dev/tox/blob/master/docs/changelog.rst) - - [Commits](https://github.com/tox-dev/tox/compare/3.6.0...3.14.3) - - Signed-off-by: dependabot-preview[bot] - - * chore: Fix find_by_images_tests.py (#495) - - * chore: Fix find_by_images_tests.py - - * Add installation opencv4nodejs - - * Fix typo - - * Add taking screen record to find_by_image_test - - * Fix errors on the emulator - - * Remove unused imports - - * feat: Add viewmatcher (#480) - - * Add android view matcher as strategy locator - - * Add docstring - - * Add functional test - - * Remove find_elements_by_android_data_matcher - - * Fix docstring - - * tweak docstring - - * Bump 0.50 - - * Update changelog for 0.50 - - * Fix flaky functional tests (#473) - - * Run all tests - - * Fix apk file path - - * Skip find_element_by_image test cases - - * Skip context switching test - - * Skip multi tap test on CI - - * Change strategy for waiting element - - * Add functions for same steps - - * Restore unexpected changes - - * Fix touch_action_tests - - * Fix - - * Fix - Fix test_driver_swipe - - * fix - - * Create _move_to_[target_view] - - * [test_driver_swipe] Add wait - - * feat: Add idempotency key header to create session requests (#514) - - * feat: Override send_keys without file upload function (#515) - - * add send_keys_direct - - * override send_keys - - * tune - - * add unittest instead of functional test - - * tweak syntax - - * Bump 0.51 - - * Update changelog for 0.51 - - * test: Fix test_clear flaky functional test (#519) - - * test: Add unit test for set_value (setImmediateValue) (#518) - - * chore: Fix int - str comparison error in ios desired capabilities (#517) - - if number >= PytestXdistWorker.COUNT: - -Fix -~~~ -- Tune mixin types, so linters could recognize them better (#536) - [Mykola Mokhnach] - -Test -~~~~ -- Test: Add appium_service functional test (#531) [Mori Atsushi] - - * Add appium_service functional test - - * Fix expressions - -Other -~~~~~ -- Bump 1.0.0. [Kazuaki Matsuo] -- Chore: Updates docstring (#533) [Mori Atsushi] - - * Updates docstring - - * Add description to Returns field - - * Remove type from docstring - - Since type hint already added to args - - * Set default lang to en - - * Change usage style in docstring - - * Updates - - * Remove rtype - - unnecessary anymore since type hint works for auto completion - - * tweak - - * Update return type - - * Restore types for keyword args - - * Remove types from Return field - - Except for property and TypeVar -- Chore: Remove saucetestcase from the client (#539) [Mykola Mokhnach] -- Chore: add py.typed in package, add maintainers (#538) [Kazuaki - Matsuo] -- Docs: Update documentation (#527) [Kazuaki Matsuo] - - * Chore: correct license, update readme - - * cleanup - - * docs: update the url of documentation -- Chore: Update readme and gitchangelog section role (#524) [Kazuaki - Matsuo] - - * chore: tweak changelog filter - - * address stoping Python 2 support - - * 2 instead of 2.0... - - * tweak readme -- Update changelog for 0.52. [Kazuaki Matsuo] - - -v0.52 (2020-04-23) ------------------- - -Fix -~~~ -- Handling of dictionary-values in WebElement.get_attribute() (#521) - [Hannes Hauer] - -Test -~~~~ -- Test: Add unit test for set_value (setImmediateValue) (#518) [Nrupesh - Patel] -- Test: Fix test_clear flaky functional test (#519) [Nrupesh Patel] - -Other -~~~~~ -- Bump 0.52. [Kazuaki Matsuo] -- Chore: Fix int - str comparison error in ios desired capabilities - (#517) [Venkatesh] - - if number >= PytestXdistWorker.COUNT: -- Update changelog for 0.51. [Kazuaki Matsuo] - - -v0.51 (2020-04-12) ------------------- - -New -~~~ -- Feat: Override send_keys without file upload function (#515) [Kazuaki - Matsuo] - - * add send_keys_direct - - * override send_keys - - * tune - - * add unittest instead of functional test - - * tweak syntax -- Feat: Add idempotency key header to create session requests (#514) - [Mykola Mokhnach] - -Fix -~~~ -- Fix flaky functional tests (#473) [Mori Atsushi] - - * Run all tests - - * Fix apk file path - - * Skip find_element_by_image test cases - - * Skip context switching test - - * Skip multi tap test on CI - - * Change strategy for waiting element - - * Add functions for same steps - - * Restore unexpected changes - - * Fix touch_action_tests - - * Fix - - * Fix - Fix test_driver_swipe - - * fix - - * Create _move_to_[target_view] - - * [test_driver_swipe] Add wait - -Other -~~~~~ -- Bump 0.51. [Kazuaki Matsuo] -- Update changelog for 0.50. [Kazuaki Matsuo] - - -v0.50 (2020-02-10) ------------------- - -New -~~~ -- Feat: Add viewmatcher (#480) [Mori Atsushi] - - * Add android view matcher as strategy locator - - * Add docstring - - * Add functional test - - * Remove find_elements_by_android_data_matcher - - * Fix docstring - - * tweak docstring - -Test -~~~~ -- Ci: Take screen record as evidence (#481) [Mori Atsushi] - - * Take screen record for android - - * Take screen record for iOS - - * Save screen record for iOS - -Other -~~~~~ -- Bump 0.50. [Kazuaki Matsuo] -- Chore: Fix find_by_images_tests.py (#495) [Mori Atsushi] - - * chore: Fix find_by_images_tests.py - - * Add installation opencv4nodejs - - * Fix typo - - * Add taking screen record to find_by_image_test - - * Fix errors on the emulator - - * Remove unused imports -- Update tox requirement from ~=3.6 to ~=3.14 (#494) [dependabot- - preview[bot]] - - Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. - - [Release notes](https://github.com/tox-dev/tox/releases) - - [Changelog](https://github.com/tox-dev/tox/blob/master/docs/changelog.rst) - - [Commits](https://github.com/tox-dev/tox/compare/3.6.0...3.14.3) -- Update tox-travis requirement from ~=0.11 to ~=0.12 (#491) - [dependabot-preview[bot]] - - Updates the requirements on [tox-travis](https://github.com/tox-dev/tox-travis) to permit the latest version. - - [Release notes](https://github.com/tox-dev/tox-travis/releases) - - [Changelog](https://github.com/tox-dev/tox-travis/blob/master/HISTORY.rst) - - [Commits](https://github.com/tox-dev/tox-travis/compare/0.11...0.12) -- Update autopep8 requirement from ~=1.4 to ~=1.5 (#490) [dependabot- - preview[bot]] - - Updates the requirements on [autopep8](https://github.com/hhatto/autopep8) to permit the latest version. - - [Release notes](https://github.com/hhatto/autopep8/releases) - - [Commits](https://github.com/hhatto/autopep8/compare/v1.4...v1.5) -- Update pytest-cov requirement from ~=2.6 to ~=2.8 (#489) [dependabot- - preview[bot]] - - Updates the requirements on [pytest-cov](https://github.com/pytest-dev/pytest-cov) to permit the latest version. - - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.6.0...v2.8.1) -- Chore: add try/catch in release script (#479) [Kazuaki Matsuo] - - * Add m and try/catch in pushing - - * fix error message - - * remove -m since it does not work for this usage -- [CI] Run with iOS 13.3 and Xcode 11.3 (#477) [Mori Atsushi] - - * [CI] Run with iOS 13.3 and Xcode 11.3 - - * Skip the case which has problem on Xcode 11.3 - - * Update FyndByIOClassChainTests along to iOS13 - - * Update FyndByElementWebelementTests along to iOS13 - - * Update KeyboardTests along to iOS13 - - * Update webdriver_tests along to iOS13 - - * Run test_find_element_by_isvisible with simpleIsVisibleCheck caps - - * Run test_hide_keyboard_no_key_name - - * Remove unused codes - - * [Readme] py.test -> pytest -- Update changelog for 0.49. [Kazuaki Matsuo] - - -v0.49 (2019-12-24) ------------------- - -New -~~~ -- Add IME unittest (#475) [Mori Atsushi] -- Add new locator strategy find_elements_by_windows_uiautomation and - test. [Manoj Kumar] -- Add new locator strategy find_element_by_windows_uiautomation. [Manoj - Kumar] - -Fix -~~~ -- Fix functional test broken by previous commit. [Manoj Kumar] -- Fix CI (Failed iOS) (#460) [Mori Atsushi] - - * Fix CI (Failed iOS) - - * Fix variable name - -Other -~~~~~ -- Bump 0.49. [Kazuaki Matsuo] -- Move session/execute_mobile commands to mixin class (#471) [Mori - Atsushi] - - * Fix get_all_sessions - - * Revert changes - - * Move execute_mobile_command codes to mixin class - - * Update docstring - - It's same to webdriver.py - - * Use /sessions as endpoint for all_sessions - - https://github.com/appium/appium-base-driver/blob/master/docs/mjsonwp/protocol-methods.md - - * Delete unnecessary codes -- Replace apk for functional test (#470) [Mori Atsushi] - - * Replace apk for functional test - - https://github.com/appium/android-apidemos/releases/tag/v3.1.0 - - * Use sdkVer 27 - - * Update app package name - - * Fix: can't find android device - - * review comments - - * tweak -- Support for log_event and get_events command (#469) [Mori Atsushi] - - * Use appium/events as endpoint to get events - - * Removed unnecessary codes - - * Update unittest along to changes - - * Update docstring - - * Created LogEvents class - - * Support log_event - - * Add unittest for log_event - - * Add functional test for log_event and get_event - - * review comments - - * Restore events API - - * Add type as arg to get_events - - * tweak - - * Removed type arg from get_events - - It isn't implemented yet for now - - * Add type arg to get_event - - The value isn't passed to the server for now. - - * Updated along to type -- Cleaned up test codes (#466) [Mori Atsushi] - - * Deleted unnecessary codes - - * Move functional tests to correct class - - * Move some tests - - * Created search_context/windows_test - - * [functional] Created search_context package - - * Remove class method decolator - - * Fix import error - - * Add BaseTestCase for ios functional testcases - - * Add test_helper for android functional test - - * Add __init__.py - - * Deleted unused imports -- Move search context methods from webdriver and webelement to - search_context (#461) [Mori Atsushi] - - * Move ios search context methods to search_context file - - * Move android search text methods - - * Move windows search context - - * Move mobile search context - - * Divided search_context into each class - - * Move custom and image methods - - * Move contents in search_context.py to __init__.py - - * Add rtype to each docstring for auto completion in IDE - - * Add comments -- [CI] Run functional tests nightly (#463) [Mori Atsushi] - - * [CI] Run functional tests nightly - - * Extend timeout to wait for 2nd session created - - * Skip flaky test_all_sessions -- Revert some changes to fix broken codes (#462) [Mori Atsushi] - - * Revert some changes - - * Fix typo -- Move commands from webdriver as mixins class (#459) [Manoj Kumar] - - * move to mixins class - - * Create common class with its tests - - * incorporating PR comments -- Update changelog for 0.48. [Kazuaki Matsuo] -- Bump 0.48. [Kazuaki Matsuo] - - -v0.48 (2019-10-22) ------------------- - -New -~~~ -- Add docs on start activity with args. [Manoj Kumar] -- Add unit tests Activate app. [Manoj Kumar] -- Add unit tests for keyboard API (#452) [Manoj Kumar] - - * Add Unit tests for Keyboard API - - * incorporating review comments - - * change per review comment -- Feat: Adding getAllSessions (#446) [Manoj Kumar] - - * Adding getAllSessions - - * adjust per lint - - * fix comments -- Add downloads badge (#441) [Mori Atsushi] - -Fix -~~~ -- Fix docstring, add getting available port number (#448) [Kazuaki - Matsuo] - - * fix docstring, add getting available port number - - * add WebDriverWait - - * define custom wait - - * move get available port in another module - - * follow python wait condition name -- Fix CI fails (Updated iOS ver) (#440) [Mori Atsushi] - - * Updated iOS ver to fix CI fails - - * Update capability for safari test on ios - - * Fix travis CI fails -- Fix CI fails (#436) [Mori Atsushi] - - * Skip taking the screenshot not in CI - - * Skip py38 on travis -- Fix isort behavior for mock (#432) [Mori Atsushi] - - * Fix isort behavior for mock - - * Add guide to add 3rd party modules to isort conf - - * Add guide for docstrings - - * Delete unnecessary codes -- Fix android flaky tests (#413) [Mori Atsushi] - - * Fix android flaky tests - - * Use androidSdkVer 27 for emulator - - * Skip find_by_accessibility_id, find_by_uiautomator - - * Changed from https://github.com/ki4070ma/python-client/pull/5 - - * Add save_appium_log.yml - - * Don't run flaky tests on CI - - * Rename class name - -Test -~~~~ -- Test: Add unit tests for application_tests (#454) [Manoj Kumar] - - * Add unit tests for application_tests - - * change body values to be empty -- Test: add Unit tests currentPackage (#453) [Manoj Kumar] -- Test: add unit test unlock (#450) [Manoj Kumar] -- Ci: try run all scripts and exit 1 when something fails (#431) - [Kazuaki Matsuo] - - * try run all scripts and exit 1 when something fails - - * ignore link in Python 3.7 because of runtime error - -Other -~~~~~ -- Docs: Minor fix in README (#445) [Aliakbar] -- AndroidKey class for Key Codes added. (#443) [Aliakbar] - - * AndroidKey class for Key Codes added. - - AndroidKey enum from java client ported. Instead of using unreadable numbers in code we can use these constant in order to write more readable code. - - * Android native key test - - Test for native key module which contains key codes for android keys. - - * Fixed # sign in comment instead of * - - * Change returns - - Instead of `if` and two return statements. - - * Used AndroidKey.XXX instead of numbers in tests - - * Make fuctions similar to is_gamepad_button - - Used a similar sentence format for similar functions as is_gamepad_button. - - * Make function names as is in java-client - - * Underscore in the beginning of constant removed -- Run unittest with python3.8 (#433) [Mori Atsushi] -- Bump 0.47. [Kazuaki Matsuo] -- Update changelog for 0.47. [Kazuaki Matsuo] - - -v0.47 (2019-08-22) ------------------- - -New -~~~ -- Add events property (#429) [Dan Graham] - - * add GET_SESSION - - * add events property, this property will get the current information of the session and get the events timings - - * add method for getting session_capabilities - - * update docstring - - * apply isort -- Add screenrecord unittest (#426) [Mori Atsushi] - - * Fix wrong docstring - - * Add screen_record unittest - - * Rename class names - - * Move test files - - * Fix docstring -- Add videoFilters option documentation (#419) [Mykola Mokhnach] -- Add remote_fs unittest (#410) [Mori Atsushi] - - * Add test_push_file unittest - - * Add test_pull_file unittest - - * Add remote_fs error cases unittest - -Fix -~~~ -- CI doesn't fail even if autopep8 makes changes (#422) [Mori Atsushi] - - * Fix: CI doesn't fail even if autopep8 makes changes - - * Fix: CI failure - -Other -~~~~~ -- Change altitude optional as arg for set_location (#415) [Mori Atsushi] - - * Change altitude optional as arg for set_location - - * Add comments - - * review comments -- Update docstring (#407) [Mori Atsushi] - - * Remove import error on pycharm - - And update docstring - - * Update docstring - - * Update docstring - - * Fix import error - - * fix - - * fix import order - - * tweak -- Update changelog for 0.46. [Kazuaki Matsuo] - - -v0.46 (2019-06-27) ------------------- -- Bump 0.46. [Kazuaki Matsuo] -- Bug fix joining path in _get_main_script (#408) [Nicholas Frederick] -- Update changelog for 0.45. [Kazuaki Matsuo] - - -v0.45 (2019-06-26) ------------------- - -New -~~~ -- Add execute driver (#406) [Kazuaki Matsuo] - - * add execute driver - - * append docstring -- Add how to solve pipenv error to readme (#403) [Mori Atsushi] - - * Add how to solve pipenv error to readme - - * review comments - - * tweak - - * review comments -- Add autocompletion for pycharm (#404) [Mori Atsushi] - - * Add autocompletion for pycharm - - * Removed flaky tests from running -- Add unit test for open_notifications (#398) [tabatask] - -Other -~~~~~ -- Bump 0.45. [Kazuaki Matsuo] -- Moving reset method from WebDriver to Applications (#399) [Mayura] -- Run android functional tests on ci (#396) [Mori Atsushi] - - * Add android functional test to ci - - * Add missing param - - * Add run_test template - - * Fixed: test running failed - - * Fixed - - * Fixed - - * fixed - - * Add run_android_test - - * Changed emulator to Nexus6 - - * Run all android tests - - * fixed - - * Resolve python-dateutil dependency - - * Run on 3 workers - - * Add chromedriver installation - - * Skip failed test cases on ci - - * fixed - - * Extend adbExecTimeout - - * Add script source to comment - - * Run 5 workers for android - - * Use Node11 - - * Extend wait time - - * Reduced running android functional tests - - * Revert some changes -- Use the same format for docstring (#395) [Mori Atsushi] - - * Update docstring - - * Update docstring - - * Update docstring - - * tweak - - * tweak - - * tweak - - * tweak - - * tweak - - * Update docstring - - * Update docstring - - * Update docstring - - * Update docstring - - * tweak - - * Update -- Publish functional test report (#394) [Mori Atsushi] - - * Move functional tests to template - - * Add publish_test_result - - * Fix typo -- Divide functional appium tests into each module(iOS) (#391) [Mori - Atsushi] - - * Divide ios appium_tests to each module - - * Fix test file name - - * Add CI status badge -- Run iOS functional tests on azure pipelines (#390) [Mori Atsushi] - - * Set up CI with Azure Pipelines - - * review comments - - * update README -- Update changelog for 0.44. [Kazuaki Matsuo] - - -v0.44 (2019-05-24) ------------------- - -Fix -~~~ -- Installed selenium4 when 'setup.py install' (#389) [Mori Atsushi] - - * Fix: installed selenium4 when setup.py install - - * Keep existing comparison operator -- Fix ios functional tests failed (#385) [Mori Atsushi] - - * Fix safari test(iOS) - - * Fix: find_by_ios_predicate - - * Delete find_by_uiautomation_tests - - since uiautomation is deprecated - - * Move non test files - - * Replace test app with the latest - - * Fix tests failed along to replaced test app - - * review comments - -Other -~~~~~ -- Bump 0.44. [Kazuaki Matsuo] -- Support get_display_density (#388) [Mori Atsushi] - - * Support get_display_density - - * Add get_display_density unittest - - * Add api doc - - * Add return description to api doc -- Support set_network_speed (#386) [Mori Atsushi] - - * Support set_nework_speed - - * Add set_network_speed unittest - - * Add api doc - - * revert unexpected change - - * revert change -- Update changelog for 0.43. [Kazuaki Matsuo] - - -v0.43 (2019-05-18) ------------------- - -New -~~~ -- Add assertions for w3c (#384) [Kazuaki Matsuo] -- Add isort to pre-commit (#379) [Mori Atsushi] - - * Add isort to pre-commit - - * Add isort.conf - - * Applied isort for test/unit - - * Add current dir to isrot arg - - * Add check to ci.sh - - * Use exit code for condition check in ci.sh - -Fix -~~~ -- Fix functional tests failed (android, push_file) (#375) [Mori - Atsushi] - - * Fix: test_push_file - - * Move remove_fs tests - - * Move teardown process - - * Delete selendroid test - - * tweak - - * Update along to review comments - - * Replace double quote with single quote under android dir - - * Remove creating tmp file - - * tweak -- Fix functional tests failed (android, ime/multi_action) (#372) [Mori - Atsushi] - - * Fix test failed: ime_tests, multi_action_tests - - * revert change and add impl for python3 - - * Remove py3 dependency - - * Change deepcopy to copy - - * Update ime_tests -- Fix functional tests failed (android, touch_action) (#374) [Mori - Atsushi] - - * Fix: test_drag_and_drop - - * Fix: test_long_press - - * Fix: long_press_x_y, swipe - - * Fix: press_and_wait - - * Fix: driver_drag_and_drop - - * Tweak - - * Add SLEEPY_TIME - - * Remove set with sleep and find_element - -Other -~~~~~ -- Bump 0.43. [Kazuaki Matsuo] -- [RD-34891] Assign w3c property on the command executor. (#382) - [Erustus Agutu] -- Get rid of sessionId (#383) [Kazuaki Matsuo] -- Divide functional appium tests into each module(android) (#378) [Mori - Atsushi] - - * Move non test files - - * Divide appium_tests into each module tests(android) - - * Skip contexts, find_by_image tests - - * Removed unnecessary codes -- Introduced pipfile (#376) [Mori Atsushi] - - * Added Pipfile - - Just created by pipenv install -r ci-requirements.txt - - * Introduced pipenv - - * Add Pipfile.lock to gitignore - - * Cover any minor versions for packages -- Move android commands to android package (#371) [Mori Atsushi] - - * Reorder mobilecommands - - * Move android commands to android package - - * Update setup.py to include added packages - - * Changed find_packages to whitelist style -- Update changelog for 0.42. [Kazuaki Matsuo] - - -v0.42 (2019-05-10) ------------------- - -New -~~~ -- Add return value. [Atsushi Mori] -- Add set_power_ac unittest. [Atsushi Mori] -- Added set_power_capacity unittest. [Atsushi Mori] - -Fix -~~~ -- Fix functional tests failed (android, appium_tests) (#366) [Mori - Atsushi] - - * Fix test failed: test_send_keys, test_screen_record - - * Fix test failed: test_update_settings - - * Fix test failed: test_start_activity_other_app - - * Move and rename helper package - - * Update along to review comments - - * Add return value to wait_for_element -- Fix poll_url in Python 3 (#370) [Kazuaki Matsuo] -- Fix functional tests failed (#364) [Mori Atsushi] - - * Fix test failed: element_location_in_view, set_text - - * Fix test failed: test_push_file - - * Merge test_pull_test into test_push_test - - * Fix test failed: test_pull_folder - - * Enable running by both py2 and py3 - - * Removed unnecessary codes - - * Remove magic number - -Other -~~~~~ -- Bump 0.42. [Kazuaki Matsuo] -- Support get_performance_data, get_performance_data_types (#368) [Mori - Atsushi] - - * Support get_performance_data, get_performance_data_types - - * Add api doc - - * Add performance unittest - - * Tweak - - * Update api doc -- Support set_gsm_voice (#367) [Mori Atsushi] - - * Support set_gsm_voice - - * Add set_gsm_voice unittest - - * Fix typo -- Support get_system_bars (#363) [Mori Atsushi] - - * Support get_system_bars - - * Add api doc - - * Add get_system_bars unittest - - * Remove FIXME -- Support make_gsm_call (#360) [Mori Atsushi] - - * Move const to gsm_signal_strength - - * Support make_gsm_call - - * Add make_gsm_call unittest - - * Move const to gsm class - - * Move get_dict_const to common.helper - - * Rename func - - * Use OrderedDict to keep defined order -- Support set_gsm_signal (#357) [Mori Atsushi] - - * Support set_gsm_signal - - * Fix: NONE_OR_UNKNOWN doesn't work - - * Add set_gsm_signal unittest - - * Use int for signal strength const - - * Raise exception when signal strength is out of range - - * Fix: wrong class name - - * Removed args validation - - Since arg validation already done by server side - - * Show warning log when arg is out of range - - * Some changes for less maintenance -- Mobile:pinchOpen and mobile:pinchClose no longer implemented in appium - drivers (#358) [Jonah] -- Remove unnecessary codes. [Atsushi Mori] -- Replace 'on' with AC_ON. [Atsushi Mori] -- Update api doc. [Atsushi Mori] -- Define AC_OFF, AC_ON as const. [Atsushi Mori] -- Skip pylint warnings. [Atsushi Mori] -- Update api doc. [Atsushi Mori] -- Support set_power_ac. [Atsushi Mori] -- Support set_power_capacity. [Atsushi Mori] -- Update changelog for 0.41. [Kazuaki Matsuo] -- Bump 0.41. [Kazuaki Matsuo] - - -v0.41 (2019-04-23) ------------------- - -New -~~~ -- Add send sms support (#351) [Mori Atsushi] - - * Support sendSms function - - * Added api doc - - * Add sms unittest - - * Revert unexpected changes - - * Update api doc -- Add pixelFormat in docstring (#346) [Kazuaki Matsuo] -- Add fingerprint unittest (#345) [Mori Atsushi] -- Add shake unittest (#344) [Mori Atsushi] - -Fix -~~~ -- Fix True/False in image settings, add boolean value in settings test - (#352) [Kazuaki Matsuo] - - * Fix True/False in image settings, add boolean value in settings test - - * use is for boolean - -Other -~~~~~ -- Make keep alive True by default (#348) [Kazuaki Matsuo] -- Move settings to mixin classes (#347) [Mori Atsushi] -- Update changelog for 0.40. [Kazuaki Matsuo] - - -v0.40 (2019-03-14) ------------------- - -Fix -~~~ -- Fix RuntimeError: maximum recursion depth exceeded in cmp happened - (#343) [Kazuaki Matsuo] - - * fix maximum recursion depth exceeded in sub classes - - * add docstring - - * add comparison of a number of commands - - * use issubclass to ensure the class is sub - -Other -~~~~~ -- Bump 0.40. [Kazuaki Matsuo] -- Update missing changelog in 0.39. [Kazuaki Matsuo] - - -v0.39 (2019-02-27) ------------------- - -New -~~~ -- Add direct connect flag to be able to handle directConnectXxxxc (#338) - [Kazuaki Matsuo] - - * add direct connect feature - - * rmeove todo - - * update readme, extract _update_command_executor - - * add logger - - * make log level info - - * show warning if no directConnectXxxxx in dict - - * tweak error message - - * tweak message format -- Add datamatcher (#335) [Kazuaki Matsuo] - - * add datamatcher - - * add zero case - - * defines search context for driver and element - -Other -~~~~~ -- Update changelog for 0.38. [Kazuaki Matsuo] -- Bump 0.38. [Kazuaki Matsuo] - - -v0.38 (2019-02-11) ------------------- -- Bump 0.38. [Kazuaki Matsuo] -- Remove io.open from getting version code (#334) [Kazuaki Matsuo] - - * remove io.open - - * remove appium module from release script - - -v0.37 (2019-02-10) ------------------- - -New -~~~ -- Add AppiumConnection to customise user agent (#327) [Kazuaki Matsuo] -- Add a test for reset (#326) [Kazuaki Matsuo] -- Add a simple class to control Appium execution from the client code - (#324) [Mykola Mokhnach] -- Add pressure option (#322) [Kazuaki Matsuo] - - * add pressure option - - * add a test, tweak comment and the method - - * fix typo -- Add a test case using another session id (#320) [Kazuaki Matsuo] - -Fix -~~~ -- Fix passing options to screen record commands (#330) [Mykola Mokhnach] - -Other -~~~~~ -- Cast set_location arguments to float (#332) [Mykola Mokhnach] -- Update changelog for 0.36. [Kazuaki MATSUO] -- Bump 0.36. [Kazuaki MATSUO] - - -v0.36 (2019-01-18) ------------------- -- Bump 0.36. [Kazuaki MATSUO] -- Import keyboard, add tests (#319) [Kazuaki Matsuo] -- Update changelog for 0.35. [Kazuaki MATSUO] - - -v0.35 (2019-01-17) ------------------- - -New -~~~ -- Add location unittest (#317) [Mori Atsushi] - - * Add test_location - - * Add test_set_location - - * Add test_toggle_location_services -- Add settings unittest (#315) [Mori Atsushi] - - * Add settings unittest - - * Remove unused import -- Added format to device_time as argument (#312) [Mori Atsushi] -- Add devicetime unittest (#309) [Mori Atsushi] - - * Add device time test - - * Removed unnecessary check from device time test - - * Changed assertion for device time test - - Along to review comments - - * Changed quote for string from double to single -- Add activities unittest (#310) [Tadashi Nemoto] - - * Add test_start_activity - - * Add current_activity and wait_activity - - * Fix pytest 4.0.2 - - * Add test_start_activity_with_opts - - * Added options -- Add network unittest (#308) [Mori Atsushi] - - * Add network connection test - - * Added set network connection test - - * Add toggle wifi test - - * Removed unnecessary codes from toggle wifi test - - * Changed assertion for set network connection test -- Add touch action unittest (#306) [Tadashi Nemoto] - - * Add press test - - * Add test_long_press - - * Add test_wait - - * Add remaining tests - - * Add tap - - * 10 -> 9 - - * Modify based on comment -- Add precommit (#304) [Kazuaki Matsuo] - - * add pre-commit hook - -Fix -~~~ -- Fixing broken pypi long description rendering (#303) [Prabhash] - - reference: https://packaging.python.org/guides/making-a-pypi-friendly-readme - - Tested at https://pypi.org/project/delayed-assert -- Fix overridden mixin method call (#297) [Mykola Mokhnach] - -Other -~~~~~ -- Bump 0.35. [Kazuaki MATSUO] -- Move device_time to a mixin class (#314) [Mori Atsushi] -- Define getting httpretty request body decoded by utf-8 (#313) [Kazuaki - Matsuo] - - * define httpretty_last_request_body - - * replace the order - - * update - - * rename -- Move action and keyboard helpers to mixin classes (#307) [Mykola - Mokhnach] -- Extract more webdriver methods into specialized mixin classes (#302) - [Mykola Mokhnach] -- Move specialized method groups to mixin classes (#301) [Mykola - Mokhnach] -- Update changelog for 0.34. [Kazuaki MATSUO] - - -v0.34 (2018-12-18) ------------------- - -Fix -~~~ -- Fix missing package, missing commands and a test (#296) [Kazuaki - Matsuo] - - * add extensions into package - - * add tests for context to make sure it loads - - * move command definition from extensions to root - -Other -~~~~~ -- Bump 0.34. [Kazuaki MATSUO] -- Update changelog for 0.33. [Kazuaki MATSUO] - - -v0.33 (2018-12-18) ------------------- - -New -~~~ -- Add newline in release script because of autopep8 (#292) [Kazuaki - Matsuo] - -Other -~~~~~ -- Bump 0.33. [Kazuaki MATSUO] -- Move read version (#294) [Kazuaki Matsuo] -- Update changelog for 0.32. [Kazuaki MATSUO] - - -v0.32 (2018-12-18) ------------------- - -New -~~~ -- Add unit tests for isLocked Library (#288) [Venkatesh Singh] - - * Add unit tests for isLocked Lib - - * moved isLocked library tests in lock.py -- Add unit test for lock lib (#287) [Venkatesh Singh] - - * Add unit test for lock lib - -Fix -~~~ -- Fixed few failing tests in appium_tests.py (#278) - [RajeshkumarAyyadurai] - - * fixed few failing tests in appium_tests.py - - * updated few tests in appium_tests.py by removing uiautomator strategy -- Fixed failing tests in find_by_accessibility_id_tests.py. - [RajeshkumarAyyadurai] - -Other -~~~~~ -- Bump 0.32. [Kazuaki MATSUO] -- Split driver methods into mixin classes (#291) [Mykola Mokhnach] -- Run with tox on travis (#290) [Kazuaki Matsuo] - - * run with tox on travis - - * update readme -- Improve pytest, adding pytest.ini and set default arguments (#284) - [Kazuaki Matsuo] -- Extract bytes and add a test for set clipboard (#282) [Kazuaki Matsuo] - - * extract bytes and add a test for set clipboard -- Introduce httpretty for unittest to mock Appium server (#281) [Kazuaki - Matsuo] - - * add httpretty - - * add clipboard tests as an example - - * add test for forceMjsonwp -- Update setup elements (#280) [Kazuaki Matsuo] - - * update setup elements - - * remove docgen since we can use markdown format in pypi -- Release automation (#276) [Kazuaki Matsuo] -- Updated requirements.txt file with version (#275) - [RajeshkumarAyyadurai] - - * updated required dependecies with version number as a best practice - - * updated required dependencies with version - - * updated pylint library version to support for python 2.7 -- Append document for recording screen (#271) [Kazuaki Matsuo] - - * append document for recording screen - - * add since appium 1.10.0 - - * remove Only works for real devices since the feature can work on both -- Update changelog for 0.31. [Kazuaki MATSUO] - - -v0.31 (2018-11-21) ------------------- -- V0.31. [Kazuaki MATSUO] -- Driver.push_file(destination_path, source_path) feature (#270) [Javon - Davis] - - * used base64 library for conversion - - * remove unnecessary library use - - * changed text in test file - - * * Using context when reading file - * changed docstring format - * Catch error thrown if file not present and present user with a better message - - * fixed incorrect file path in test - - * removed change in pul_file that broke backwards compat and updated docstring description for `destination_path` - - -v0.30 (2018-10-31) ------------------- - -New -~~~ -- Add release section in readme. [Kazuaki MATSUO] - -Fix -~~~ -- Fix python3 set_clipboard error (#267) [Kazuaki Matsuo] - - * fix python3 set_clipboard error - - * apply formatter - -Other -~~~~~ -- V0.30. [Kazuaki MATSUO] - - -v0.29 (2018-10-30) ------------------- - -New -~~~ -- Add an endpoint for pressing buttons (#262) [Alex] -- Add custom locator strategy (#260) [Jonathan Lipps] -- Add a duration for scroll for ios (#256) [Kazuaki Matsuo] - - * add a duration for scroll for ios - - * tweak default duration - - * apply autoformat - - * set 600 duration by default if it's w3c spec - - * skip wait if duration is none - - * add comment -- Add finger print (#252) [Kazuaki Matsuo] - - * add fingre print - - * apply auto format -- Add find_elements w3c for webelement (#251) [Kazuaki Matsuo] - - * add find_elements w3c for webelement - - * add tests for child elements - - * add todo for future work -- Add a github issue template (#250) [Kazuaki Matsuo] -- Add xdist port handling (#248) [Kazuaki Matsuo] - - * add handling port number to run ios tests in parallel - - * define PytestXdistWorker - - * use gw0 if the number of worker is over the count of workers -- Add autopep8 (#243) [Kazuaki Matsuo] - - * apply autopep8 - - * add development section as the first draft - - * relax max-line-length - - * add global-config -- Add toggle wifi command (#241) [joshuazhusince1986] - - * add toggle_wifi command - - * update comment to indicate toggle_wifi is only for Android -- Add selenium into ci-requirements (#240) [Kazuaki Matsuo] - - fix pylint - - add --py3k -- Add travis to run pylint and unit tests (#239) [Kazuaki Matsuo] - - * add pylint - - * add rcfile - - * tweak pylint - - * fix lint - - * add running pytest - - * tweak indentations -- Add tag view for android (#238) [Kazuaki Matsuo] - - * add tag view for android - - * fix typo... and tweak names of arguments - - * tweak docstring - - * add find element by viewtag section in readme - -Other -~~~~~ -- V0.29. [Kazuaki MATSUO] -- Bump selenium 3.14.1, call RemoteCommand without workaround (#259) - [Kazuaki Matsuo] - - * bump selenium 3.14.1, call RemoteCommand without workaround - - * make attributeValue check safe - - * define str = basestring for Python 2 - - * apply formatter - - * add missing value check -- Update obsolete link for mobile json wire protocol spec. (#257) - [Andrei Petre] -- Remove always_match and use first_match instead (#246) [Kazuaki - Matsuo] - - remove always_match and use first_match instead -- Use normal element for find image by (#236) [Kazuaki Matsuo] - - * use normal element - - * get rid of png - - * get rid of imagelement.py - - * apply formatter -- Typo fix: finiding -> finding (#245) [Andrew Fuller] -- Tweak PyPi URLs and add a badge (#232) [Kazuaki Matsuo] - - -v0.28 (2018-07-13) ------------------- - -Fix -~~~ -- Fix base64 encoded string (#231) [Kazuaki Matsuo] - -Other -~~~~~ -- V0.28. [Isaac Murchie] - - -v0.27 (2018-07-10) ------------------- - -New -~~~ -- Add support for is keyboard shown command. [Jonathan Lipps] -- Add find by image commands and tests (#224) [Jonathan Lipps] - - * add find by image commands and tests - - * remove and ignore pytest cache files - - * address review comments - - * fix docstrings -- Add flags argument to press_keycode (#222) [Mykola Mokhnach] - - * Add flags argument to press_keycode - - * Add flags to long press as well -- Add an endpoint for getting battery info (#217) [Mykola Mokhnach] -- Add wrappers for OpenCV-based image comparison (#216) [Mykola - Mokhnach] - - * Add wrappers for OpenCV-based image comparison - - * Tune some docs -- Add clipboard handlers (#209) [Mykola Mokhnach] - - * Add clipboard handlers - - * Fix documentation - - * fix options notation -- Add applications management endpoint handlers (#204) [Mykola Mokhnach] -- Add methods for start/stop screen record API endpoints (#201) [Mykola - Mokhnach] - - * Add methods for start/stop screen record API endpoints - - * Fix typo - - * Add a separate test for Android and get rid of redundant stuff - - * Tune documentation - - * Add videoSize arg description - - * Fix arg name -- Add appium prefix in create session and fix find_elements for W3C - (#196) [Kazuaki Matsuo] - - * add appium prefix in create session - - * fix find_elements by w3c for Appium - - * introduce forceMjsonwp - - * refine a bit - - * fix some tests - - * update the docset -- Add endpoints for lock/unlock. [Mykola Mokhnach] - -Other -~~~~~ -- V0.27. [Isaac Murchie] -- Set None as default value to lock device (#227) [Miguel Hernández] - - * Set 0 as default value to lock device - - * Set None as default value instead of 0 -- Avoid setting coordinates to null for touch actions (#214) [Mykola - Mokhnach] -- Change QUERY_APP_STATE request type to POST (#205) [Mykola Mokhnach] - - -v0.26 (2018-01-09) ------------------- -- V0.26. [Isaac Murchie] - - -v0.25 (2018-01-09) ------------------- - -New -~~~ -- Add method for getting current package. [Isaac Murchie] -- Add tests for ios class chain and rename methods a bit. [Kazuaki - MATSUO] -- Add class chain. [Kazuaki MATSUO] -- Add toggleTouchIdEnrollment. [Dan Graham] - -Fix -~~~ -- Fix typos in the README. [Mel Shafer] - -Other -~~~~~ -- V0.25. [Isaac Murchie] -- Only if key_name, key, and strategy are None do we need to set the - strategy to 'tapOutside'. This change allows setting just the strategy - to some other value, like 'swipeDown'. (#181) [Daniel Freer] -- Correct a wording. [Kazuaki MATSUO] -- Create README.md. [Kazuaki Matsuo] -- Append class chain related descriptions. [Kazuaki MATSUO] -- Update README to include instructions for using iOS predicates. [Emil - Petersen] -- Update docs for UIAutomation selector to include version requirement. - [Emil Petersen] - - -v0.24 (2016-12-20) ------------------- - -New -~~~ -- Added test cases for clear and find elements by ios predicate string. - [ben.zhou] -- Added clear to driver. Added find elements by ios predicate string. - [ben.zhou] - -Other -~~~~~ -- V0.24. [Isaac Murchie] -- DontStopAppOnReset instead of stopAppOnReset. [s.zubov] - - -v0.23 (2016-11-10) ------------------- - -New -~~~ -- Added touchId to driver (#143) [Dan Graham] - - * Added touchId to driver - - Wrote a test for it (still need help running Python tests though). Updated capabilities to use iOS 10.1 - -Other -~~~~~ -- V0.23. [Isaac Murchie] - - -v0.22 (2016-03-16) ------------------- -- V0.22. [Isaac Murchie] -- Use id instead of elementId. [Isaac Murchie] - - -v0.21 (2016-01-20) ------------------- - -New -~~~ -- Add device_time property. [Isaac Murchie] - -Fix -~~~ -- Fix saucetestcase to run under Python3. [Ling Lin] - - The module 'new' was removed. Instead of new.newclass, use type(). - -Other -~~~~~ -- V0.21. [Isaac Murchie] -- Update README.md. [tophercf] - - smallest win in history - - -v0.20 (2015-10-12) ------------------- -- V0.20. [Isaac Murchie] -- Revert actions change. [Isaac Murchie] - - -v0.19 (2015-10-09) ------------------- -- V0.19. [Isaac Murchie] -- Change 'actions' to 'gestures' in single action. [Isaac Murchie] - - -v0.18 (2015-10-07) ------------------- - -New -~~~ -- Add string file argument to driver.app_strings. [Isaac Murchie] -- Add wait_activity method for webdriver. [zhaoqifa] -- Add el.location_in_view method. [Isaac Murchie] - -Fix -~~~ -- Fixed typographical error, changed accomodate to accommodate in - README. [orthographic-pedant] -- Fix bug with monkeypatching. [Isaac Murchie] -- Fix to issue #71. [James Salt] -- Fix start_activity for Python 3.x. [Artur Tanistra] -- Fix start_activity for Python3. [Isaac Murchie] - -Other -~~~~~ -- V0.18. [Isaac Murchie] -- Remove dependency on enum. [Isaac Murchie] -- Bump version. [Isaac Murchie] -- Use WebDriverWait to implement wait_activity. [zhaoqifa] -- Make tap duration be handled as ms, not s. [Isaac Murchie] -- Bump version. [Isaac Murchie] -- Bump version. [Isaac Murchie] -- Move monkeypatched set_value into WebElement. [Isaac Murchie] - - -v0.14 (2015-03-06) ------------------- - -Fix -~~~ -- Fix issue with single tap. [Isaac Murchie] -- Fix handling of sauce test case so ImportError is suppressed. [Isaac - Murchie] - -Other -~~~~~ -- Bump version. [Isaac Murchie] -- Bump version. [Isaac Murchie] - - -v0.12 (2015-01-13) ------------------- - -New -~~~ -- Add base class for Sauce tests. [Isaac Murchie] -- Add remaining optional arguments to start_activity method. [Isaac - Murchie] - -Fix -~~~ -- Fix package names for starting activity. [Isaac Murchie] - -Other -~~~~~ -- Bump version. [Isaac Murchie] -- Update README.md. [Mikhail Martin] - - Missing dot causes errors. -- Update webdriver.py. [urtow] - - -v0.11 (2014-11-14) ------------------- - -New -~~~ -- Add toggle_location_services. [Isaac Murchie] - -Other -~~~~~ -- Bump version. [Isaac Murchie] -- Update webdriver.py. [urtow] - - Start_y - y-coordinate for start, not end - - -v0.10 (2014-09-24) ------------------- - -New -~~~ -- Added start_activity and tests. [Eric Millin] -- Added 'keyevent' since it is needed for Selendroid. [Payman Delshad] -- Add set_text method for Android. [Isaac Murchie] - -Other -~~~~~ -- Bump version. [Isaac Murchie] -- Removed complex_find, added get_settings, update_settings. [Jonah - Stiennon] -- Make long_press works with 'duration' parameter. [ianxiaohanxu] - - Add a new parameter 'duration = None' to _get_opts -- Typo fix! [Cass] -- Update README.md. [Johan Lundstroem] - - Verison -> Version -- Revert "Fix for #23: Re-add 'keyevent' temporarily." [Payman Delshad] - - This reverts commit ccbcaf809704bf1ac752d1b4446d1175b7434c36. - - -v0.9 (2014-07-07) ------------------ - -New -~~~ -- Add some more tests, fix others. [Isaac Murchie] -- Add ConnectionType enum. [Isaac Murchie] -- Add methods for Android ime access. [Isaac Murchie] -- Add network connection methods. [Isaac Murchie] -- Add strategy to hide_keyboard. [Isaac Murchie] -- Add necessary ios attributes. [Brad Pitcher] -- Add pull_file method. [Isaac Murchie] -- Add support for open_notifications. [Isaac Murchie] -- Add optional argument 'language' to app_strings. [Isaac Murchie] -- Add context method for simplicity. [Isaac Murchie] -- Add find methods to WebElement. [Isaac Murchie] -- Add reset and hide_keyboard. [Isaac Murchie] -- Add PyPi packaging setup. [Isaac Murchie] -- Add miscellaneous methods. [Isaac Murchie] -- Add touch and multi touch. [Isaac Murchie] -- Add accessibility id locator strategy. [Isaac Murchie] -- Add Android UIAutomator locator strategy. [Isaac Murchie] -- Add iOS UIAutomation locator strategy. [Isaac Murchie] -- Add context methods. [Isaac Murchie] - -Fix -~~~ -- Fix for #23: Re-add 'keyevent' temporarily. [Payman Delshad] -- Fix keycode command. [Isaac Murchie] -- Fix for Python 3. [Isaac Murchie] -- Fix typos with context. [Alexander Bayandin] -- Fix typo in README (resolve #12) [Alexander Bayandin] -- Fix timing. [Isaac Murchie] -- Fix setup for egg distro, and add install instructions. [Isaac - Murchie] - -Other -~~~~~ -- Bump version. [Isaac Murchie] -- Bump version. [Isaac Murchie] -- Change call to single-gesture tap. [Isaac Murchie] -- Bump version. [Isaac Murchie] -- Renamed keyevent to press_keycode and added long_press_keycode. - [Payman Delshad] -- Bump version. [Isaac Murchie] -- Numerous fixes. [Alexander Bayandin] - - 1. fix comparation with None - 2. remove unused imports - 3. fix imports order (according to pep8) - 4. style fixes (according to pep8) - 5. another minor fixes -- Update zoom/pinch signatures. [Isaac Murchie] -- Remove tag name, use class. [Isaac Murchie] -- Don't send multitouch for single finger tap. [Isaac Murchie] -- Miscellaneous fixes. [Isaac Murchie] -- Update desired caps. [Isaac Murchie] -- Basic module structure. [Isaac Murchie] - - diff --git a/Makefile b/Makefile index 40af673d..4ed823f4 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,50 @@ .PHONY: Commands for developers .PHONY: check-all -check-all: ## Run all lint checks and unittest - @echo "[Notice] If you'd like to run commands with same env to CI, please run \`tox\`." - @bash ci.sh +check-all: check unittest -.PHONY: isort -isort: ## Run isort - python -m isort $(ARGS) -rc . +.PHONY: check +check: check-lint check-format -.PHONY: autopep8 -autopep8: ## Run autopep8 - python -m autopep8 $(ARGS) -a -r -i . +.PHONY: check-lint +check-lint: + uv run ruff check . + uv run mypy appium -.PHONY: pylint -pylint: ## Run pylint - # TODO Remove --disable=E1136 when no errors in py39 - python -m pylint $(ARGS) --rcfile .pylintrc appium test --disable=E1136 +.PHONY: check-format +check-format: + uv run ruff format --check . -.PHONY: mypy -mypy: ## Run mypy - python -m mypy appium test +.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 - python -m pytest test/unit/ + uv run pytest $(ARGS) test/unit/ .PHONY: help help: ## Display this help screen diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 17f15897..00000000 --- a/Pipfile +++ /dev/null @@ -1,28 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] -pre-commit = "~=2.6" - -[packages] -selenium = "~=3.141" - -autopep8 = "~=1.5" - -pytest = "~=6.0" -pytest-cov = "~=2.10" - -tox = "~=3.19" -tox-travis = "~=0.12" - -httpretty = "~=1.0" -python-dateutil = "~=2.8" -mock = "~=4.0" - -pylint = "~=2.5" -astroid = "~=2.4" -isort = "~=4.3" # TODO Can be 5> when pylint uses isort 5> - -mypy = "==0.782" diff --git a/README.md b/README.md index c1c99689..b7768faa 100644 --- a/README.md +++ b/README.md @@ -3,21 +3,9 @@ [![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) -[![Build Status](https://travis-ci.org/appium/python-client.svg?branch=master)](https://travis-ci.org/appium/python-client) -[![Build Status](https://dev.azure.com/AppiumCI/Appium%20CI/_apis/build/status/appium.python-client?branchName=master)](https://dev.azure.com/AppiumCI/Appium%20CI/_build/latest?definitionId=56&branchName=master) +[![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) -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://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md) -functionality to the Python language bindings, for use with the mobile testing -framework [Appium](https://appium.io). - -## Notice - -Since **v1.0.0**, only Python 3 is supported - -### developing version -[selenium-4](https://github.com/appium/python-client/tree/selenium-4) branch is a developing branch to switch base selenium client version from v3 to v4. The branch is available as pre-release versioning like `2.0.0.a0` via pypi. - -Main differences since current v1 is the v2 can connect to invalid SSL environment like self-certificated server. Please take a look at the branch's README for more details. +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). ## Getting the Appium Python client @@ -49,14 +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() +``` + +- [appium/webdriver/extensions/action_helpers.py](appium/webdriver/extensions/action_helpers.py) +- https://www.selenium.dev/documentation/webdriver/actions_api/ + ## Usage -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.org/project/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. +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 @@ -68,43 +167,127 @@ 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/3/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 = dict( - platformName='Android', - platformVersion='10', - automationName='uiautomator2', - deviceName='Android Emulator', - app=PATH('../../../apps/selendroid-test-app.apk') -) -self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) -el = self.driver.find_element_by_accessibility_id('item') -el.click() +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.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() ``` -```python -# iOS environment -import unittest -from appium import webdriver +### Available `options` -desired_caps = dict( - platformName='iOS', - platformVersion='13.4', - automationName='xcuitest', - deviceName='iPhone Simulator', - app=PATH('../../apps/UICatalog.app.zip') -) +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. -self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) -el = self.driver.find_element_by_accessibility_id('item') -el.click() -``` +Available options for each automation name below will help to check what options are already defined. +Please use proper options for your automaiton usage. + +`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 @@ -118,89 +301,244 @@ If your Selenium/Appium server decorates the new session capabilities response w Then python client will switch its endpoint to the one specified by the values of those keys. ```python -import unittest 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 + +`strict_ssl` option allows you to send commands to an invalid certificate host like a self-signed one. + +```python +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) +``` + +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 +from appium import webdriver + +from selenium.webdriver.remote.client_config import ClientConfig + +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) +``` -desired_caps = dict( - platformName='iOS', - platformVersion='13.4', - automationName='xcuitest', - deviceName='iPhone Simulator', - app=PATH('../../apps/UICatalog.app.zip') +## Set custom `AppiumConnection` + +The first argument of `webdriver.Remote` can set an arbitrary command executor for you. + +1. Set init arguments for the pool manager Appium Python client uses to manage HTTP requests. + +```python +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 ) -self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, direct_connection=True) +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) ``` + +2. Define a subclass of `AppiumConnection` + +```python +from appium import webdriver +from appium.options.ios import XCUITestOptions + +from appium.webdriver.appium_connection import AppiumConnection + +class CustomAppiumConnection(AppiumConnection): + # Can add your own methods for the custom class + pass + +custom_executor = CustomAppiumConnection(remote_server_addr='http://127.0.0.1:4723') + +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) + +``` + +The `AppiumConnection` can set `selenium.webdriver.remote.client_config.ClientConfig` as well. + +## 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. + +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)) + ## Documentation -https://appium.github.io/python-client-sphinx/ is detailed documentation +- https://appium.github.io/python-client-sphinx/ is detailed documentation +- [functional tests](test/functional) also may help to see concrete examples. ## Development - Code Style: [PEP-0008](https://www.python.org/dev/peps/pep-0008/) - - Apply `autopep8`, `isort` and `mypy` as pre commit hook + - 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) -- `gitchangelog` generates `CHANGELOG.rst` ### Setup -- `pip install --user pipenv` -- `python -m pipenv lock --clear` - - If you experience `Locking Failed! unknown locale: UTF-8` error, then refer [pypa/pipenv#187](https://github.com/pypa/pipenv/issues/187) to solve it. -- `python -m pipenv install --dev --system` -- `pre-commit install` +```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`: + +```bash +uv venv --python +``` + +where `` is the actual Python version, for example `3.12`. + +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: + +```bash +uv venv /venv/root/folder +``` + +In order to activate the newly created virtual environment you may either source it: + +```bash +source /venv/root/folder/bin/activate +``` + +or add it to PATH: + +```bash +export "PATH=/venv/root/folder/bin:$PATH" +``` + +### Linting And Formatting -### Run tests +Run linter and format checks -You can run all of tests running on CI via `tox` in your local. +```bash +make check +``` + +Address autofixable linter and formatting issues ```bash -$ tox +make fix ``` -You also can run particular tests like below. +### Testing #### Unit ```bash -$ pytest test/unit +make unittest ``` -Run with `pytest-xdist` +Run in parallel (2 threads) ```bash -$ pytest -n 2 test/unit +make unittest ARGS="-n 2" ``` #### Functional ```bash -$ pytest test/functional/ios/search_context/find_by_ios_class_chain_tests.py +uv run pytest test/functional/ios/search_context/find_by_ios_class_chain_tests.py ``` #### In parallel for iOS -1. Create simulators named 'iPhone 8 - 8100' and 'iPhone 8 - 8101' -2. Install test libraries via pip, `pip install pytest pytest-xdist` -3. Run tests +1. Create simulators named 'iPhone X - 8100' and 'iPhone X - 8101' +1. Run tests ```bash -$ pytest -n 2 test/functional/ios/search_context/find_by_ios_class_chain_tests.py +uv run pytest -n 2 test/functional/ios/search_context/find_by_ios_class_chain_tests.py ``` ## Release -Follow below steps. +In case you need to release a version manually. ```bash -$ pip install twine -$ pip install git+git://github.com/vaab/gitchangelog.git # Getting via GitHub repository is necessary for Python 3.7 -# Type the new version number and 'yes' if you can publish it -# You can test the command with DRY_RUN -$ DRY_RUN=1 ./release.sh -$ ./release.sh # release +rm -rf dist +# bumping the version, building a package and creating a tag. +uv run semantic-release version --patch|--minor|--major + +# To publish the version's module to pypi +UV_PUBLISH_TOKEN= uv publish + +# To push built modules in 'dist' directory to the GH release page. +uv run semantic-release publish ``` ## License diff --git a/appium/common/exceptions.py b/appium/common/exceptions.py index 8d9593b5..3fe1712e 100644 --- a/appium/common/exceptions.py +++ b/appium/common/exceptions.py @@ -24,4 +24,3 @@ class NoSuchContextException(InvalidSwitchToTargetException): print(driver.contexts) """ - pass diff --git a/appium/common/helper.py b/appium/common/helper.py index 1a8a358e..1565b96e 100644 --- a/appium/common/helper.py +++ b/appium/common/helper.py @@ -12,6 +12,7 @@ # 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 @@ -26,12 +27,16 @@ def extract_const_attributes(cls: type) -> Dict[str, Any]: Returns: dict with constants attributes and values in the class """ - return dict([(attr, value) for attr, value in vars(cls).items() - if not callable(getattr(cls, attr)) and attr.isupper()]) + 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 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/helper/__init__.py b/appium/options/__init__.py similarity index 100% rename from test/functional/android/helper/__init__.py rename to appium/options/__init__.py 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/test/functional/android/search_context/__init__.py b/appium/options/android/common/__init__.py similarity index 100% rename from test/functional/android/search_context/__init__.py rename to appium/options/android/common/__init__.py diff --git a/test/functional/ios/search_context/__init__.py b/appium/options/android/common/adb/__init__.py similarity index 100% rename from test/functional/ios/search_context/__init__.py rename to appium/options/android/common/adb/__init__.py 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/webdriver/py.typed b/appium/options/android/common/app/__init__.py similarity index 100% rename from appium/webdriver/py.typed rename to appium/options/android/common/app/__init__.py 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/test/functional/android/device_time_tests.py b/appium/protocols/webdriver/can_execute_commands.py similarity index 66% rename from test/functional/android/device_time_tests.py rename to appium/protocols/webdriver/can_execute_commands.py index b5d576fb..de4f1b4a 100644 --- a/test/functional/android/device_time_tests.py +++ b/appium/protocols/webdriver/can_execute_commands.py @@ -1,5 +1,4 @@ #!/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. @@ -13,13 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dateutil.parser import parse +from typing import Dict, Protocol, Union -from .helper.test_helper import BaseTestCase +from selenium.webdriver.remote.remote_connection import RemoteConnection -class TestDeviceTime(BaseTestCase): - def test_device_time(self) -> None: - date_time = self.driver.device_time - # convert to date ought to work - parse(date_time) +class CanExecuteCommands(Protocol): + command_executor: RemoteConnection + + def execute(self, driver_command: str, params: Union[Dict, None] = None) -> Dict: ... diff --git a/test/functional/ios/remote_fs_tests.py b/appium/protocols/webdriver/can_execute_scripts.py similarity index 58% rename from test/functional/ios/remote_fs_tests.py rename to appium/protocols/webdriver/can_execute_scripts.py index 89b42568..1d04f6ca 100644 --- a/test/functional/ios/remote_fs_tests.py +++ b/appium/protocols/webdriver/can_execute_scripts.py @@ -12,16 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os +from typing import Any, List, Optional, Protocol -from test.functional.ios.helper.test_helper import BaseTestCase +class CanExecuteScripts(Protocol): + def pin_script(self, script: str, script_key: Optional[Any] = None) -> Any: ... -class TestRemoteFs(BaseTestCase): + def unpin(self, script_key: Any) -> None: ... - def test_push_file(self) -> None: - file_name = 'test_image.jpg' - source_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'file', file_name) - destination_path = file_name + def get_pinned_scripts(self) -> List[str]: ... - self.driver.push_file(destination_path, source_path=source_path) + def execute_script(self, script: str, *args: Any) -> Any: ... + + def execute_async_script(self, script: str, *args: Any) -> Any: ... diff --git a/appium/webdriver/extensions/search_context/base_search_context.py b/appium/protocols/webdriver/can_find_elements.py similarity index 53% rename from appium/webdriver/extensions/search_context/base_search_context.py rename to appium/protocols/webdriver/can_find_elements.py index 317069f0..07b0b827 100644 --- a/appium/webdriver/extensions/search_context/base_search_context.py +++ b/appium/protocols/webdriver/can_find_elements.py @@ -12,19 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=abstract-method - -from typing import TYPE_CHECKING, Dict, List, Union +from typing import TYPE_CHECKING, Dict, List, Protocol, Union, runtime_checkable if TYPE_CHECKING: from appium.webdriver.webelement import WebElement -class BaseSearchContext: - """Used by each search context. Dummy find_element/s are for preventing pylint error""" +@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) -> 'WebElement': - raise NotImplementedError + def find_element(self, by: str, value: Union[str, Dict, None] = None) -> 'WebElement': ... - def find_elements(self, by: str, value: Union[str, Dict] = None) -> List['WebElement']: - raise NotImplementedError + def find_elements(self, by: str, value: Union[str, Dict, None] = None) -> List['WebElement']: ... diff --git a/test/functional/android/log_event_tests.py b/appium/protocols/webdriver/can_remember_extension_presence.py similarity index 64% rename from test/functional/android/log_event_tests.py rename to appium/protocols/webdriver/can_remember_extension_presence.py index 9ad0b53d..1684788b 100644 --- a/test/functional/android/log_event_tests.py +++ b/appium/protocols/webdriver/can_remember_extension_presence.py @@ -1,5 +1,4 @@ #!/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. @@ -13,12 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .helper.test_helper import BaseTestCase +from typing import Protocol, TypeVar +T = TypeVar('T') -class TestLogEvent(BaseTestCase): - def test_log_event(self) -> None: - vendor = 'appium' - event = 'funEvent' - self.driver.log_event(vendor, event) - assert f'{vendor}:{event}' in self.driver.get_events().keys() + +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/version.py b/appium/version.py index 4eda78c4..0616a7c5 100644 --- a/appium/version.py +++ b/appium/version.py @@ -1 +1,22 @@ -version = '1.0.2' +#!/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 importlib import metadata + + +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 index f643a168..9ca31182 100644 --- a/appium/webdriver/appium_connection.py +++ b/appium/webdriver/appium_connection.py @@ -23,15 +23,43 @@ 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""" - headers = RemoteConnection.get_remote_connection_headers(parsed_url, keep_alive=keep_alive) - headers['User-Agent'] = 'appium/python {} ({})'.format(library_version(), headers['User-Agent']) + """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 - headers['X-Idempotency-Key'] = str(uuid.uuid4()) + cls.extra_headers[HEADER_IDEMOTENCY_KEY] = str(uuid.uuid4()) + else: + cls.extra_headers = _get_new_headers(HEADER_IDEMOTENCY_KEY, cls.extra_headers) - return 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 index 5bdacce1..93c926c6 100644 --- a/appium/webdriver/appium_service.py +++ b/appium/webdriver/appium_service.py @@ -13,121 +13,54 @@ # limitations under the License. import os +import re import subprocess as sp import sys import time -from typing import Any, List, Optional, Union +from typing import Any, Callable, List, Optional, Set -import urllib3 +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 = '/wd/hub/status' +STATUS_URL = '/status' +DEFAULT_BASE_PATH = '/' +HTTP_STATUS_ERROR = 400 -def find_executable(executable: str) -> Optional[str]: - path = os.environ['PATH'] - paths = path.split(os.pathsep) - base, 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 poll_url(host: str, port: int, path: str, timeout_ms: int) -> bool: - time_started_sec = time.time() - while time.time() < time_started_sec + timeout_ms / 1000.0: - try: - conn = urllib3.PoolManager(timeout=1.0) - resp = conn.request('HEAD', f'http://{host}:{port}{path}') - if resp.status < 400: - return True - except Exception: - pass - time.sleep(1.0) - return False +class AppiumServiceError(RuntimeError): + pass -class AppiumServiceError(RuntimeError): +class AppiumStartupError(RuntimeError): pass class AppiumService: def __init__(self) -> None: self._process: Optional[sp.Popen] = None - self._cmd: Optional[List] = None - - def _get_node(self) -> str: - if not hasattr(self, '_node_executable'): - self._node_executable = find_executable('node') - if self._node_executable is None: - raise AppiumServiceError('NodeJS main executable cannot be found. ' + - 'Make sure it is installed and present in PATH') - return self._node_executable - - def _get_npm(self) -> str: - if not hasattr(self, '_npm_executable'): - self._npm_executable = find_executable('npm.cmd' if sys.platform == 'win32' else 'npm') - if self._npm_executable is None: - raise AppiumServiceError('Node Package Manager executable cannot be found. ' + - 'Make sure it is installed and present in PATH') - return self._npm_executable - - def _get_main_script(self) -> Union[str, bytes]: - if not hasattr(self, '_main_script'): - for args in [['root', '-g'], ['root']]: - try: - modules_root = sp.check_output([self._get_npm()] + args).strip().decode('utf-8') - if os.path.exists(os.path.join(modules_root, MAIN_SCRIPT_PATH)): - self._main_script: Union[str, bytes] = os.path.join(modules_root, MAIN_SCRIPT_PATH) - break - except sp.CalledProcessError: - continue - if not hasattr(self, '_main_script'): - try: - self._main_script = sp.check_output( - [self._get_node(), - '-e', - 'console.log(require.resolve("{}"))'.format(MAIN_SCRIPT_PATH)]).strip() - except sp.CalledProcessError as e: - raise AppiumServiceError(e.output) from e - return self._main_script - - @staticmethod - def _parse_port(args: List[str]) -> int: - for idx, arg in enumerate(args or []): - if arg in ('--port', '-p') and idx < len(args) - 1: - return int(args[idx + 1]) - return DEFAULT_PORT - - @staticmethod - def _parse_host(args: List[str]) -> str: - for idx, arg in enumerate(args or []): - if arg in ('--address', '-a') and idx < len(args) - 1: - return args[idx + 1] - return DEFAULT_HOST + 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. - Keyword Args: + 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 by default. + 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. @@ -136,39 +69,59 @@ def start(self, **kwargs: Any) -> sp.Popen: 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 this is not set + (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 + 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 = kwargs['node'] if 'node' in kwargs else self._get_node() + 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 - main_script = kwargs['main_script'] if 'main_script' in kwargs else self._get_main_script() - args = [node, main_script] + 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) - host = self._parse_host(args) - port = self._parse_port(args) error_msg: Optional[str] = None - if not self.is_running or (timeout_ms > 0 and not poll_url(host, port, STATUS_URL, timeout_ms)): - error_msg = f'Appium has failed to start on {host}:{port} within {timeout_ms}ms timeout' + 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)}' @@ -176,58 +129,202 @@ def start(self, **kwargs: Any) -> sp.Popen: raise AppiumServiceError(error_msg) return self._process - def stop(self) -> bool: + 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 """ - is_terminated = False + was_running = False if self.is_running: - self._process.terminate() # type: ignore - is_terminated = True + 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 is_terminated + return was_running @property def is_running(self) -> bool: """Check if the service is running. - Returns: - bool: `True` if the service is running + :return: `True` if the service is running """ - return self._process is not None and self._process.poll() is None + 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 values can be customized by providing --address/--port - command line arguments while starting the service. + The default host/port/base path values can be customized by providing + --address/--port/--base-path command line arguments while starting the service. Returns: - bool: `True` if the service is running and listening on the given/default host/port + `True` if the service is running and listening on the given/default host/port """ - if not self.is_running or self._cmd is None: + if not self.is_running: return False - host = self._parse_host(self._cmd) - port = self._parse_port(self._cmd) - return self.is_running and poll_url(host, port, STATUS_URL, 1000) + + 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) + 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) + assert not service.is_running + assert not service.is_listening 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/test/functional/android/location_tests.py b/appium/webdriver/command_method.py similarity index 71% rename from test/functional/android/location_tests.py rename to appium/webdriver/command_method.py index 30cd7880..dde45cb4 100644 --- a/test/functional/android/location_tests.py +++ b/appium/webdriver/command_method.py @@ -1,5 +1,4 @@ #!/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. @@ -13,9 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .helper.test_helper import BaseTestCase +import enum -class TestLocation(BaseTestCase): - def test_toggle_location_services(self) -> None: - self.driver.toggle_location_services() # TODO Add assert +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/mobileby.py b/appium/webdriver/common/appiumby.py similarity index 54% rename from appium/webdriver/common/mobileby.py rename to appium/webdriver/common/appiumby.py index 2034275f..371573ab 100644 --- a/appium/webdriver/common/mobileby.py +++ b/appium/webdriver/common/appiumby.py @@ -12,18 +12,43 @@ # 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 MobileBy(By): +class AppiumBy(By): IOS_PREDICATE = '-ios predicate string' - IOS_UIAUTOMATION = '-ios uiautomation' IOS_CLASS_CHAIN = '-ios class chain' ANDROID_UIAUTOMATOR = '-android uiautomator' ANDROID_VIEWTAG = '-android viewtag' ANDROID_DATA_MATCHER = '-android datamatcher' ANDROID_VIEW_MATCHER = '-android viewmatcher' - WINDOWS_UI_AUTOMATION = '-windows uiautomation' 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 6cb4f4d7..00000000 --- a/appium/webdriver/common/multi_action.py +++ /dev/null @@ -1,89 +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 typing import TYPE_CHECKING, Dict, List, Optional, TypeVar, Union - -from appium.webdriver.mobilecommand import MobileCommand as Command - -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - from appium.webdriver.webelement import WebElement - from appium.webdriver.common.touch_action import TouchAction - -T = TypeVar('T', bound='MultiAction') - - -class MultiAction: - def __init__(self, driver: 'WebDriver', element: Optional['WebElement'] = None) -> None: - self._driver = driver - self._element = element - self._touch_actions: List['TouchAction'] = [] - - def add(self, *touch_actions: 'TouchAction') -> None: - """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) - - Returns: - `MultiAction`: Self instance - """ - for touch_action in touch_actions: - if self._touch_actions is None: - self._touch_actions = [] - - self._touch_actions.append(copy.copy(touch_action)) - - def perform(self: T) -> T: - """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() - - Returns: - `MultiAction`: Self instance - """ - 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) -> Dict[str, Union[List, str]]: - 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} - return {'actions': actions} diff --git a/appium/webdriver/common/touch_action.py b/appium/webdriver/common/touch_action.py deleted file mode 100644 index e068d499..00000000 --- a/appium/webdriver/common/touch_action.py +++ /dev/null @@ -1,187 +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`. - -# pylint: disable=no-self-use - -import copy -from typing import TYPE_CHECKING, Dict, List, Optional, TypeVar, Union - -from appium.webdriver.mobilecommand import MobileCommand as Command - -if TYPE_CHECKING: - from appium.webdriver.webelement import WebElement - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound='TouchAction') - - -class TouchAction: - - def __init__(self, driver: Optional['WebDriver'] = None): - self._driver = driver - self._actions: List = [] - - def tap(self: T, element: Optional['WebElement'] = None, x: Optional[int] - = None, y: Optional[int] = None, count: int = 1) -> T: - """Perform a tap action on the element - - Args: - element: the element to tap - x : x coordinate to tap, relative to the top left corner of the element. - y : y coordinate. If y is used, x must also be set, and vice versa - - Returns: - `TouchAction`: Self instance - """ - opts = self._get_opts(element, x, y) - opts['count'] = count - self._add_action('tap', opts) - - return self - - def press(self: T, el: Optional['WebElement'] = None, x: Optional[int] = None, - y: Optional[int] = None, pressure: Optional[float] = None) -> T: - """Begin a chain with a press down action at a particular element or point - - Args: - el: the element to press - x: x coordiate to press. If y is used, x must also be set - y: y coordiate to press. If x is used, y must also be set - pressure: [iOS Only] press as force touch. Read the description of `force` property on Apple's UITouch class - (https://developer.apple.com/documentation/uikit/uitouch?language=objc) for more details on possible value ranges. - - Returns: - `TouchAction`: Self instance - """ - self._add_action('press', self._get_opts(el, x, y, pressure=pressure)) - - return self - - def long_press(self: T, el: Optional['WebElement'] = None, x: Optional[int] - = None, y: Optional[int] = None, duration: int = 1000) -> T: - """Begin a chain with a press down that lasts `duration` milliseconds - - Args: - el: the element to press - x: x coordiate to press. If y is used, x must also be set - y: y coordiate to press. If x is used, y must also be set - duration: Duration to press - - Returns: - `TouchAction`: Self instance - """ - self._add_action('longPress', self._get_opts(el, x, y, duration)) - - return self - - def wait(self: T, ms: int = 0) -> T: - """Pause for `ms` milliseconds. - - Args: - ms: The time to pause - - Returns: - `TouchAction`: Self instance - """ - if ms is None: - ms = 0 - - opts = {'ms': ms} - - self._add_action('wait', opts) - - return self - - def move_to(self: T, el: Optional['WebElement'] = None, x: Optional[int] = None, y: Optional[int] = None) -> T: - """Move the pointer from the previous point to the element or point specified - - Args: - el: the element to be moved to - x: x coordiate to be moved to. If y is used, x must also be set - y: y coordiate to be moved to. If x is used, y must also be set - - Returns: - `TouchAction`: Self instance - """ - self._add_action('moveTo', self._get_opts(el, x, y)) - - return self - - def release(self: T) -> T: - """End the action by lifting the pointer off the screen - - Returns: - `TouchAction`: Self instance - """ - self._add_action('release', {}) - - return self - - def perform(self: T) -> T: - """Perform the action by sending the commands to the server to be operated upon - - Returns: - `TouchAction`: Self instance - """ - if self._driver is None: - raise ValueError('Set driver to constructor as a argument when to create the instance.') - 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) -> List[Dict]: - gestures = [] - for action in self._actions: - gestures.append(copy.deepcopy(action)) - return gestures - - def _add_action(self, action: str, options: Dict) -> None: - gesture = { - 'action': action, - 'options': options, - } - self._actions.append(gesture) - - def _get_opts(self, el: Optional['WebElement'] = None, x: Optional[int] = None, y: Optional[int] = None, - duration: Optional[int] = None, pressure: Optional[float] = None) -> Dict[str, Union[int, float]]: - opts = {} - if el is not None: - opts['element'] = el.id - - # it makes no sense to have x but no y, or vice versa. - if x is not None and y is not None: - opts['x'] = x - opts['y'] = y - - if duration is not None: - opts['duration'] = duration - - if pressure is not None: - opts['pressure'] = pressure - - return opts diff --git a/appium/webdriver/errorhandler.py b/appium/webdriver/errorhandler.py index 8612d86a..b2a5cab9 100644 --- a/appium/webdriver/errorhandler.py +++ b/appium/webdriver/errorhandler.py @@ -12,20 +12,114 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict +import json +from typing import Any, Dict, List, Sequence, Type, Union -from selenium.common.exceptions import WebDriverException +import selenium.common.exceptions as sel_exceptions from selenium.webdriver.remote import errorhandler -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: Dict) -> None: - try: - super().check_response(response) - except WebDriverException as wde: - if wde.msg == 'No such context found.': - raise NoSuchContextException(wde.msg, wde.screen, wde.stacktrace) from wde - 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/action_helpers.py b/appium/webdriver/extensions/action_helpers.py index 37628440..9c370922 100644 --- a/appium/webdriver/extensions/action_helpers.py +++ b/appium/webdriver/extensions/action_helpers.py @@ -12,31 +12,31 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, List, Optional, Tuple, TypeVar, Union +from typing import TYPE_CHECKING, List, Optional, Tuple, cast -from selenium import webdriver +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.common.multi_action import MultiAction -from appium.webdriver.common.touch_action import TouchAction from appium.webdriver.webelement import WebElement if TYPE_CHECKING: # noinspection PyUnresolvedReferences from appium.webdriver.webdriver import WebDriver -T = TypeVar('T', bound=Union['WebDriver', 'ActionHelpers']) - -class ActionHelpers(webdriver.Remote): - - def scroll(self: T, origin_el: WebElement, destination_el: WebElement, duration: Optional[int] = None) -> T: +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 being scrolling - destination_el: the element to scroll to - duration: a duration after pressing originalEl and move the element to destinationEl. - Default is 600 ms for W3C spec. Zero for MJSONWP. + 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) @@ -44,33 +44,47 @@ def scroll(self: T, origin_el: WebElement, destination_el: WebElement, duration: Returns: Union['WebDriver', 'ActionHelpers']: Self instance """ - # XCUITest x W3C spec has no duration by default in server side - if self.w3c and duration is None: + if duration is None: duration = 600 - action = TouchAction(self) - if duration is None: - action.press(origin_el).move_to(destination_el).release().perform() - else: - action.press(origin_el).wait(duration).move_to(destination_el).release().perform() + 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: T, origin_el: WebElement, destination_el: WebElement) -> T: + 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 """ - action = TouchAction(self) - action.long_press(origin_el).move_to(destination_el).release().perform() + 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: T, positions: List[Tuple[int, int]], duration: Optional[int] = None) -> T: + 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 @@ -86,30 +100,41 @@ def tap(self: T, positions: List[Tuple[int, int]], duration: Optional[int] = Non Union['WebDriver', 'ActionHelpers']: Self instance """ if len(positions) == 1: - action = TouchAction(self) + 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: - action.long_press(x=x, y=y, duration=duration).release() + actions.w3c_actions.pointer_action.pause(duration / 1000) else: - action.tap(x=x, y=y) - action.perform() + actions.w3c_actions.pointer_action.pause(0.1) + actions.w3c_actions.pointer_action.release() + actions.perform() else: - ma = MultiAction(self) + finger = 0 + actions = ActionChains(cast('WebDriver', self)) + actions.w3c_actions.devices = [] + for position in positions: + finger += 1 x = position[0] y = position[1] - action = TouchAction(self) + + # 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: - action.long_press(x=x, y=y, duration=duration).release() + new_input.create_pause(duration / 1000) else: - action.press(x=x, y=y).release() - ma.add(action) - - ma.perform() + new_input.create_pause(0.1) + new_input.create_pointer_up(MouseButton.LEFT) + actions.perform() return self - def swipe(self: T, start_x: int, start_y: int, end_x: int, end_y: int, duration: int = 0) -> T: + 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: @@ -117,7 +142,7 @@ def swipe(self: T, start_x: int, start_y: int, end_x: int, end_y: int, duration: 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: time to take the swipe, in ms. + 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) @@ -125,18 +150,20 @@ def swipe(self: T, start_x: int, start_y: int, end_x: int, end_y: int, duration: Returns: Union['WebDriver', 'ActionHelpers']: Self instance """ - # `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() + 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: T, start_x: int, start_y: int, end_x: int, end_y: int) -> T: + def flick(self, start_x: int, start_y: int, end_x: int, end_y: int) -> Self: """Flick from one point to another point. Args: @@ -151,10 +178,11 @@ def flick(self: T, start_x: int, start_y: int, end_x: int, end_y: int) -> T: Returns: Union['WebDriver', 'ActionHelpers']: Self instance """ - action = TouchAction(self) - action \ - .press(x=start_x, y=start_y) \ - .move_to(x=end_x, y=end_y) \ - .release() - action.perform() + 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/activities.py b/appium/webdriver/extensions/android/activities.py index 9a81485b..76da521b 100644 --- a/appium/webdriver/extensions/android/activities.py +++ b/appium/webdriver/extensions/android/activities.py @@ -12,70 +12,31 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, TypeVar, Union - -from selenium import webdriver -from selenium.common.exceptions import TimeoutException +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 -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'Activities']) - - -class Activities(webdriver.Remote): - def start_activity(self: T, app_package: str, app_activity: str, **opts: str) -> T: - """Opens an arbitrary activity during a test. If the activity belongs to - another application, that application is started and the activity is opened. - - This is an Android-only method. - - Args: - app_package: The package containing the activity to start. - app_activity: The activity to start. - - Keyword Args: - app_wait_package (str): Begin automation after this package starts. - app_wait_activity (str): Begin automation after this activity starts. - intent_action (str): Intent to start. - intent_category (str): Intent category to start. - intent_flags (str): Flags to send to the intent. - optional_intent_arguments (str): Optional arguments to the intent. - dont_stop_app_on_reset (str): Should the app be stopped on reset? - """ - data = { - 'appPackage': app_package, - 'appActivity': app_activity - } - arguments = { - 'app_wait_package': 'appWaitPackage', - 'app_wait_activity': 'appWaitActivity', - 'intent_action': 'intentAction', - 'intent_category': 'intentCategory', - 'intent_flags': 'intentFlags', - 'optional_intent_arguments': 'optionalIntentArguments', - 'dont_stop_app_on_reset': 'dontStopAppOnReset' - } - for key, value in arguments.items(): - if key in opts: - data[value] = opts[key] - self.execute(Command.START_ACTIVITY, data) - return self +class Activities(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): @property - def current_activity(self: T) -> str: + def current_activity(self) -> str: """Retrieves the current activity running on the device. Returns: str: The current activity name running on the device """ - return self.execute(Command.GET_CURRENT_ACTIVITY)['value'] + 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: T, activity: str, timeout: int, interval: int = 1) -> bool: + 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. @@ -89,16 +50,16 @@ def wait_activity(self: T, activity: str, timeout: int, interval: int = 1) -> bo `True` if the target activity is shown """ try: - WebDriverWait(self, timeout, interval).until( - lambda d: d.current_activity == activity) + WebDriverWait(self, timeout, interval).until( # type: ignore[type-var] + lambda d: d.current_activity == activity + ) return True except TimeoutException: return False - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.GET_CURRENT_ACTIVITY] = \ - ('GET', '/session/$sessionId/appium/device/current_activity') - self.command_executor._commands[Command.START_ACTIVITY] = \ - ('POST', '/session/$sessionId/appium/device/start_activity') + 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 index 6c91ddea..fe75260b 100644 --- a/appium/webdriver/extensions/android/common.py +++ b/appium/webdriver/extensions/android/common.py @@ -12,60 +12,48 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, TypeVar, Union - -from selenium import webdriver +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 -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'Common']) - - -class Common(webdriver.Remote): - - def end_test_coverage(self: T, intent: str, path: str) -> Any: # TODO Check return type - """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/writing-running-appium/android/android-coverage.md - Args: - intent: description of operation to be performed - path: path to coverage.ec file to be pulled from the device - - Returns: - TODO - """ - data = { - 'intent': intent, - 'path': path, - } - return self.execute(Command.END_TEST_COVERAGE, data)['value'] - - def open_notifications(self: T) -> T: +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 """ - self.execute(Command.OPEN_NOTIFICATIONS, {}) + 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: T) -> str: - """Retrieves the current package running on the device. - """ - return self.execute(Command.GET_CURRENT_PACKAGE)['value'] - - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.GET_CURRENT_PACKAGE] = \ - ('GET', '/session/$sessionId/appium/device/current_package') - self.command_executor._commands[Command.END_TEST_COVERAGE] = \ - ('POST', '/session/$sessionId/appium/app/end_test_coverage') - self.command_executor._commands[Command.OPEN_NOTIFICATIONS] = \ - ('POST', '/session/$sessionId/appium/device/open_notifications') + 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 index 136770e5..28abdcbb 100644 --- a/appium/webdriver/extensions/android/display.py +++ b/appium/webdriver/extensions/android/display.py @@ -12,22 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, TypeVar, Union - -from selenium import webdriver +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 -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'Display']) - -class Display(webdriver.Remote): - - def get_display_density(self: T) -> int: +class Display(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def get_display_density(self) -> int: """Get the display density, Android only Returns: @@ -39,10 +33,16 @@ def get_display_density(self: T) -> int: Return: int: The display density """ - return self.execute(Command.GET_DISPLAY_DENSITY)['value'] - - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.GET_DISPLAY_DENSITY] = \ - ('GET', '/session/$sessionId/appium/device/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 index ecd2840f..ed43d3c6 100644 --- a/appium/webdriver/extensions/android/gsm.py +++ b/appium/webdriver/extensions/android/gsm.py @@ -12,20 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, TypeVar, Union - -from selenium import webdriver +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 -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'Gsm']) - class GsmCallActions: CALL = 'call' @@ -52,9 +48,8 @@ class GsmVoiceState: ON = 'on' -class Gsm(webdriver.Remote): - - def make_gsm_call(self: T, phone_number: str, action: str) -> T: +class Gsm(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def make_gsm_call(self, phone_number: str, action: str) -> Self: """Make GSM call (Emulator only) Android only. @@ -70,15 +65,22 @@ def make_gsm_call(self: T, phone_number: str, action: str) -> T: 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)') - self.execute(Command.MAKE_GSM_CALL, {'phoneNumber': phone_number, 'action': action}) + 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: T, strength: int) -> T: + def set_gsm_signal(self, strength: int) -> Self: """Set GSM signal strength (Emulator only) Android only. @@ -93,15 +95,23 @@ def set_gsm_signal(self: T, strength: int) -> T: 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)') - self.execute(Command.SET_GSM_SIGNAL, {'signalStrength': strength, 'signalStrengh': strength}) + 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: T, state: str) -> T: + def set_gsm_voice(self, state: str) -> Self: """Set GSM voice state (Emulator only) Android only. @@ -116,20 +126,22 @@ def set_gsm_voice(self: T, state: str) -> T: 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)') - self.execute(Command.SET_GSM_VOICE, {'state': state}) + 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 - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.MAKE_GSM_CALL] = \ - ('POST', '/session/$sessionId/appium/device/gsm_call') - self.command_executor._commands[Command.SET_GSM_SIGNAL] = \ - ('POST', '/session/$sessionId/appium/device/gsm_signal') - self.command_executor._commands[Command.SET_GSM_VOICE] = \ - ('POST', '/session/$sessionId/appium/device/gsm_voice') + 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 index 79973dba..05b55879 100644 --- a/appium/webdriver/extensions/android/nativekey.py +++ b/appium/webdriver/extensions/android/nativekey.py @@ -867,7 +867,7 @@ class AndroidKey: TV_SATELLITE_SERVICE = 240 # Key code constant: Toggle Network key. - # Toggles selecting broacast services. + # Toggles selecting broadcast services. TV_NETWORK = 241 # Key code constant: Antenna/Cable key. @@ -1010,14 +1010,39 @@ class AndroidKey: # 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] + 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: @@ -1031,9 +1056,19 @@ 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] + 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: @@ -1041,13 +1076,35 @@ def is_media_key(code: int) -> bool: 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] + 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: diff --git a/appium/webdriver/extensions/android/network.py b/appium/webdriver/extensions/android/network.py index 29c0c047..6054e29d 100644 --- a/appium/webdriver/extensions/android/network.py +++ b/appium/webdriver/extensions/android/network.py @@ -12,20 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, TypeVar, Union - -from selenium import webdriver +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 -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'Network']) - class NetSpeed: GSM = 'gsm' # GSM/CSD (up: 14.4(kbps), down: 14.4(kbps)) @@ -39,18 +35,36 @@ class NetSpeed: FULL = 'full' # No limit, the default (up: 0.0, down: 0.0) -class Network(webdriver.Remote): +class NetworkMask: + WIFI = 0b010 + DATA = 0b100 + AIRPLANE_MODE = 0b001 + +class Network(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): @property - def network_connection(self: T) -> int: + 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` - """ - return self.execute(Command.GET_NETWORK_CONNECTION, {})['value'] - def set_network_connection(self: T, connection_type: int) -> int: + 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: @@ -71,29 +85,49 @@ def set_network_connection(self: T, connection_type: int) -> int: 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 """ - data = { - 'parameters': { - 'type': connection_type - } - } - return self.execute(Command.SET_NETWORK_CONNECTION, data)['value'] - - def toggle_wifi(self: T) -> T: + 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 """ - self.execute(Command.TOGGLE_WIFI, {}) + 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: T, speed_type: str) -> T: + def set_network_speed(self, speed_type: str) -> Self: """Set the network speed emulation. Android Emulator only. @@ -112,19 +146,30 @@ def set_network_speed(self: T, speed_type: str) -> T: 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)') - - self.execute(Command.SET_NETWORK_SPEED, {'netspeed': speed_type}) + 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 - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.TOGGLE_WIFI] = \ - ('POST', '/session/$sessionId/appium/device/toggle_wifi') - 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.SET_NETWORK_SPEED] = \ - ('POST', '/session/$sessionId/appium/device/network_speed') + 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 index fab80aa0..f5781cf3 100644 --- a/appium/webdriver/extensions/android/performance.py +++ b/appium/webdriver/extensions/android/performance.py @@ -12,23 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Dict, List, TypeVar, Union +from typing import Dict, List, Union -from selenium import webdriver +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 -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver -T = TypeVar('T', bound=Union['WebDriver', 'Performance']) - - -class Performance(webdriver.Remote): - - def get_performance_data(self: T, package_name: str, data_type: str, - data_read_timeout: int = None) -> List[List[str]]: +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. @@ -47,12 +44,17 @@ def get_performance_data(self: T, package_name: str, data_type: str, Returns: The data along to `data_type` """ - data: Dict[str, Union[str, int]] = {'packageName': package_name, 'dataType': data_type} - if data_read_timeout is not None: - data['dataReadTimeout'] = data_read_timeout - return self.execute(Command.GET_PERFORMANCE_DATA, data)['value'] - - def get_performance_data_types(self: T) -> List: + 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. @@ -63,13 +65,21 @@ def get_performance_data_types(self: T) -> List: Returns: Available data types """ - return self.execute(Command.GET_PERFORMANCE_DATA_TYPES)['value'] - - # pylint: disable=protected-access - - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.GET_PERFORMANCE_DATA] = \ - ('POST', '/session/$sessionId/appium/getPerformanceData') - self.command_executor._commands[Command.GET_PERFORMANCE_DATA_TYPES] = \ - ('POST', '/session/$sessionId/appium/performanceData/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 index 6b6d3d80..537ab680 100644 --- a/appium/webdriver/extensions/android/power.py +++ b/appium/webdriver/extensions/android/power.py @@ -12,24 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, TypeVar, Union - -from selenium import webdriver +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 -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'Power']) - - -class Power(webdriver.Remote): +class Power(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): AC_OFF, AC_ON = 'off', 'on' - def set_power_capacity(self: T, percent: int) -> T: + def set_power_capacity(self, percent: int) -> Self: """Emulate power capacity change on the connected emulator. Android only. @@ -43,10 +38,16 @@ def set_power_capacity(self: T, percent: int) -> T: Returns: Union['WebDriver', 'Power']: Self instance """ - self.execute(Command.SET_POWER_CAPACITY, {'percent': percent}) + 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: T, ac_state: str) -> T: + def set_power_ac(self, ac_state: str) -> Self: """Emulate power state change on the connected emulator. Android only. @@ -61,13 +62,19 @@ def set_power_ac(self: T, ac_state: str) -> T: Returns: Union['WebDriver', 'Power']: Self instance """ - self.execute(Command.SET_POWER_AC, {'state': ac_state}) + 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 - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.SET_POWER_CAPACITY] = \ - ('POST', '/session/$sessionId/appium/device/power_capacity') - self.command_executor._commands[Command.SET_POWER_AC] = \ - ('POST', '/session/$sessionId/appium/device/power_ac') + 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 index 4c8c44a3..f5769c56 100644 --- a/appium/webdriver/extensions/android/sms.py +++ b/appium/webdriver/extensions/android/sms.py @@ -12,22 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, TypeVar, Union - -from selenium import webdriver +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 -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'Sms']) - - -class Sms(webdriver.Remote): - def send_sms(self: T, phone_number: str, message: str) -> T: +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. @@ -42,11 +37,14 @@ def send_sms(self: T, phone_number: str, message: str) -> T: Returns: Union['WebDriver', 'Sms']: Self instance """ - self.execute(Command.SEND_SMS, {'phoneNumber': phone_number, 'message': message}) + 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 - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.SEND_SMS] = \ - ('POST', '/session/$sessionId/appium/device/send_sms') + 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 index cb309a17..a02c21f8 100644 --- a/appium/webdriver/extensions/android/system_bars.py +++ b/appium/webdriver/extensions/android/system_bars.py @@ -12,22 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Dict, TypeVar, Union +from typing import Dict, Union -from selenium import webdriver +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 -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver -T = TypeVar('T', bound=Union['WebDriver', 'SystemBars']) - - -class SystemBars(webdriver.Remote): - - def get_system_bars(self: T) -> Dict[str, Dict[str, Union[int, bool]]]: +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. @@ -47,10 +43,16 @@ def get_system_bars(self: T) -> Dict[str, Dict[str, Union[int, bool]]]: - width - height """ - return self.execute(Command.GET_SYSTEM_BARS)['value'] - - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.GET_SYSTEM_BARS] = \ - ('GET', '/session/$sessionId/appium/device/system_bars') + 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 index fc8ec424..b259422e 100644 --- a/appium/webdriver/extensions/applications.py +++ b/appium/webdriver/extensions/applications.py @@ -11,37 +11,40 @@ # WITHOUT WARRANTIES 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 typing import TYPE_CHECKING, Any, Dict, TypeVar, Union +from selenium.common.exceptions import InvalidArgumentException, UnknownMethodException +from typing_extensions import Self -from selenium import webdriver +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 -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver -T = TypeVar('T', bound=Union['WebDriver', 'Applications']) - - -class Applications(webdriver.Remote): - def background_app(self: T, seconds: int) -> T: +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 + 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 """ - data = { - 'seconds': seconds, - } - self.execute(Command.BACKGROUND, data) + 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: T, bundle_id: str) -> bool: + def is_app_installed(self, bundle_id: str) -> bool: """Checks whether the application specified by `bundle_id` is installed on the device. Args: @@ -50,12 +53,25 @@ def is_app_installed(self: T, bundle_id: str) -> bool: Returns: `True` if app is installed """ - data = { - 'bundleId': bundle_id, - } - return self.execute(Command.IS_APP_INSTALLED, data)['value'] - - def install_app(self: T, app_path: str, **options: Any) -> T: + 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: @@ -75,15 +91,25 @@ def install_app(self: T, app_path: str, **options: Any) -> T: Returns: Union['WebDriver', 'Applications']: Self instance """ - data: Dict[str, Any] = { - 'appPath': app_path, - } - if options: - data.update({'options': options}) - self.execute(Command.INSTALL_APP, data) + 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: T, app_id: str, **options: Any) -> T: + def remove_app(self, app_id: str, **options: Any) -> Self: """Remove the specified application from the device. Args: @@ -98,34 +124,25 @@ def remove_app(self: T, app_id: str, **options: Any) -> T: Returns: Union['WebDriver', 'Applications']: Self instance """ - data: Dict[str, Any] = { - 'appId': app_id, - } - if options: - data.update({'options': options}) - self.execute(Command.REMOVE_APP, data) - return self - - def launch_app(self: T) -> T: - """Start on the device the application specified in the desired capabilities. - - Returns: - Union['WebDriver', 'Applications']: Self instance - """ - self.execute(Command.LAUNCH_APP) + 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 close_app(self: T) -> T: - """Stop the running application, specified in the desired capabilities, on - the device. - - Returns: - Union['WebDriver', 'Applications']: Self instance - """ - self.execute(Command.CLOSE_APP) - return self - - def terminate_app(self: T, app_id: str, **options: Any) -> bool: + def terminate_app(self, app_id: str, **options: Any) -> bool: """Terminates the application if it is running. Args: @@ -138,14 +155,24 @@ def terminate_app(self: T, app_id: str, **options: Any) -> bool: Returns: True if the app has been successfully terminated """ - data: Dict[str, Any] = { - 'appId': app_id, - } - if options: - data.update({'options': options}) - return self.execute(Command.TERMINATE_APP, data)['value'] - - def activate_app(self: T, app_id: str) -> T: + 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. @@ -155,13 +182,21 @@ def activate_app(self: T, app_id: str) -> T: Returns: Union['WebDriver', 'Applications']: Self instance """ - data = { - 'appId': app_id, - } - self.execute(Command.ACTIVATE_APP, data) + 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: T, app_id: str) -> int: + def query_app_state(self, app_id: str) -> int: """Queries the state of the application. Args: @@ -171,60 +206,69 @@ def query_app_state(self: T, app_id: str) -> int: One of possible application state constants. See ApplicationState class for more details. """ - data = { - 'appId': app_id, - } - return self.execute(Command.QUERY_APP_STATE, data)['value'] - - def app_strings(self: T, language: str = None, string_file: str = None) -> Dict[str, str]: + 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 + 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 - return self.execute(Command.GET_APP_STRINGS, data)['value'] - - def reset(self: T) -> T: - """Resets the current application on the device. - - Returns: - Union['WebDriver', 'Applications']: Self instance - """ - self.execute(Command.RESET) - return self - - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - 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.TERMINATE_APP] = \ - ('POST', '/session/$sessionId/appium/device/terminate_app') - self.command_executor._commands[Command.ACTIVATE_APP] = \ - ('POST', '/session/$sessionId/appium/device/activate_app') - self.command_executor._commands[Command.QUERY_APP_STATE] = \ - ('POST', '/session/$sessionId/appium/device/app_state') - self.command_executor._commands[Command.GET_APP_STRINGS] = \ - ('POST', '/session/$sessionId/appium/app/strings') - self.command_executor._commands[Command.RESET] = \ - ('POST', '/session/$sessionId/appium/app/reset') - 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') + 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 index d2035657..f5354f2e 100644 --- a/appium/webdriver/extensions/clipboard.py +++ b/appium/webdriver/extensions/clipboard.py @@ -13,25 +13,23 @@ # limitations under the License. import base64 -from typing import TYPE_CHECKING, Optional, TypeVar, Union +from typing import Optional -from selenium import webdriver +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 -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver -T = TypeVar('T', bound=Union['WebDriver', 'Clipboard']) - - -class Clipboard(webdriver.Remote): - - def set_clipboard(self: T, content: bytes, content_type: str = ClipboardContentType.PLAINTEXT, - label: Optional[str] = None) -> T: +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: @@ -43,16 +41,21 @@ def set_clipboard(self: T, content: bytes, content_type: str = ClipboardContentT 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 - self.execute(Command.SET_CLIPBOARD, options) + 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: T, text: str, label: Optional[str] = None) -> T: + def set_clipboard_text(self, text: str, label: Optional[str] = None) -> Self: """Copies the given text to the system clipboard Args: @@ -62,11 +65,9 @@ def set_clipboard_text(self: T, text: str, label: Optional[str] = None) -> T: Returns: Union['WebDriver', 'Clipboard']: Self instance """ + return self.set_clipboard(bytes(str(text), 'UTF-8'), ClipboardContentType.PLAINTEXT, label) - self.set_clipboard(bytes(str(text), 'UTF-8'), ClipboardContentType.PLAINTEXT, label) - return self - - def get_clipboard(self: T, content_type: str = ClipboardContentType.PLAINTEXT) -> bytes: + def get_clipboard(self, content_type: str = ClipboardContentType.PLAINTEXT) -> bytes: """Receives the content of the system clipboard Args: @@ -74,14 +75,18 @@ def get_clipboard(self: T, content_type: str = ClipboardContentType.PLAINTEXT) - is supported on Android Returns: - base64-encoded string: Clipboard content. Or return an empty string if the clipboard is empty + Clipboard content as bytearray. Or empty bytes if the clipboard is empty """ - base64_str = self.execute(Command.GET_CLIPBOARD, { - 'contentType': content_type - })['value'] + 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: T) -> str: + def get_clipboard_text(self) -> str: """Receives the text of the system clipboard Returns: @@ -89,10 +94,14 @@ def get_clipboard_text(self: T) -> str: """ return self.get_clipboard(ClipboardContentType.PLAINTEXT).decode('UTF-8') - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.SET_CLIPBOARD] = \ - ('POST', '/session/$sessionId/appium/device/set_clipboard') - self.command_executor._commands[Command.GET_CLIPBOARD] = \ - ('POST', '/session/$sessionId/appium/device/get_clipboard') + 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 index a410bceb..628432e1 100644 --- a/appium/webdriver/extensions/context.py +++ b/appium/webdriver/extensions/context.py @@ -12,22 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, List, TypeVar, Union +from typing import List -from selenium import webdriver +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from ..mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver -T = TypeVar('T', bound=Union['WebDriver', 'Context']) - - -class Context(webdriver.Remote): +class Context(CanExecuteCommands): @property - def contexts(self: T) -> List[str]: + def contexts(self) -> List[str]: """Returns the contexts within the current session. Usage: @@ -40,7 +34,7 @@ def contexts(self: T) -> List[str]: return self.execute(Command.CONTEXTS)['value'] @property - def current_context(self: T) -> str: + def current_context(self) -> str: """Returns the current context of the current session. Usage: @@ -52,7 +46,7 @@ def current_context(self: T) -> str: return self.execute(Command.GET_CURRENT_CONTEXT)['value'] @property - def context(self: T) -> str: + def context(self) -> str: """Returns the current context of the current session. Usage: @@ -63,12 +57,7 @@ def context(self: T) -> str: """ return self.current_context - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - 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') + 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 index 3699f86b..22ac3c25 100644 --- a/appium/webdriver/extensions/device_time.py +++ b/appium/webdriver/extensions/device_time.py @@ -12,31 +12,33 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Optional, TypeVar, Union +from typing import Optional -from selenium import webdriver +from selenium.common.exceptions import UnknownMethodException -from ..mobilecommand import MobileCommand as Command - -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'DeviceTime']) +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(webdriver.Remote): +class DeviceTime(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): @property - def device_time(self: T) -> str: + def device_time(self) -> str: """Returns the date and time from the device. Return: str: The date and time """ - return self.execute(Command.GET_DEVICE_TIME_GET, {})['value'] + 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: T, format: Optional[str] = None) -> str: + def get_device_time(self, format: Optional[str] = None) -> str: """Returns the date and time from the device. Args: @@ -52,14 +54,22 @@ def get_device_time(self: T, format: Optional[str] = None) -> str: Return: str: The date and time """ + ext_name = 'mobile: getDeviceTime' if format is None: return self.device_time - return self.execute(Command.GET_DEVICE_TIME_POST, {'format': format})['value'] + 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'] - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.GET_DEVICE_TIME_GET] = \ - ('GET', '/session/$sessionId/appium/device/system_time') - self.command_executor._commands[Command.GET_DEVICE_TIME_POST] = \ - ('POST', '/session/$sessionId/appium/device/system_time') + 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 index b5b3f0be..698c0218 100644 --- a/appium/webdriver/extensions/execute_driver.py +++ b/appium/webdriver/extensions/execute_driver.py @@ -12,23 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Dict, Optional, TypeVar, Union +from typing import Any, Dict, Optional, Union -from selenium import webdriver +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from ..mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'ExecuteDriver']) - - -class ExecuteDriver(webdriver.Remote): +class ExecuteDriver(CanExecuteCommands): # TODO Inner class case - def execute_driver(self: T, script: str, script_type: str = 'webdriverio', timeout_ms: Optional[int] = None) -> Any: + 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. @@ -48,11 +41,10 @@ def execute_driver(self: T, script: str, script_type: str = 'webdriverio', timeo ExecuteDriver.Result: The result of the script. It has 'result' and 'logs' keys. Raises: - WebDriverException: If something error happenes in the script. The message has the original error message. + 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'] @@ -64,8 +56,5 @@ def __init__(self, res: Dict): response = self.execute(Command.EXECUTE_DRIVER, option)['value'] return Result(response) - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.EXECUTE_DRIVER] = \ - ('POST', '/session/$sessionId/appium/execute_driver') + 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 index 63972d27..8d78d418 100644 --- a/appium/webdriver/extensions/execute_mobile_command.py +++ b/appium/webdriver/extensions/execute_mobile_command.py @@ -12,20 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Dict, TypeVar, Union +from typing import Any, Dict -from selenium import webdriver +from typing_extensions import Self -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts -T = TypeVar('T', bound=Union['WebDriver', 'ExecuteMobileCommand']) - -class ExecuteMobileCommand(webdriver.Remote): - - def press_button(self: T, button_name: str) -> T: +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. @@ -39,14 +34,12 @@ def press_button(self: T, button_name: str) -> T: Union['WebDriver', 'ExecuteMobileCommand']: Self instance """ - data = { - 'name': button_name - } + data = {'name': button_name} self.execute_script('mobile: pressButton', data) return self @property - def battery_info(self: T) -> Dict[str, Any]: + def battery_info(self) -> Dict[str, Any]: """Retrieves battery information for the device under test. Returns: 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 index f2e5802f..b6bbb468 100644 --- a/appium/webdriver/extensions/hw_actions.py +++ b/appium/webdriver/extensions/hw_actions.py @@ -12,22 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union +from typing import Optional -from selenium import webdriver +from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self -from ..mobilecommand import MobileCommand as Command - -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'HardwareActions']) +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(webdriver.Remote): - def lock(self: T, seconds: Optional[int] = None) -> T: +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: @@ -39,40 +37,59 @@ def lock(self: T, seconds: Optional[int] = None) -> T: Returns: Union['WebDriver', 'HardwareActions']: Self instance """ - if seconds is None: - self.execute(Command.LOCK) - else: - self.execute(Command.LOCK, {'seconds': seconds}) - + 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: T) -> T: + def unlock(self) -> Self: """Unlock the device. No changes are made if the device is already locked. Returns: Union['WebDriver', 'HardwareActions']: Self instance """ - self.execute(Command.UNLOCK) + 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: T) -> bool: + def is_locked(self) -> bool: """Checks whether the device is locked. Returns: `True` if the device is locked """ - return self.execute(Command.IS_LOCKED)['value'] - - def shake(self: T) -> T: + 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 """ - self.execute(Command.SHAKE) + 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: T, match: bool) -> T: + def touch_id(self, match: bool) -> Self: """Simulate touchId on iOS Simulator Args: @@ -81,46 +98,52 @@ def touch_id(self: T, match: bool) -> T: Returns: Union['WebDriver', 'HardwareActions']: Self instance """ - data = { - 'match': match - } - self.execute(Command.TOUCH_ID, data) + self.execute_script( + 'mobile: sendBiometricMatch', + { + 'type': 'touchId', + 'match': match, + }, + ) return self - def toggle_touch_id_enrollment(self: T) -> T: + def toggle_touch_id_enrollment(self) -> Self: """Toggle enroll touchId on iOS Simulator Returns: Union['WebDriver', 'HardwareActions']: Self instance """ - self.execute(Command.TOGGLE_TOUCH_ID_ENROLLMENT) + is_enrolled = self.execute_script('mobile: isBiometricEnrolled') + self.execute_script('mobile: enrollBiometric', {'isEnabled': not is_enrolled}) return self - def finger_print(self: T, finger_id: int) -> Any: + 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) - - Returns: - TODO """ - return self.execute(Command.FINGER_PRINT, {'fingerprintId': finger_id})['value'] - - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.LOCK] = \ - ('POST', '/session/$sessionId/appium/device/lock') - self.command_executor._commands[Command.UNLOCK] = \ - ('POST', '/session/$sessionId/appium/device/unlock') - self.command_executor._commands[Command.IS_LOCKED] = \ - ('POST', '/session/$sessionId/appium/device/is_locked') - self.command_executor._commands[Command.SHAKE] = \ - ('POST', '/session/$sessionId/appium/device/shake') - self.command_executor._commands[Command.TOUCH_ID] = \ - ('POST', '/session/$sessionId/appium/simulator/touch_id') - self.command_executor._commands[Command.TOGGLE_TOUCH_ID_ENROLLMENT] = \ - ('POST', '/session/$sessionId/appium/simulator/toggle_touch_id_enrollment') - self.command_executor._commands[Command.FINGER_PRINT] = \ - ('POST', '/session/$sessionId/appium/device/finger_print') + 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 index 4c569433..04b8f114 100644 --- a/appium/webdriver/extensions/images_comparison.py +++ b/appium/webdriver/extensions/images_comparison.py @@ -12,22 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Dict, TypeVar, Union +from typing import Any, Dict, Union -from selenium import webdriver +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from ..mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver +Base64Payload = Union[str, bytes] -T = TypeVar('T', bound=Union['WebDriver', 'ImagesComparison']) - -class ImagesComparison(webdriver.Remote): - - def match_images_features(self: T, base64_image1: bytes, base64_image2: bytes, **opts: Any) -> Dict[str, Any]: +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 @@ -75,14 +70,15 @@ def match_images_features(self: T, base64_image1: bytes, base64_image2: bytes, * """ options = { 'mode': 'matchFeatures', - 'firstImage': base64_image1, - 'secondImage': base64_image2, - 'options': opts + '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: T, base64_full_image: bytes, base64_partial_image: bytes, - **opts: Any) -> Dict[str, Union[bytes, Dict]]: + 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. @@ -108,14 +104,15 @@ def find_image_occurrence(self: T, base64_full_image: bytes, base64_partial_imag """ options = { 'mode': 'matchTemplate', - 'firstImage': base64_full_image, - 'secondImage': base64_partial_image, - 'options': opts + '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: T, base64_image1: bytes, base64_image2: bytes, - **opts: Any) -> Dict[str, Union[bytes, Dict]]: + 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 @@ -139,14 +136,18 @@ def get_images_similarity(self: T, base64_image1: bytes, base64_image2: bytes, """ options = { 'mode': 'getSimilarity', - 'firstImage': base64_image1, - 'secondImage': base64_image2, - 'options': opts + 'firstImage': _adjust_image_payload(base64_image1), + 'secondImage': _adjust_image_payload(base64_image2), + 'options': opts, } return self.execute(Command.COMPARE_IMAGES, options)['value'] - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.COMPARE_IMAGES] = \ - ('POST', '/session/$sessionId/appium/compare_images') + 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/ime.py b/appium/webdriver/extensions/ime.py deleted file mode 100644 index 910d980a..00000000 --- a/appium/webdriver/extensions/ime.py +++ /dev/null @@ -1,104 +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. - -from typing import TYPE_CHECKING, List, TypeVar, Union - -from selenium import webdriver - -from ..mobilecommand import MobileCommand as Command - -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'IME']) - - -class IME(webdriver.Remote): - - @property - def available_ime_engines(self: T) -> List[str]: - """Get the available input methods for an Android device. - - Package and activity are returned (e.g., ['com.android.inputmethod.latin/.LatinIME']) - Android only. - - Returns: - :obj:`list` of :obj:`str`: The available input methods for an Android device - """ - return self.execute(Command.GET_AVAILABLE_IME_ENGINES, {})['value'] - - def is_ime_active(self: T) -> bool: - """Checks whether the device has IME service active. - Android only. - - Returns: - `True` if IME service is active - """ - return self.execute(Command.IS_IME_ACTIVE, {})['value'] - - def activate_ime_engine(self: T, engine: str) -> T: - """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') - - Returns: - Union['WebDriver', 'IME']: Self instance - """ - data = { - 'engine': engine - } - self.execute(Command.ACTIVATE_IME_ENGINE, data) - return self - - def deactivate_ime_engine(self: T) -> T: - """Deactivates the currently active IME engine on the device. - - Android only. - - Returns: - Union['WebDriver', 'IME']: Self instance - """ - self.execute(Command.DEACTIVATE_IME_ENGINE, {}) - return self - - @property - def active_ime_engine(self: T) -> str: - """Returns the activity and package of the currently active IME engine - (e.g., 'com.android.inputmethod.latin/.LatinIME'). - - Android only. - - Returns: - str: The activity and package of the currently active IME engine - """ - return self.execute(Command.GET_ACTIVE_IME_ENGINE, {})['value'] - - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - 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') diff --git a/appium/webdriver/extensions/keyboard.py b/appium/webdriver/extensions/keyboard.py index 83a97fbd..4640b116 100644 --- a/appium/webdriver/extensions/keyboard.py +++ b/appium/webdriver/extensions/keyboard.py @@ -12,23 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Dict, Optional, TypeVar, Union +from typing import Dict, Optional -from selenium import webdriver +from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self -from ..mobilecommand import MobileCommand as Command - -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'Keyboard']) +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(webdriver.Remote): - def hide_keyboard(self: T, key_name: Optional[str] = None, key: Optional[str] - = None, strategy: Optional[str] = None) -> T: +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 @@ -42,26 +39,37 @@ def hide_keyboard(self: T, key_name: Optional[str] = None, key: Optional[str] Returns: Union['WebDriver', 'Keyboard']: Self instance """ - 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.execute(Command.HIDE_KEYBOARD, data) + 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: T) -> bool: + def is_keyboard_shown(self) -> bool: """Attempts to detect whether a software keyboard is present Returns: `True` if keyboard is shown """ - return self.execute(Command.IS_KEYBOARD_SHOWN)['value'] + 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: T, keycode: int, metastate: Optional[int] = None) -> T: + def keyevent(self, keycode: int, metastate: Optional[int] = None) -> Self: """Sends a keycode to the device. Android only. @@ -74,15 +82,9 @@ def keyevent(self: T, keycode: int, metastate: Optional[int] = None) -> T: Returns: Union['WebDriver', 'Keyboard']: Self instance """ - data = { - 'keycode': keycode, - } - if metastate is not None: - data['metastate'] = metastate - self.execute(Command.KEY_EVENT, data) - return self + return self.press_keycode(keycode=keycode, metastate=metastate) - def press_keycode(self: T, keycode: int, metastate: Optional[int] = None, flags: Optional[int] = None) -> T: + 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 @@ -96,17 +98,20 @@ def press_keycode(self: T, keycode: int, metastate: Optional[int] = None, flags: Returns: Union['WebDriver', 'Keyboard']: Self instance """ - data = { - 'keycode': keycode, - } + ext_name = 'mobile: pressKey' + args = {'keycode': keycode} if metastate is not None: - data['metastate'] = metastate + args['metastate'] = metastate if flags is not None: - data['flags'] = flags - self.execute(Command.PRESS_KEYCODE, data) + 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: T, keycode: int, metastate: Optional[int] = None, flags: Optional[int] = None) -> T: + 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 @@ -120,26 +125,44 @@ def long_press_keycode(self: T, keycode: int, metastate: Optional[int] = None, f Returns: Union['WebDriver', 'Keyboard']: Self instance """ - data = { - 'keycode': keycode - } + ext_name = 'mobile: pressKey' + args = {'keycode': keycode} if metastate is not None: - data['metastate'] = metastate + args['metastate'] = metastate if flags is not None: - data['flags'] = flags - self.execute(Command.LONG_PRESS_KEYCODE, data) + 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 - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.HIDE_KEYBOARD] = \ - ('POST', '/session/$sessionId/appium/device/hide_keyboard') - self.command_executor._commands[Command.IS_KEYBOARD_SHOWN] = \ - ('GET', '/session/$sessionId/appium/device/is_keyboard_shown') - 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') + 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 index 846fae2c..3141050a 100644 --- a/appium/webdriver/extensions/location.py +++ b/appium/webdriver/extensions/location.py @@ -12,58 +12,71 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Dict, TypeVar, Union +from typing import Dict, Union -from selenium import webdriver +from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self -from ..mobilecommand import MobileCommand as Command - -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts -T = TypeVar('T', bound=Union['WebDriver', 'Location']) +from ..mobilecommand import MobileCommand as Command -class Location(webdriver.Remote): - def toggle_location_services(self: T) -> T: +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 """ - self.execute(Command.TOGGLE_LOCATION_SERVICES, {}) + try: + self.execute_script('mobile: toggleGps') + except UnknownMethodException: + # TODO: Remove the fallback + self.execute(Command.TOGGLE_LOCATION_SERVICES) return self - def set_location(self: T, - latitude: Union[float, str], - longitude: Union[float, str], - altitude: Union[float, str] = None) -> T: + 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, + '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: T) -> Dict[str, float]: + def location(self) -> Dict[str, float]: """Retrieves the current location Returns: @@ -74,12 +87,12 @@ def location(self: T) -> Dict[str, float]: """ return self.execute(Command.GET_LOCATION)['value'] - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.TOGGLE_LOCATION_SERVICES] = \ - ('POST', '/session/$sessionId/appium/device/toggle_location_services') - self.command_executor._commands[Command.GET_LOCATION] = \ - ('GET', '/session/$sessionId/location') - self.command_executor._commands[Command.SET_LOCATION] = \ - ('POST', '/session/$sessionId/location') + 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 index 585ae3e6..4ca53b3f 100644 --- a/appium/webdriver/extensions/log_event.py +++ b/appium/webdriver/extensions/log_event.py @@ -12,23 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Dict, List, TypeVar, Union +from typing import Dict, List, Union -from selenium import webdriver +from typing_extensions import Self -from ..mobilecommand import MobileCommand as Command - -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'LogEvent']) +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands +from ..mobilecommand import MobileCommand as Command -class LogEvent(webdriver.Remote): - def get_events(self: T, type: List[str] = None) -> Dict[str, Union[str, int]]: - """ Retrieves events information from the current session +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: @@ -50,7 +45,7 @@ def get_events(self: T, type: List[str] = None) -> Dict[str, Union[str, int]]: data['type'] = type return self.execute(Command.GET_EVENTS, data)['value'] - def log_event(self: T, vendor: str, event: str) -> T: + def log_event(self, vendor: str, event: str) -> Self: """Log a custom event on the Appium server. (Since Appium 1.16.0) @@ -64,17 +59,10 @@ def log_event(self: T, vendor: str, event: str) -> T: Returns: Union['WebDriver', 'LogEvent']: Self instance """ - data = { - 'vendor': vendor, - 'event': event - } + data = {'vendor': vendor, 'event': event} self.execute(Command.LOG_EVENT, data) return self - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.GET_EVENTS] = \ - ('POST', '/session/$sessionId/appium/events') - self.command_executor._commands[Command.LOG_EVENT] = \ - ('POST', '/session/$sessionId/appium/log_event') + 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 index 47069d9f..5ccebd37 100644 --- a/appium/webdriver/extensions/remote_fs.py +++ b/appium/webdriver/extensions/remote_fs.py @@ -13,22 +13,20 @@ # limitations under the License. import base64 -from typing import TYPE_CHECKING, Optional, TypeVar, Union +from typing import Optional -from selenium import webdriver -from selenium.common.exceptions import InvalidArgumentException +from selenium.common.exceptions import InvalidArgumentException, UnknownMethodException +from typing_extensions import Self -from ..mobilecommand import MobileCommand as Command - -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver +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 -T = TypeVar('T', bound=Union['WebDriver', 'RemoteFS']) +from ..mobilecommand import MobileCommand as Command -class RemoteFS(webdriver.Remote): - def pull_file(self: T, path: str) -> str: +class RemoteFS(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): + def pull_file(self, path: str) -> str: """Retrieves the file at `path`. Args: @@ -37,12 +35,14 @@ def pull_file(self: T, path: str) -> str: Returns: The file's contents encoded as Base64. """ - data = { - 'path': path, - } - return self.execute(Command.PULL_FILE, data)['value'] - - def pull_folder(self: T, path: str) -> str: + 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: @@ -51,13 +51,14 @@ def pull_folder(self: T, path: str) -> str: Returns: The folder's contents zipped and encoded as Base64. """ - data = { - 'path': path, - } - return self.execute(Command.PULL_FOLDER, data)['value'] - - def push_file(self: T, destination_path: str, - base64data: Optional[str] = None, source_path: Optional[str] = None) -> T: + 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` @@ -83,19 +84,27 @@ def push_file(self: T, destination_path: str, raise InvalidArgumentException(message) from e base64data = base64.b64encode(file_data).decode('utf-8') - data = { - 'path': destination_path, - 'data': base64data, - } - self.execute(Command.PUSH_FILE, data) + 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 - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - 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') + 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 index 56455330..07c9b2ba 100644 --- a/appium/webdriver/extensions/screen_record.py +++ b/appium/webdriver/extensions/screen_record.py @@ -12,24 +12,69 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, TypeVar, Union +from typing import Any, Union -from selenium import webdriver +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from ..mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver -T = TypeVar('T', bound=Union['WebDriver', 'ScreenRecord']) - - -class ScreenRecord(webdriver.Remote): - - def start_recording_screen(self: T, **options: Any) -> Union[bytes, str]: +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. @@ -48,42 +93,63 @@ def start_recording_screen(self: T, **options: Any) -> Union[bytes, str]: 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. - bugReport (str): 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). 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 only] The video encoding quality: 'low', 'medium', 'high', 'photo'. Defaults + videoQuality (str): [iOS] The video encoding quality: 'low', 'medium', 'high', 'photo'. Defaults to 'medium'. - videoType (str): [iOS only] The format of the screen capture to be recorded. + 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 only] The Frames Per Second rate of the recorded video. Change this value if the + 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 only] The FFMPEG video filters to apply. These filters allow to scale, + 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 only] The scaling value to apply. Read https://trac.ffmpeg.org/wiki/Scaling for + 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 only] Output pixel format. Run `ffmpeg -pix_fmts` to list possible values. + 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 only] The video size of the generated media file. The format is WIDTHxHEIGHT. + + 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 only] The video bit rate for the video, in megabits per second. - The default value is 4. You can increase the bit rate to improve video quality, + 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 @@ -95,7 +161,7 @@ def start_recording_screen(self: T, **options: Any) -> Union[bytes, str]: del options['password'] return self.execute(Command.START_RECORDING_SCREEN, {'options': options})['value'] - def stop_recording_screen(self: T, **options: Any) -> bytes: + def stop_recording_screen(self, **options: Any) -> bytes: """Gather the output from the previously started screen recording to a media file. Keyword Args: @@ -112,6 +178,11 @@ def stop_recording_screen(self: T, **options: Any) -> bytes: 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 @@ -123,10 +194,14 @@ def stop_recording_screen(self: T, **options: Any) -> bytes: del options['password'] return self.execute(Command.STOP_RECORDING_SCREEN, {'options': options})['value'] - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.START_RECORDING_SCREEN] = \ - ('POST', '/session/$sessionId/appium/start_recording_screen') - self.command_executor._commands[Command.STOP_RECORDING_SCREEN] = \ - ('POST', '/session/$sessionId/appium/stop_recording_screen') + 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/search_context/__init__.py b/appium/webdriver/extensions/search_context/__init__.py deleted file mode 100644 index 40f38aac..00000000 --- a/appium/webdriver/extensions/search_context/__init__.py +++ /dev/null @@ -1,43 +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. - -# pylint: disable=abstract-method - -from selenium import webdriver -from selenium.webdriver.remote.webelement import \ - WebElement as SeleniumWebElement - -from .android import AndroidSearchContext -from .custom import CustomSearchContext -from .ios import iOSSearchContext -from .mobile import MobileSearchContext -from .windows import WindowsSearchContext - - -class AppiumSearchContext(webdriver.Remote, - AndroidSearchContext, - CustomSearchContext, - iOSSearchContext, - MobileSearchContext, - WindowsSearchContext): - """Returns appium driver search conext""" - - -class AppiumWebElementSearchContext(SeleniumWebElement, - AndroidSearchContext, - CustomSearchContext, - iOSSearchContext, - MobileSearchContext, - WindowsSearchContext): - """Returns appium web element search context""" diff --git a/appium/webdriver/extensions/search_context/android.py b/appium/webdriver/extensions/search_context/android.py deleted file mode 100644 index 882716e3..00000000 --- a/appium/webdriver/extensions/search_context/android.py +++ /dev/null @@ -1,192 +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. - -# pylint: disable=abstract-method - -import json -from typing import TYPE_CHECKING, Any, List, Optional, TypeVar, Union - -from appium.webdriver.common.mobileby import MobileBy - -from .base_search_context import BaseSearchContext - -if TYPE_CHECKING: - from appium.webdriver.webelement import WebElement - -T = TypeVar('T', bound=Union[BaseSearchContext, 'AndroidSearchContext']) - - -class AndroidSearchContext(BaseSearchContext): - """Define search context for Android""" - - def find_element_by_android_view_matcher(self: T, name: Optional[str] = None, args: Optional[Any] = None, - className: Optional[str] = None) -> 'WebElement': - """Finds element by [onView](https://developer.android.com/training/testing/espresso/basics) in Android - - It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). - - Args: - name: The name of a method to invoke. - The method must return a Hamcrest - [Matcher](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html) - args: The args provided to the method - className: The class name that the method is part of (defaults to `org.hamcrest.Matchers`). - Can be fully qualified by having the androidx.test.espresso.matcher. prefix. - If the prefix is not provided then it is going to be added implicitly. - (e.g.: `class=CursorMatchers` fully qualified is `class=androidx.test.espresso.matcher.CursorMatchers` - - Returns: - `appium.webdriver.webelement.WebElement`: The found element - - Raises: - TypeError - Raises a TypeError if the arguments are not validated for JSON format - - Usage: - driver.find_element_by_android_view_matcher(name='withText', args=['Accessibility'], className='ViewMatchers') - """ - - return self.find_element( - by=MobileBy.ANDROID_VIEW_MATCHER, - value=self._build_data_matcher(name=name, args=args, className=className) - ) - - def find_element_by_android_data_matcher(self: T, name: Optional[str] = None, args: Optional[Any] = None, - className: Optional[str] = None) -> 'WebElement': - """Finds element by - [onData](https://medium.com/androiddevelopers/adapterviews-and-espresso-f4172aa853cf) in Android - - It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). - - Args: - name: The name of a method to invoke. - The method must return a Hamcrest - [Matcher](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html) - args: The args provided to the method - className: The class name that the method is part of - (defaults to `org.hamcrest.Matchers`). - Can be fully qualified, or simple, and simple defaults to `androidx.test.espresso.matcher` package - (e.g.: `class=CursorMatchers` fully qualified is `class=androidx.test.espresso.matcher.CursorMatchers` - - Returns: - `appium.webdriver.webelement.WebElement`: The found element - - Raises: - TypeError - Raises a TypeError if the arguments are not validated for JSON format - - Usage: - driver.find_element_by_android_data_matcher(name='hasEntry', args=['title', 'Animation']) - """ - - return self.find_element( - by=MobileBy.ANDROID_DATA_MATCHER, - value=self._build_data_matcher(name=name, args=args, className=className) - ) - - def find_elements_by_android_data_matcher(self: T, name: Optional[str] = None, args: Optional[Any] = None, - className: Optional[str] = None) -> List['WebElement']: - """Finds elements by - [onData](https://medium.com/androiddevelopers/adapterviews-and-espresso-f4172aa853cf) in Android - It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). - - Args: - name: The name of a method to invoke. - The method must return a Hamcrest - [Matcher](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html) - args: The args provided to the method - className: The class name that the method is part of - (defaults to `org.hamcrest.Matchers`). - Can be fully qualified, or simple, and simple defaults to `androidx.test.espresso.matcher` package - (e.g.: `class=CursorMatchers` fully qualified is `class=androidx.test.espresso.matcher.CursorMatchers` - - Returns: - `appium.webdriver.webelement.WebElement`: The found elements - - Usage: - driver.find_elements_by_android_data_matcher(name='hasEntry', args=['title', 'Animation']) - """ - - return self.find_elements( - by=MobileBy.ANDROID_DATA_MATCHER, - value=self._build_data_matcher(name=name, args=args, className=className) - ) - - def _build_data_matcher(self: T, name: Optional[str] = None, args: Optional[Any] = None, - className: Optional[str] = None) -> str: - result = {} - - for key, value in {'name': name, 'args': args, 'class': className}.items(): - if value is not None: - result[key] = value - - return json.dumps(result) - - def find_element_by_android_uiautomator(self: T, uia_string: str) -> 'WebElement': - """Finds element by uiautomator in Android. - - Args: - uia_string: The element name in the Android UIAutomator library - - Usage: - driver.find_element_by_android_uiautomator('.elements()[1].cells()[2]') - - Returns: - `appium.webdriver.webelement.WebElement`: The found element - """ - return self.find_element(by=MobileBy.ANDROID_UIAUTOMATOR, value=uia_string) - - def find_elements_by_android_uiautomator(self: T, uia_string: str) -> List['WebElement']: - """Finds elements by uiautomator in Android. - - Args: - uia_string: The element name in the Android UIAutomator library - - Usage: - driver.find_elements_by_android_uiautomator('.elements()[1].cells()[2]') - - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`: The found elements - """ - return self.find_elements(by=MobileBy.ANDROID_UIAUTOMATOR, value=uia_string) - - def find_element_by_android_viewtag(self: T, tag: str) -> 'WebElement': - """Finds element by [View#tags](https://developer.android.com/reference/android/view/View#tags) in Android. - - It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). - - Args: - tag: The tag name of the view to look for - - Usage: - driver.find_element_by_android_viewtag('a tag name') - - Returns: - `appium.webdriver.webelement.WebElement`: The found element - """ - return self.find_element(by=MobileBy.ANDROID_VIEWTAG, value=tag) - - def find_elements_by_android_viewtag(self: T, tag: str) -> List['WebElement']: - """Finds element by [View#tags](https://developer.android.com/reference/android/view/View#tags) in Android. - - It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver). - - Args: - tag: The tag name of the view to look for - - Usage: - driver.find_elements_by_android_viewtag('a tag name') - - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`: The found elements - """ - return self.find_elements(by=MobileBy.ANDROID_VIEWTAG, value=tag) diff --git a/appium/webdriver/extensions/search_context/custom.py b/appium/webdriver/extensions/search_context/custom.py deleted file mode 100644 index e202728e..00000000 --- a/appium/webdriver/extensions/search_context/custom.py +++ /dev/null @@ -1,65 +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. - -# pylint: disable=abstract-method - -from typing import TYPE_CHECKING, List, TypeVar, Union - -from appium.webdriver.common.mobileby import MobileBy - -from .base_search_context import BaseSearchContext - -if TYPE_CHECKING: - from appium.webdriver.webelement import WebElement - -T = TypeVar('T', bound=Union[BaseSearchContext, 'CustomSearchContext']) - - -class CustomSearchContext(BaseSearchContext): - """Define search context for custom plugin""" - - def find_element_by_custom(self: T, selector: str) -> 'WebElement': - """Finds an element in conjunction with a custom element finding plugin - - Args: - selector: a string of the form "module:selector", where "module" is - the shortcut name given in the customFindModules capability, and - "selector" is the string that will be passed to the custom element - finding plugin itself - - Usage: - driver.find_element_by_custom("foo:bar") - - Returns: - `appium.webdriver.webelement.WebElement`: The found element - - """ - return self.find_element(by=MobileBy.CUSTOM, value=selector) - - def find_elements_by_custom(self: T, selector: str) -> List['WebElement']: - """Finds elements in conjunction with a custom element finding plugin - - Args: - selector: a string of the form "module:selector", where "module" is - the shortcut name given in the customFindModules capability, and - "selector" is the string that will be passed to the custom element - finding plugin itself - - Usage: - driver.find_elements_by_custom("foo:bar") - - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`: The found elements - """ - return self.find_elements(by=MobileBy.CUSTOM, value=selector) diff --git a/appium/webdriver/extensions/search_context/ios.py b/appium/webdriver/extensions/search_context/ios.py deleted file mode 100644 index b97bfa9c..00000000 --- a/appium/webdriver/extensions/search_context/ios.py +++ /dev/null @@ -1,117 +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. - -# pylint: disable=abstract-method - -from typing import TYPE_CHECKING, List, TypeVar, Union - -from appium.webdriver.common.mobileby import MobileBy - -from .base_search_context import BaseSearchContext - -if TYPE_CHECKING: - from appium.webdriver.webelement import WebElement - -T = TypeVar('T', bound=Union[BaseSearchContext, 'iOSSearchContext']) - - -class iOSSearchContext(BaseSearchContext): - """Define search context for iOS""" - - def find_element_by_ios_uiautomation(self: T, uia_string: str) -> 'WebElement': - """Finds an element by uiautomation in iOS. - - Args: - uia_string: The element name in the iOS UIAutomation library - - Usage: - driver.find_element_by_ios_uiautomation('.elements()[1].cells()[2]') - - Returns: - `appium.webdriver.webelement.WebElement`: The found element - - """ - return self.find_element(by=MobileBy.IOS_UIAUTOMATION, value=uia_string) - - def find_elements_by_ios_uiautomation(self: T, uia_string: str) -> List['WebElement']: - """Finds elements by uiautomation in iOS. - - Args: - uia_string: The element name in the iOS UIAutomation library - - Usage: - driver.find_elements_by_ios_uiautomation('.elements()[1].cells()[2]') - - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`: The found elements - - """ - return self.find_elements(by=MobileBy.IOS_UIAUTOMATION, value=uia_string) - - def find_element_by_ios_predicate(self: T, predicate_string: str) -> 'WebElement': - """Find an element by ios predicate string. - - Args: - predicate_string: The predicate string - - Usage: - driver.find_element_by_ios_predicate('label == "myLabel"') - - Returns: - `appium.webdriver.webelement.WebElement`: The found element - - """ - return self.find_element(by=MobileBy.IOS_PREDICATE, value=predicate_string) - - def find_elements_by_ios_predicate(self: T, predicate_string: str) -> List['WebElement']: - """Finds elements by ios predicate string. - - Args: - predicate_string: The predicate string - - Usage: - driver.find_elements_by_ios_predicate('label == "myLabel"') - - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`: The found elements - """ - return self.find_elements(by=MobileBy.IOS_PREDICATE, value=predicate_string) - - def find_element_by_ios_class_chain(self: T, class_chain_string: str) -> 'WebElement': - """Find an element by ios class chain string. - - Args: - class_chain_string: The class chain string - - Usage: - driver.find_element_by_ios_class_chain('XCUIElementTypeWindow/XCUIElementTypeButton[3]') - - Returns: - `appium.webdriver.webelement.WebElement`: The found element - """ - return self.find_element(by=MobileBy.IOS_CLASS_CHAIN, value=class_chain_string) - - def find_elements_by_ios_class_chain(self: T, class_chain_string: str) -> List['WebElement']: - """Finds elements by ios class chain string. - - Args: - class_chain_string: The class chain string - - Usage: - driver.find_elements_by_ios_class_chain('XCUIElementTypeWindow[2]/XCUIElementTypeAny[-2]') - - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`: The found elements - """ - return self.find_elements(by=MobileBy.IOS_CLASS_CHAIN, value=class_chain_string) diff --git a/appium/webdriver/extensions/search_context/mobile.py b/appium/webdriver/extensions/search_context/mobile.py deleted file mode 100644 index 52b16d71..00000000 --- a/appium/webdriver/extensions/search_context/mobile.py +++ /dev/null @@ -1,96 +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. - -# pylint: disable=abstract-method - -import base64 -from typing import TYPE_CHECKING, List, TypeVar, Union - -from appium.webdriver.common.mobileby import MobileBy - -from .base_search_context import BaseSearchContext - -if TYPE_CHECKING: - from appium.webdriver.webelement import WebElement - -T = TypeVar('T', bound=Union[BaseSearchContext, 'MobileSearchContext']) - - -class MobileSearchContext(BaseSearchContext): - """Define search context for Mobile(Android, iOS)""" - - def find_element_by_accessibility_id(self: T, accessibility_id: str) -> 'WebElement': - """Finds an element by accessibility id. - - Args: - accessibility_id: A string corresponding to a recursive element search using the - Id/Name that the native Accessibility options utilize - - Usage: - driver.find_element_by_accessibility_id() - - Returns: - `appium.webdriver.webelement.WebElement`: The found element - - """ - return self.find_element(by=MobileBy.ACCESSIBILITY_ID, value=accessibility_id) - - def find_elements_by_accessibility_id(self: T, accessibility_id: str) -> List['WebElement']: - """Finds elements by accessibility id. - - Args: - accessibility_id: a string corresponding to a recursive element search using the - Id/Name that the native Accessibility options utilize - - Usage: - driver.find_elements_by_accessibility_id() - - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`: The found elements - - """ - return self.find_elements(by=MobileBy.ACCESSIBILITY_ID, value=accessibility_id) - - def find_element_by_image(self: T, img_path: str) -> 'WebElement': - """Finds a portion of a screenshot by an image. - - Uses driver.find_image_occurrence under the hood. - - Args: - img_path: a string corresponding to the path of a image - - Returns: - `appium.webdriver.webelement.WebElement`: The found element - """ - with open(img_path, 'rb') as i_file: - b64_data = base64.b64encode(i_file.read()).decode('UTF-8') - - return self.find_element(by=MobileBy.IMAGE, value=b64_data) - - def find_elements_by_image(self: T, img_path: str) -> List['WebElement']: - """Finds a portion of a screenshot by an image. - - Uses driver.find_image_occurrence under the hood. Note that this will - only ever return at most one element - - Args: - img_path: a string corresponding to the path of a image - - Return: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`: The found elements - """ - with open(img_path, 'rb') as i_file: - b64_data = base64.b64encode(i_file.read()).decode('UTF-8') - - return self.find_elements(by=MobileBy.IMAGE, value=b64_data) diff --git a/appium/webdriver/extensions/search_context/windows.py b/appium/webdriver/extensions/search_context/windows.py deleted file mode 100644 index b1d4f491..00000000 --- a/appium/webdriver/extensions/search_context/windows.py +++ /dev/null @@ -1,59 +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. - -# pylint: disable=abstract-method - -from typing import TYPE_CHECKING, List, TypeVar, Union - -from appium.webdriver.common.mobileby import MobileBy - -from .base_search_context import BaseSearchContext - -if TYPE_CHECKING: - from appium.webdriver.webelement import WebElement - -T = TypeVar('T', bound=Union[BaseSearchContext, 'WindowsSearchContext']) - - -class WindowsSearchContext(BaseSearchContext): - """Define search context for Windows""" - - def find_element_by_windows_uiautomation(self: T, win_uiautomation: str) -> 'WebElement': - """Finds an element by windows uiautomation - - Args: - win_uiautomation: The element name in the windows UIAutomation selector - - Usage: - driver.find_element_by_windows_uiautomation() - - Returns: - `appium.webdriver.webelement.WebElement`: The found element - - """ - return self.find_element(by=MobileBy.WINDOWS_UI_AUTOMATION, value=win_uiautomation) - - def find_elements_by_windows_uiautomation(self: T, win_uiautomation: str) -> List['WebElement']: - """Finds elements by windows uiautomation - - Args: - win_uiautomation: The element name in the windows UIAutomation selector - - Usage: - driver.find_elements_by_windows_uiautomation() - - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`: The found elements - """ - return self.find_elements(by=MobileBy.WINDOWS_UI_AUTOMATION, value=win_uiautomation) diff --git a/appium/webdriver/extensions/session.py b/appium/webdriver/extensions/session.py index 3fe2d31c..e0bcd8b6 100644 --- a/appium/webdriver/extensions/session.py +++ b/appium/webdriver/extensions/session.py @@ -12,49 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Dict, List, TypeVar, Union - -from selenium import webdriver +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 -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver - -T = TypeVar('T', bound=Union['WebDriver', 'Session']) - - -class Session(webdriver.Remote): - @property - def session(self: T) -> Dict[str, Any]: - """ Retrieves session information from the current session - - Usage: - session = driver.session - - Returns: - `dict`: containing information from the current session - """ - return self.execute(Command.GET_SESSION)['value'] - - @property - def all_sessions(self: T) -> List[Dict[str, Any]]: - """ Retrieves all sessions that are open - - Usage: - sessions = driver.all_sessions - - Returns: - :obj:`list` of :obj:`dict`: containing all open sessions - """ - return self.execute(Command.GET_ALL_SESSIONS)['value'] +class Session(CanExecuteCommands): @property - def events(self: T) -> Dict: - """ Retrieves events information from the current session + def events(self) -> Dict: + """Retrieves events information from the current session Usage: events = driver.events @@ -63,16 +32,10 @@ def events(self: T) -> Dict: `dict`: containing events timing information from the current session """ try: - session = self.session - return session['events'] + 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 {} - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.GET_SESSION] = \ - ('GET', '/session/$sessionId') - self.command_executor._commands[Command.GET_ALL_SESSIONS] = \ - ('GET', '/sessions') + 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 index 7a71622b..d9a55111 100644 --- a/appium/webdriver/extensions/settings.py +++ b/appium/webdriver/extensions/settings.py @@ -12,21 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Dict, TypeVar, Union +from typing import Any, Dict -from selenium import webdriver +from typing_extensions import Self -from ..mobilecommand import MobileCommand as Command - -if TYPE_CHECKING: - # noinspection PyUnresolvedReferences - from appium.webdriver.webdriver import WebDriver +from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands -T = TypeVar('T', bound=Union['WebDriver', 'Settings']) +from ..mobilecommand import MobileCommand as Command -class Settings(webdriver.Remote): - def get_settings(self: T) -> Dict[str, Any]: +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 @@ -37,7 +33,7 @@ def get_settings(self: T) -> Dict[str, Any]: """ return self.execute(Command.GET_SETTINGS, {})['value'] - def update_settings(self: T, settings: Dict[str, Any]) -> T: + 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 @@ -45,15 +41,9 @@ def update_settings(self: T, settings: Dict[str, Any]) -> T: Args: settings: dictionary of settings to apply to the current test session """ - data = {"settings": settings} - - self.execute(Command.UPDATE_SETTINGS, data) + self.execute(Command.UPDATE_SETTINGS, {'settings': settings}) return self - # pylint: disable=protected-access - # noinspection PyProtectedMember - def _addCommands(self) -> None: - self.command_executor._commands[Command.GET_SETTINGS] = \ - ('GET', '/session/$sessionId/appium/settings') - self.command_executor._commands[Command.UPDATE_SETTINGS] = \ - ('POST', '/session/$sessionId/appium/settings') + 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/test/functional/android/keyboard_tests.py b/appium/webdriver/locator_converter.py similarity index 55% rename from test/functional/android/keyboard_tests.py rename to appium/webdriver/locator_converter.py index 5e1ebe6a..4c6416c6 100644 --- a/test/functional/android/keyboard_tests.py +++ b/appium/webdriver/locator_converter.py @@ -1,5 +1,4 @@ #!/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. @@ -13,14 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .helper.test_helper import BaseTestCase +from typing import Tuple +from selenium.webdriver.remote.locator_converter import LocatorConverter -class TestKeyboard(BaseTestCase): - def test_press_keycode(self) -> None: - # TODO not sure how to test this. - self.driver.press_keycode(176) - def test_long_press_keycode(self) -> None: - # TODO not sure how to test this. - self.driver.long_press_keycode(176) +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 b0bd8c4d..79ac25e1 100644 --- a/appium/webdriver/mobilecommand.py +++ b/appium/webdriver/mobilecommand.py @@ -16,17 +16,13 @@ class MobileCommand: # Common GET_SESSION = 'getSession' - GET_ALL_SESSIONS = 'getAllSessions' + GET_STATUS = 'getStatus' + + ## MJSONWP for Selenium v4 GET_LOCATION = 'getLocation' SET_LOCATION = 'setLocation' - GET_AVAILABLE_IME_ENGINES = 'getAvailableIMEEngines' - IS_IME_ACTIVE = 'isIMEActive' - ACTIVATE_IME_ENGINE = 'activateIMEEngine' - DEACTIVATE_IME_ENGINE = 'deactivateIMEEngine' - GET_ACTIVE_IME_ENGINE = 'getActiveEngine' - CLEAR = 'clear' LOCATION_IN_VIEW = 'locationInView' @@ -34,15 +30,6 @@ class MobileCommand: GET_CURRENT_CONTEXT = 'getCurrentContext' SWITCH_TO_CONTEXT = 'switchToContext' - TOUCH_ACTION = 'touchAction' - MULTI_ACTION = 'multiAction' - - SET_IMMEDIATE_VALUE = 'setImmediateValue' - REPLACE_KEYS = 'replaceKeys' - - LAUNCH_APP = 'launchApp' - CLOSE_APP = 'closeApp' - RESET = 'reset' BACKGROUND = 'background' GET_APP_STRINGS = 'getAppStrings' @@ -80,16 +67,24 @@ class MobileCommand: 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' - START_ACTIVITY = 'startActivity' GET_CURRENT_ACTIVITY = 'getCurrentActivity' GET_CURRENT_PACKAGE = 'getCurrentPackage' GET_SYSTEM_BARS = 'getSystemBars' GET_DISPLAY_DENSITY = 'getDisplayDensity' TOGGLE_WIFI = 'toggleWiFi' TOGGLE_LOCATION_SERVICES = 'toggleLocationServices' - END_TEST_COVERAGE = 'endTestCoverage' GET_PERFORMANCE_DATA_TYPES = 'getPerformanceDataTypes' GET_PERFORMANCE_DATA = 'getPerformanceData' GET_NETWORK_CONNECTION = 'getNetworkConnection' diff --git a/appium/webdriver/switch_to.py b/appium/webdriver/switch_to.py index eb753d47..ed9db7c7 100644 --- a/appium/webdriver/switch_to.py +++ b/appium/webdriver/switch_to.py @@ -12,18 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypeVar +from typing import Optional from selenium.webdriver.remote.switch_to import SwitchTo +from typing_extensions import Self from .mobilecommand import MobileCommand -T = TypeVar('T', bound='MobileSwitchTo') - class MobileSwitchTo(SwitchTo): - def context(self, context_name: str) -> T: + 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. diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 3b8e08b1..5cdd2321 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -12,20 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=too-many-lines,too-many-public-methods,too-many-statements,no-self-use - -import copy -from typing import Any, Dict, List, Optional, TypeVar, Union - -from selenium.common.exceptions import InvalidArgumentException -from selenium.webdriver.common.by import By +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union + +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.webdriver.common.mobileby import MobileBy +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 @@ -45,78 +49,166 @@ from .extensions.execute_mobile_command import ExecuteMobileCommand from .extensions.hw_actions import HardwareActions from .extensions.images_comparison import ImagesComparison -from .extensions.ime import IME 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.search_context import AppiumSearchContext 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 remote/webdriver.py -_W3C_CAPABILITY_NAMES = frozenset([ - 'acceptInsecureCerts', - 'browserName', - 'browserVersion', - 'platformName', - 'pageLoadStrategy', - 'proxy', - 'setWindowRect', - 'timeouts', - 'unhandledPromptBehavior', -]) - -# From remote/webdriver.py -_OSS_W3C_CONVERSION = { - 'acceptSslCerts': 'acceptInsecureCerts', - 'version': 'browserVersion', - 'platform': 'platformName' -} - -_EXTENSION_CAPABILITY = ':' -_FORCE_MJSONWP = 'forceMjsonwp' - -# override -# Add appium prefix for the non-W3C capabilities - - -def _make_w3c_caps(caps: Dict) -> Dict[str, List[Dict[str, Any]]]: - appium_prefix = 'appium:' - - caps = copy.deepcopy(caps) - profile = caps.get('firefox_profile') - first_match = {} - if caps.get('proxy') and caps['proxy'].get('proxyType'): - caps['proxy']['proxyType'] = caps['proxy']['proxyType'].lower() - for k, v in caps.items(): - if v and k in _OSS_W3C_CONVERSION: - first_match[_OSS_W3C_CONVERSION[k]] = v.lower() if k == 'platform' else v - if k in _W3C_CAPABILITY_NAMES or _EXTENSION_CAPABILITY in k: - first_match[k] = v - else: - if not k.startswith(appium_prefix): - first_match[appium_prefix + k] = v - if profile: - moz_opts = first_match.get('moz:firefoxOptions', {}) - # If it's already present, assume the caller did that intentionally. - if 'profile' not in moz_opts: - # Don't mutate the original capabilities. - new_opts = copy.deepcopy(moz_opts) - new_opts['profile'] = profile - first_match['moz:firefoxOptions'] = new_opts - return {'firstMatch': [first_match]} +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`. + + #. Defines an extension as a subclass of `ExtensionBase` + .. code-block:: python + + class YourCustomCommand(ExtensionBase): + def method_name(self): + return 'custom_method_name' + + # 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'] + + # 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') + + #. Creates a session with the extension. + .. code-block:: python + + # Appium capabilities + options = AppiumOptions() + driver = webdriver.Remote('http://localhost:4723/wd/hub', options=options, + extensions=[YourCustomCommand]) + + #. Calls the custom command + .. code-block:: python + + # 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() + + #. Remove added commands (if needed) + .. code-block:: python + + # 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() + + + You can give arbitrary arguments for the command like the below. + + .. code-block:: python + + class YourCustomCommand(ExtensionBase): + def method_name(self): + return 'custom_method_name' + + def test_command(self, argument): + return self.execute(argument)['value'] + + def add_command(self): + return ('post', 'session/$sessionId/path/to/your/custom/url') + + driver = webdriver.Remote('http://localhost:4723/wd/hub', options=options, + extensions=[YourCustomCommand]) + + # 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'}) + + + When you customize the URL dynamically with element id. + + .. code-block:: python + + class CustomURLCommand(ExtensionBase): + def method_name(self): + return 'custom_method_name' -T = TypeVar('T', bound='WebDriver') + def custom_method_name(self, element_id): + return self.execute({'id': element_id})['value'] + + def add_command(self): + return ('GET', 'session/$sessionId/path/to/your/custom/$id/url') + + driver = webdriver.Remote('http://localhost:4723/wd/hub', options=options, + extensions=[YourCustomCommand]) + element = driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='id') + + # 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) + + """ + + def __init__(self, execute: Callable[[str, Dict], Dict[str, Any]]): + self._execute = execute + + def execute(self, parameters: Union[Dict[str, Any], None] = None) -> Any: + param = {} + if parameters: + param = parameters + return self._execute(self.method_name(), param) + + def method_name(self) -> str: + """ + Expected to return a method name. + This name will be available as a driver method. + + Returns: + 'str' The method name. + """ + raise NotImplementedError() + + def add_command(self) -> Tuple[str, str]: + """ + Expected to define the pair of HTTP method and its URL. + """ + raise NotImplementedError() + + +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( - AppiumSearchContext, + webdriver.Remote, ActionHelpers, Activities, Applications, @@ -130,10 +222,10 @@ class WebDriver( Gsm, HardwareActions, ImagesComparison, - IME, Keyboard, Location, LogEvent, + Logs, Network, Performance, Power, @@ -142,38 +234,65 @@ class WebDriver( Session, Settings, Sms, - SystemBars + SystemBars, ): - - def __init__(self, command_executor: str = 'http://127.0.0.1:4444/wd/hub', - desired_capabilities: Optional[Dict] = None, browser_profile: str = None, proxy: str = None, keep_alive: bool = True, direct_connection: bool = False): - + 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__( - AppiumConnection(command_executor, keep_alive=keep_alive), - desired_capabilities, - browser_profile, - proxy + command_executor=command_executor, + options=options, # type: ignore[arg-type] + locator_converter=AppiumLocatorConverter(), + web_element_cls=MobileWebElement, + client_config=client_config, ) - if hasattr(self, 'command_executor'): - self._addCommands() + # to explicitly set type after the initialization + self.command_executor: RemoteConnection + + self._add_commands() self.error_handler = MobileErrorHandler() - self._switch_to = MobileSwitchTo(self) - - if direct_connection: - self._update_command_executor(keep_alive=keep_alive) - - # add new method to the `find_by_*` pantheon - By.IOS_UIAUTOMATION = MobileBy.IOS_UIAUTOMATION - By.IOS_PREDICATE = MobileBy.IOS_PREDICATE - By.IOS_CLASS_CHAIN = MobileBy.IOS_CLASS_CHAIN - By.ANDROID_UIAUTOMATOR = MobileBy.ANDROID_UIAUTOMATOR - By.ANDROID_VIEWTAG = MobileBy.ANDROID_VIEWTAG - By.WINDOWS_UI_AUTOMATION = MobileBy.WINDOWS_UI_AUTOMATION - By.ACCESSIBILITY_ID = MobileBy.ACCESSIBILITY_ID - By.IMAGE = MobileBy.IMAGE - By.CUSTOM = MobileBy.CUSTOM + + 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""" @@ -182,140 +301,78 @@ def _update_command_executor(self, keep_alive: bool) -> None: direct_port = 'directConnectPort' direct_path = 'directConnectPath' - if (not {direct_protocol, direct_host, direct_port, direct_path}.issubset(set(self.capabilities))): + 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 += '{}: \'{}\'\n'.format(key, self.capabilities.get(key, '')) - logger.warning(message) + message += f"{key}: '{self.caps.get(key, '')}' " + logger.debug(message) return - protocol = self.capabilities[direct_protocol] - hostname = self.capabilities[direct_host] - port = self.capabilities[direct_port] - path = self.capabilities[direct_path] + 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.info('Updated request endpoint to %s', executor) - # Override command executor - self.command_executor = RemoteConnection(executor, keep_alive=keep_alive) - self._addCommands() + 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() - def start_session(self, capabilities: Dict, browser_profile: Optional[str] = None) -> None: + # 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. Override for Appium Args: - capabilities: Capabilities which have following keys like 'automation_name', 'platform_name', 'platform_version', 'app'. - Read https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md for more details. + capabilities: Read https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md + for more details. browser_profile: Browser profile """ - if not isinstance(capabilities, dict): - raise InvalidArgumentException('Capabilities must be a dictionary') - if browser_profile: - if 'moz:firefoxOptions' in capabilities: - # encoded is defined in selenium's original codes - capabilities['moz:firefoxOptions']['profile'] = browser_profile.encoded # type: ignore - else: - # encoded is defined in selenium's original codes - capabilities.update({'firefox_profile': browser_profile.encoded}) # type: ignore - - parameters = self._merge_capabilities(capabilities) - - response = self.execute(RemoteCommand.NEW_SESSION, parameters) - if 'sessionId' not in response: - response = response['value'] - self.session_id = response['sessionId'] - self.capabilities = response.get('value') - - # if capabilities is none we are probably speaking to - # a W3C endpoint - if self.capabilities is None: - self.capabilities = response.get('capabilities') - - # Double check to see if we have a W3C Compliant browser - self.w3c = response.get('status') is None - self.command_executor.w3c = self.w3c - - def _merge_capabilities(self, capabilities: Dict) -> Dict[str, Any]: - """Manage capabilities whether W3C format or MJSONWP format + if not isinstance(capabilities, (dict, AppiumOptions)): + raise InvalidArgumentException('Capabilities must be a dictionary or AppiumOptions instance') + + 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 {} + + def get_status(self) -> Dict: """ - if _FORCE_MJSONWP in capabilities: - logger.warning("[Deprecated] 'forceMjsonwp' capability will be dropped after switching base selenium client from v3 to v4 " - "to follow W3C spec capabilities. Appium 2.0 will also support only W3C session creation capabilities.") - force_mjsonwp = capabilities[_FORCE_MJSONWP] - del capabilities[_FORCE_MJSONWP] - - if force_mjsonwp != False: - return {'desiredCapabilities': capabilities} - - w3c_caps = _make_w3c_caps(capabilities) - return {'capabilities': w3c_caps, 'desiredCapabilities': capabilities} - - def find_element(self, by: str = By.ID, value: Union[str, Dict] = None) -> MobileWebElement: - """'Private' method used by the find_element_by_* methods. - - Override for Appium + Get the Appium server status Usage: - Use the corresponding find_element_by_* instead of this. + driver.get_status() Returns: - `appium.webdriver.webelement.WebElement`: The found element + dict: The status information """ - # TODO: If we need, we should enable below converter for Web context - # if self.w3c: - # if by == By.ID: - # by = By.CSS_SELECTOR - # value = '[id="%s"]' % value - # elif by == By.TAG_NAME: - # by = By.CSS_SELECTOR - # elif by == By.CLASS_NAME: - # by = By.CSS_SELECTOR - # value = ".%s" % value - # elif by == By.NAME: - # by = By.CSS_SELECTOR - # value = '[name="%s"]' % value - - return self.execute(RemoteCommand.FIND_ELEMENT, { - 'using': by, - 'value': value})['value'] - - def find_elements(self, by: str = By.ID, value: Union[str, Dict] - = None) -> Union[List[MobileWebElement], List]: - """'Private' method used by the find_elements_by_* methods. - - Override for Appium - - Usage: - Use the corresponding find_elements_by_* instead of this. + return self.execute(Command.GET_STATUS)['value'] - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`: The found elements - """ - # TODO: If we need, we should enable below converter for Web context - # if self.w3c: - # if by == By.ID: - # by = By.CSS_SELECTOR - # value = '[id="%s"]' % value - # elif by == By.TAG_NAME: - # by = By.CSS_SELECTOR - # elif by == By.CLASS_NAME: - # by = By.CSS_SELECTOR - # value = ".%s" % value - # elif by == By.NAME: - # by = By.CSS_SELECTOR - # value = '[name="%s"]' % value - - # Return empty list if driver returns null - # See https://github.com/SeleniumHQ/selenium/issues/4555 - - return self.execute(RemoteCommand.FIND_ELEMENTS, { - 'using': by, - 'value': value})['value'] or [] - - def create_web_element(self, element_id: Union[int, str], w3c: bool = False) -> MobileWebElement: + def create_web_element(self, element_id: Union[int, str]) -> MobileWebElement: """Creates a web element with the specified element_id. Overrides method in Selenium WebDriver in order to always give them @@ -323,51 +380,116 @@ def create_web_element(self, element_id: Union[int, str], w3c: bool = False) -> Args: element_id: The element id to create a web element - w3c: Whether the element is W3C or MJSONWP Returns: `MobileWebElement` """ - return MobileWebElement(self, element_id, w3c) + return MobileWebElement(self, element_id) + + @property + def switch_to(self) -> MobileSwitchTo: + """Returns an object containing all options to switch focus into + + Override for appium - def set_value(self, element: MobileWebElement, value: str) -> T: - """Set the value on an element in the application. + Returns: + `appium.webdriver.switch_to.MobileSwitchTo` + + """ + + return MobileSwitchTo(self) + + # MJSONWP for Selenium v4 + @property # type: ignore[override] + def orientation(self) -> str: + """ + Gets the current orientation of the device + + Example: + + .. code-block:: python + + orientation = driver.orientation + """ + return self.execute(Command.GET_SCREEN_ORIENTATION)['value'] + + # MJSONWP for Selenium v4 + @orientation.setter + def orientation(self, value: str) -> None: + """ + Sets the current orientation of the device Args: - element: the element whose value will be set - value: the value to set on the element + - value: orientation to set it to. + + Example: + .. code-block:: python + + driver.orientation = 'landscape' + """ + 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 assert_extension_exists(self, ext_name: str) -> Self: + """ + 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: + ext_name: extension name Returns: - `appium.webdriver.webdriver.WebDriver`: Self instance + self instance for chaining + + Raises: + UnknownMethodException: If the extension has been marked as absent once """ - data = { - 'id': element.id, - 'value': [value], - } - self.execute(Command.SET_IMMEDIATE_VALUE, data) + if ext_name in self._absent_extensions: + raise UnknownMethodException() return self - # pylint: disable=protected-access + def mark_extension_absence(self, ext_name: str) -> Self: + """ + Marks the given extension as absent for the given driver instance. + This API is designed for private usage. + + Args: + ext_name: extension name + + Returns: + self instance for chaining + """ + logger.debug(f'Marking driver extension "{ext_name}" as absent for the current instance') + self._absent_extensions.add(ext_name) + return self - def _addCommands(self) -> None: + 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._addCommands.__name__): - getattr(mixin_class, self._addCommands.__name__, None)(self) + 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._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.SET_IMMEDIATE_VALUE] = \ - ('POST', '/session/$sessionId/appium/element/$id/value') + self.command_executor.add_command(Command.GET_STATUS, 'GET', '/status') # TODO Move commands for element to webelement - self.command_executor._commands[Command.REPLACE_KEYS] = \ - ('POST', '/session/$sessionId/appium/element/$id/replace_value') - self.command_executor._commands[Command.CLEAR] = \ - ('POST', '/session/$sessionId/element/$id/clear') - self.command_executor._commands[Command.LOCATION_IN_VIEW] = \ - ('GET', '/session/$sessionId/element/$id/location_in_view') + 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 dec57cdf..61076ca3 100644 --- a/appium/webdriver/webelement.py +++ b/appium/webdriver/webelement.py @@ -12,20 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, List, Optional, TypeVar, Union +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 .extensions.search_context import AppiumWebElementSearchContext from .mobilecommand import MobileCommand as Command -T = TypeVar('T', bound='WebElement') +class WebElement(SeleniumWebElement): + _execute: Callable + _id: str -class WebElement(AppiumWebElementSearchContext): - def get_attribute(self, name: str) -> Optional[Union[str, Dict]]: + if TYPE_CHECKING: + + def find_element(self, by: str, value: Union[str, Dict, None] = None) -> Self: # type: ignore[override] + ... + + def find_elements(self, by: str, value: Union[str, Dict, None] = None) -> List[Self]: # type: ignore[override] + ... + + def get_attribute(self, name: str) -> Optional[Union[str, Dict]]: # type: ignore[override] """Gets the given attribute or property of the element. Override for Appium @@ -53,99 +62,31 @@ def get_attribute(self, name: str) -> Optional[Union[str, Dict]]: """ resp = self._execute(RemoteCommand.GET_ELEMENT_ATTRIBUTE, {'name': name}) - attributeValue = resp.get('value') + attribute_value = resp.get('value') - if attributeValue is None: + if attribute_value is None: return None - if isinstance(attributeValue, dict): - return attributeValue + if isinstance(attribute_value, dict): + return attribute_value # Convert to str along to the spec - if not isinstance(attributeValue, str): - attributeValue = str(attributeValue) + if not isinstance(attribute_value, str): + attribute_value = str(attribute_value) - if name != 'value' and attributeValue.lower() in ('true', 'false'): - return attributeValue.lower() + if name != 'value' and attribute_value.lower() in ('true', 'false'): + return attribute_value.lower() - return attributeValue + return attribute_value def is_displayed(self) -> bool: """Whether the element is visible to a user. Override for Appium """ - return self._execute(RemoteCommand.IS_ELEMENT_DISPLAYED)['value'] - - def find_element(self, by: str = By.ID, value: Union[str, Dict] = None) -> T: - """Find an element given a By strategy and locator - - Override for Appium - - Prefer the find_element_by_* methods when possible. - - Args: - by: The strategy - value: The locator - - Usage: - element = element.find_element(By.ID, 'foo') - - Returns: - `appium.webdriver.webelement.WebElement` - """ - # TODO: If we need, we should enable below converter for Web context - # if self._w3c: - # if by == By.ID: - # by = By.CSS_SELECTOR - # value = '[id="%s"]' % value - # elif by == By.TAG_NAME: - # by = By.CSS_SELECTOR - # elif by == By.CLASS_NAME: - # by = By.CSS_SELECTOR - # value = ".%s" % value - # elif by == By.NAME: - # by = By.CSS_SELECTOR - # value = '[name="%s"]' % value - - return self._execute(RemoteCommand.FIND_CHILD_ELEMENT, - {"using": by, "value": value})['value'] - - def find_elements(self, by: str = By.ID, value: Union[str, Dict] = None) -> List[T]: - """Find elements given a By strategy and locator - - Override for Appium - - Prefer the find_elements_by_* methods when possible. - - Args: - by: The strategy - value: The locator - - Usage: - element = element.find_elements(By.CLASS_NAME, 'foo') + return self._execute(Command.IS_ELEMENT_DISPLAYED)['value'] - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement` - """ - # TODO: If we need, we should enable below converter for Web context - # if self._w3c: - # if by == By.ID: - # by = By.CSS_SELECTOR - # value = '[id="%s"]' % value - # elif by == By.TAG_NAME: - # by = By.CSS_SELECTOR - # elif by == By.CLASS_NAME: - # by = By.CSS_SELECTOR - # value = ".%s" % value - # elif by == By.NAME: - # by = By.CSS_SELECTOR - # value = '[name="%s"]' % value - - return self._execute(RemoteCommand.FIND_CHILD_ELEMENTS, - {"using": by, "value": value})['value'] - - def clear(self) -> T: + def clear(self) -> Self: # type: ignore[override] """Clears text. Override for Appium @@ -153,32 +94,13 @@ def clear(self) -> T: Returns: `appium.webdriver.webelement.WebElement` """ + + # 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 - def set_text(self, keys: str = '') -> T: - """Sends text to the element. - - Previous text is removed. - Android only. - - Args: - keys: the text to be sent to the element. - - Usage: - element.set_text('some text') - - Returns: - `appium.webdriver.webelement.WebElement` - """ - data = { - 'id': self._id, - 'value': [keys] - } - self._execute(Command.REPLACE_KEYS, data) - return self - @property def location_in_view(self) -> Dict[str, int]: """Gets the location of an element relative to the view. @@ -193,24 +115,8 @@ def location_in_view(self) -> Dict[str, int]: """ return self._execute(Command.LOCATION_IN_VIEW)['value'] - def set_value(self, value: str) -> T: - """Set the value on this element in the application - - Args: - value: The value to be set - - Returns: - `appium.webdriver.webelement.WebElement` - """ - data = { - 'id': self.id, - 'value': [value], - } - self._execute(Command.SET_IMMEDIATE_VALUE, data) - return self - # Override - def send_keys(self, *value: str) -> T: + def send_keys(self, *value: str) -> Self: # type: ignore[override] """Simulates typing into the element. Args: @@ -220,6 +126,5 @@ def send_keys(self, *value: str) -> T: `appium.webdriver.webelement.WebElement` """ keys = keys_to_typing(value) - self._execute(RemoteCommand.SEND_KEYS_TO_ELEMENT, - {'text': ''.join(keys), 'value': keys}) + self._execute(RemoteCommand.SEND_KEYS_TO_ELEMENT, {'text': ''.join(keys), 'value': keys}) return self diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 4eb2fbbb..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,15 +0,0 @@ -# Xcode -# Build, test, and archive an Xcode workspace on macOS. -# Add steps that install certificates, test, sign, and distribute an app, save build artifacts, and more: -# https://docs.microsoft.com/azure/devops/pipelines/languages/xcode -jobs: - - template: ./ci-jobs/functional_test.yml - -# Runs tests nightly to make sure they works against appium@beta -schedules: - - cron: "0 0 * * *" - displayName: Daily Nightly build (UTC) - branches: - include: - - master - always: true diff --git a/ci-jobs/functional/ios_setup.yml b/ci-jobs/functional/ios_setup.yml deleted file mode 100644 index b482b152..00000000 --- a/ci-jobs/functional/ios_setup.yml +++ /dev/null @@ -1,5 +0,0 @@ -steps: -- script: sudo xcode-select -s /Applications/Xcode_${{ parameters.xcodeVersion }}.app/Contents/Developer - displayName: Xcode Select ${{ parameters.xcodeVersion }} -- script: xcrun simctl list - displayName: List Installed Simulators diff --git a/ci-jobs/functional/publish_test_result.yml b/ci-jobs/functional/publish_test_result.yml deleted file mode 100644 index a488e640..00000000 --- a/ci-jobs/functional/publish_test_result.yml +++ /dev/null @@ -1,11 +0,0 @@ -steps: -- task: PublishTestResults@2 - condition: succeededOrFailed() - inputs: - testResultsFiles: '**/test-*.xml' - testRunTitle: ${{ parameters.title }} -- task: PublishCodeCoverageResults@1 - inputs: - codeCoverageTool: Cobertura - summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' - reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' diff --git a/ci-jobs/functional/run_android_test.yml b/ci-jobs/functional/run_android_test.yml deleted file mode 100644 index 4c7df366..00000000 --- a/ci-jobs/functional/run_android_test.yml +++ /dev/null @@ -1,22 +0,0 @@ -jobs: - - job: ${{ parameters.name }} - pool: - vmImage: ${{ parameters.vmImage }} - variables: - ANDROID_SDK_VERSION: ${{ parameters.sdkVer }} - CI: ${{ parameters.ci }} - steps: - - template: ./run_appium.yml - parameters: - OPENCV: ${{ parameters.opencv }} - DONT_RUN_APPIUM: ${{ parameters.dont_run_appium }} - - script: bash ci-jobs/functional/start-emulator.sh - displayName: Create and run Emulator - - script: | - cd test/functional/android - python -m pytest ${{ parameters.testFiles}} ${{ parameters.pytestOpt }} - displayName: Run Android functional tests - - template: ./publish_test_result.yml - - template: ./save_appium_log.yml - parameters: - name: ${{ parameters.name }} diff --git a/ci-jobs/functional/run_appium.yml b/ci-jobs/functional/run_appium.yml deleted file mode 100644 index c9b21b0c..00000000 --- a/ci-jobs/functional/run_appium.yml +++ /dev/null @@ -1,32 +0,0 @@ -steps: -- task: NodeTool@0 - inputs: - versionSpec: '11.x' - displayName: Install Node 11.x -- script: npm install -g appium@beta --chromedriver_version='2.44' - displayName: Install appium -- script: npm install -g opencv4nodejs - condition: eq('${{ parameters.opencv }}', true) - displayName: Install opencv4nodejs -- task: UsePythonVersion@0 - inputs: - versionSpec: '3.x' -- script: brew install ffmpeg - displayName: Resolve dependencies (Appium server) -- script: python setup.py install - displayName: Install python language bindings for Appium -- script: | - pip install pipenv - pipenv lock --clear - pipenv install --system - displayName: Resolve dependencies (Python) -- script: | - git --no-pager log -n1 - python --version - ffmpeg -version - appium --version - node --version - displayName: Check versions -- script: nohup appium --relaxed-security > appium_log.txt & - condition: ne('${{ parameters.dont_run_appium }}', true) - displayName: Run Appium in background diff --git a/ci-jobs/functional/run_ios_test.yml b/ci-jobs/functional/run_ios_test.yml deleted file mode 100644 index 5fa792d0..00000000 --- a/ci-jobs/functional/run_ios_test.yml +++ /dev/null @@ -1,19 +0,0 @@ -jobs: - - job: ${{ parameters.name }} - pool: - vmImage: ${{ parameters.vmImage }} - variables: - CI: ${{ parameters.ci }} - steps: - - template: ./run_appium.yml - - template: ./ios_setup.yml - parameters: - xcodeVersion: ${{ parameters.xcodeForIOS }} - - script: | - cd test/functional/ios - python -m pytest ${{ parameters.testFiles}} ${{ parameters.pytestOpt }} - displayName: Run iOS functional tests - - template: ./publish_test_result.yml - - template: ./save_appium_log.yml - parameters: - name: ${{ parameters.name }} diff --git a/ci-jobs/functional/save_appium_log.yml b/ci-jobs/functional/save_appium_log.yml deleted file mode 100644 index 81abba17..00000000 --- a/ci-jobs/functional/save_appium_log.yml +++ /dev/null @@ -1,18 +0,0 @@ -steps: -- task: CopyFiles@2 - condition: succeededOrFailed() - inputs: - contents: - '**/appium_log.txt' - targetFolder: $(Build.ArtifactStagingDirectory) -- task: CopyFiles@2 - condition: succeededOrFailed() - inputs: - contents: - '**/test_*.mp4' - targetFolder: $(Build.ArtifactStagingDirectory) -- task: PublishBuildArtifacts@1 - condition: succeededOrFailed() - inputs: - pathToPublish: $(Build.ArtifactStagingDirectory) - artifactName: ${{ parameters.name }} diff --git a/ci-jobs/functional/start-emulator.sh b/ci-jobs/functional/start-emulator.sh deleted file mode 100644 index 6228a988..00000000 --- a/ci-jobs/functional/start-emulator.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash - -# This file comes from https://github.com/appium/ruby_lib_core - -# This script was copy-pasted from https://docs.microsoft.com/en-us/azure/devops/pipelines/languages/android?view=azure-devops#test-on-the-android-emulator -# with some changes - -# Install AVD files -declare -r emulator="system-images;android-${ANDROID_SDK_VERSION};google_apis;x86" -echo "y" | ${ANDROID_HOME}/tools/bin/sdkmanager --install "$emulator" - -# Show a list of emulators -${ANDROID_HOME}/tools/bin/avdmanager list - -# Create emulator -echo "no" | ${ANDROID_HOME}/tools/bin/avdmanager create avd -d "Nexus 6" -n testemulator -k "${emulator}" --force - -echo ${ANDROID_HOME}/emulator/emulator -list-avds - -echo "Starting emulator" - -# Start emulator in background -nohup ${ANDROID_HOME}/emulator/emulator -avd testemulator -no-boot-anim -no-snapshot > /dev/null 2>&1 & -${ANDROID_HOME}/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do sleep 1; done; input keyevent 82' - -${ANDROID_HOME}/platform-tools/adb devices - -echo "Emulator started" diff --git a/ci-jobs/functional_test.yml b/ci-jobs/functional_test.yml deleted file mode 100644 index 672a4fc4..00000000 --- a/ci-jobs/functional_test.yml +++ /dev/null @@ -1,90 +0,0 @@ -parameters: - vmImage: 'macOS-10.15' - pytestOpt: '--doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html' - androidSdkVer: 27 - xcodeForIOS: 11.6 - CI: true - -jobs: - - template: ./functional/run_ios_test.yml - parameters: - name: 'func_test_ios1' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'search_context/find_by_*.py remote_fs_tests.py safari_tests.py execute_driver_tests.py' - CI: ${{ parameters.ci }} - xcodeForIOS: ${{ parameters.xcodeForIOS }} - - template: ./functional/run_ios_test.yml - parameters: - name: 'func_test_ios2' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'applications_tests.py hw_actions_tests.py keyboard_tests.py screen_record_tests.py webdriver_tests.py' - CI: ${{ parameters.ci }} - xcodeForIOS: ${{ parameters.xcodeForIOS }} - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android1' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'device_time_tests.py search_context/find_by_*.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - OPENCV: true - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android2' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'ime_tests.py keyboard_tests.py location_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android3' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'appium_service_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - DONT_RUN_APPIUM: true - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android4' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'finger_print_tests.py screen_record_tests.py settings_tests.py chrome_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android5' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'context_switching_tests.py remote_fs_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android6' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'common_tests.py multi_action_tests.py webelement_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android7' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'applications_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android8' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'network_connection_tests.py log_event_tests.py activities_tests.py hw_actions_tests.py touch_action_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} diff --git a/ci.sh b/ci.sh deleted file mode 100755 index 4d92eb76..00000000 --- a/ci.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -EXIT_STATUS=0 - -if ! make autopep8 ARGS=--exit-code ; then - echo "Please run command 'make autopep8' on your local and commit the result" - EXIT_STATUS=1 -fi - -if ! make isort ARGS=--check-only ; then - echo "Please run command 'make isort' on your local and commit the result" - EXIT_STATUS=1 -fi - -if ! make pylint ARGS=--errors-only ; then - echo "Please run command 'make pylint' on your local and fix errors" - EXIT_STATUS=1 -fi - -if ! make unittest ; then - EXIT_STATUS=1 -fi - -if ! make mypy ; then - EXIT_STATUS=1 -fi - -exit $EXIT_STATUS diff --git a/docs/Makefile b/docs/Makefile index d4bb2cbb..3fb41af6 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,7 +4,7 @@ # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build +SPHINXBUILD ?= uv run sphinx-build SOURCEDIR = . BUILDDIR = _build diff --git a/docs/README.md b/docs/README.md index 9e0f7e68..ed9b03f1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,11 +1,3 @@ -### How to generate doc - -```bash -$cd python-client/docs -$pip install -r requirements.txt -$bash generate.sh -``` - ### How to check generated doc ```bash @@ -19,4 +11,4 @@ Access to http://localhost:1234 on web browser ### How to deploy generated doc -See https://github.com/ki4070ma/python-client-sphinx#how-to-deploy for now +Handled at https://github.com/ki4070ma/python-client-sphinx diff --git a/docs/conf.py b/docs/conf.py index 3a256a8b..236b9e5d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,12 +18,12 @@ # -- Project information ----------------------------------------------------- -project = 'Python client 1.0' -copyright = '2020, Appium' +project = 'Python client 1.1' +copyright = '2020-2025, Appium' author = 'Appium' # The full version, including alpha/beta/rc tags -release = '1.0' +release = '1.1' language = 'en' @@ -32,11 +32,7 @@ # 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' -] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.githubpages'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/generate.sh b/docs/generate.sh index 219b7325..71131e83 100644 --- a/docs/generate.sh +++ b/docs/generate.sh @@ -1,4 +1,4 @@ #!/bin/sh rm -rf *rst _build -sphinx-apidoc -F -H 'Appium python client' -o . ../appium/webdriver +uv run sphinx-apidoc -F -H 'Appium python client' -o . ../appium/webdriver make html diff --git a/docs/index.rst b/docs/index.rst index 40c4de96..9c16ec4d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,21 +1,18 @@ .. Appium python client documentation master file, created by - sphinx-quickstart on Sun May 10 14:33:16 2020. + 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. -Welcome to Appium python client's documentation! -================================================ +Appium python client documentation +================================== + +Add your content using ``reStructuredText`` syntax. See the +`reStructuredText `_ +documentation for details. + .. toctree:: :maxdepth: 4 :caption: Contents: webdriver - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 3dab8f14..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -sphinx >= 3.0, <4.0 -sphinx_rtd_theme < 1.0 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 index 1da461fb..bdc71e98 100644 --- a/docs/webdriver.common.rst +++ b/docs/webdriver.common.rst @@ -4,35 +4,18 @@ webdriver.common package Submodules ---------- -webdriver.common.mobileby module +webdriver.common.appiumby module -------------------------------- -.. automodule:: webdriver.common.mobileby +.. automodule:: webdriver.common.appiumby :members: - :undoc-members: :show-inheritance: - -webdriver.common.multi\_action module -------------------------------------- - -.. automodule:: webdriver.common.multi_action - :members: :undoc-members: - :show-inheritance: - -webdriver.common.touch\_action module -------------------------------------- - -.. automodule:: webdriver.common.touch_action - :members: - :undoc-members: - :show-inheritance: - Module contents --------------- .. automodule:: webdriver.common :members: - :undoc-members: :show-inheritance: + :undoc-members: diff --git a/docs/webdriver.extensions.android.rst b/docs/webdriver.extensions.android.rst index 55a32a1f..a1a54bfa 100644 --- a/docs/webdriver.extensions.android.rst +++ b/docs/webdriver.extensions.android.rst @@ -9,86 +9,85 @@ webdriver.extensions.android.activities module .. automodule:: webdriver.extensions.android.activities :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.android.common module ------------------------------------------ .. automodule:: webdriver.extensions.android.common :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.android.display module ------------------------------------------- .. automodule:: webdriver.extensions.android.display :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.android.gsm module --------------------------------------- .. automodule:: webdriver.extensions.android.gsm :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.android.nativekey module --------------------------------------------- .. automodule:: webdriver.extensions.android.nativekey :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.android.network module ------------------------------------------- .. automodule:: webdriver.extensions.android.network :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.android.performance module ----------------------------------------------- .. automodule:: webdriver.extensions.android.performance :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.android.power module ----------------------------------------- .. automodule:: webdriver.extensions.android.power :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.android.sms module --------------------------------------- .. automodule:: webdriver.extensions.android.sms :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.android.system\_bars module ------------------------------------------------ .. automodule:: webdriver.extensions.android.system_bars :members: - :undoc-members: :show-inheritance: - + :undoc-members: Module contents --------------- .. automodule:: webdriver.extensions.android :members: - :undoc-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 index 048cb493..631f09f1 100644 --- a/docs/webdriver.extensions.rst +++ b/docs/webdriver.extensions.rst @@ -8,7 +8,7 @@ Subpackages :maxdepth: 4 webdriver.extensions.android - webdriver.extensions.search_context + webdriver.extensions.flutter_integration Submodules ---------- @@ -18,142 +18,141 @@ webdriver.extensions.action\_helpers module .. automodule:: webdriver.extensions.action_helpers :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.applications module ---------------------------------------- .. automodule:: webdriver.extensions.applications :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.clipboard module ------------------------------------- .. automodule:: webdriver.extensions.clipboard :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.context module ----------------------------------- .. automodule:: webdriver.extensions.context :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.device\_time module ---------------------------------------- .. automodule:: webdriver.extensions.device_time :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.execute\_driver module ------------------------------------------- .. automodule:: webdriver.extensions.execute_driver :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.execute\_mobile\_command module ---------------------------------------------------- .. automodule:: webdriver.extensions.execute_mobile_command :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.hw\_actions module --------------------------------------- .. automodule:: webdriver.extensions.hw_actions :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.images\_comparison module ---------------------------------------------- .. automodule:: webdriver.extensions.images_comparison :members: - :undoc-members: :show-inheritance: - -webdriver.extensions.ime module -------------------------------- - -.. automodule:: webdriver.extensions.ime - :members: :undoc-members: - :show-inheritance: webdriver.extensions.keyboard module ------------------------------------ .. automodule:: webdriver.extensions.keyboard :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.location module ------------------------------------ .. automodule:: webdriver.extensions.location :members: - :undoc-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: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.screen\_record module ------------------------------------------ .. automodule:: webdriver.extensions.screen_record :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.session module ----------------------------------- .. automodule:: webdriver.extensions.session :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.extensions.settings module ------------------------------------ .. automodule:: webdriver.extensions.settings :members: - :undoc-members: :show-inheritance: - + :undoc-members: Module contents --------------- .. automodule:: webdriver.extensions :members: - :undoc-members: :show-inheritance: + :undoc-members: diff --git a/docs/webdriver.extensions.search_context.rst b/docs/webdriver.extensions.search_context.rst deleted file mode 100644 index cff8b87e..00000000 --- a/docs/webdriver.extensions.search_context.rst +++ /dev/null @@ -1,62 +0,0 @@ -webdriver.extensions.search\_context package -============================================ - -Submodules ----------- - -webdriver.extensions.search\_context.android module ---------------------------------------------------- - -.. automodule:: webdriver.extensions.search_context.android - :members: - :undoc-members: - :show-inheritance: - -webdriver.extensions.search\_context.base\_search\_context module ------------------------------------------------------------------ - -.. automodule:: webdriver.extensions.search_context.base_search_context - :members: - :undoc-members: - :show-inheritance: - -webdriver.extensions.search\_context.custom module --------------------------------------------------- - -.. automodule:: webdriver.extensions.search_context.custom - :members: - :undoc-members: - :show-inheritance: - -webdriver.extensions.search\_context.ios module ------------------------------------------------ - -.. automodule:: webdriver.extensions.search_context.ios - :members: - :undoc-members: - :show-inheritance: - -webdriver.extensions.search\_context.mobile module --------------------------------------------------- - -.. automodule:: webdriver.extensions.search_context.mobile - :members: - :undoc-members: - :show-inheritance: - -webdriver.extensions.search\_context.windows module ---------------------------------------------------- - -.. automodule:: webdriver.extensions.search_context.windows - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: webdriver.extensions.search_context - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/webdriver.rst b/docs/webdriver.rst index 2f1bbff6..f74a7db8 100644 --- a/docs/webdriver.rst +++ b/docs/webdriver.rst @@ -18,86 +18,109 @@ webdriver.appium\_connection module .. automodule:: webdriver.appium_connection :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.appium\_service module -------------------------------- .. automodule:: webdriver.appium_service :members: - :undoc-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: - :undoc-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: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.switch\_to module --------------------------- .. automodule:: webdriver.switch_to :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.webdriver module -------------------------- .. automodule:: webdriver.webdriver :members: - :undoc-members: :show-inheritance: + :undoc-members: webdriver.webelement module --------------------------- .. automodule:: webdriver.webelement :members: - :undoc-members: :show-inheritance: - + :undoc-members: Module contents --------------- .. automodule:: webdriver :members: - :undoc-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/release.sh b/release.sh deleted file mode 100755 index 4d5b4fe7..00000000 --- a/release.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -o pipefail - -if [ -z "$PYTHON_BIN_PATH" ]; then - PYTHON_BIN_PATH=$(which python3 || which python || true) -fi - -export PYTHON_BIN_PATH - -CONFIGURE_DIR=$(dirname "$0") -"$PYTHON_BIN_PATH" "${CONFIGURE_DIR}/script/release.py" "$@" - -echo "Finish release process" diff --git a/script/release.py b/script/release.py deleted file mode 100644 index 93381cd6..00000000 --- a/script/release.py +++ /dev/null @@ -1,169 +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. -"""Release script to publish release module to pipy.""" - -import glob -import io -import os -import shutil -import subprocess -import sys -from typing import List - -VERSION_FILE_PATH = os.path.join(os.path.dirname('__file__'), 'appium', 'version.py') -CHANGELOG_PATH = os.path.join(os.path.dirname('__file__'), 'CHANGELOG.rst') - -APPIUM_DIR_PATH = os.path.join(os.path.dirname('__file__'), 'appium') -BUILT_APPIUM_DIR_PATH = os.path.join(os.path.dirname('__file__'), 'build', 'lib', 'appium') - -MESSAGE_RED = '\033[1;31m{}\033[0m' -MESSAGE_GREEN = '\033[1;32m{}\033[0m' -MESSAGE_YELLOW = '\033[1;33m{}\033[0m' - - -def get_current_version(): - current = io.open(os.path.join(os.path.dirname('__file__'), 'appium', - 'version.py'), encoding='utf-8').read().rstrip() - print('The current version is {}, type a new one'.format(MESSAGE_YELLOW.format(current))) - return current - - -def get_new_version(): - print(MESSAGE_GREEN.format('new version:')) - for line in sys.stdin: - return line.rstrip() - - -VERSION_FORMAT = "version = '{}'\n" - - -def update_version_file(version): - new_version = VERSION_FORMAT.format(version) - with open(VERSION_FILE_PATH, 'w') as f: - f.write(new_version) - - -def call_bash_script(cmd): - if os.environ.get('DRY_RUN') is not None: - print('{} Calls: {}'.format(MESSAGE_RED.format('[DRY_RUN]'), cmd)) - else: - os.system(cmd) - - -def commit_version_code(new_version_num): - call_bash_script('git commit {} -m "Bump {}"'.format(VERSION_FILE_PATH, new_version_num)) - - -def tag_and_generate_changelog(new_version_num): - call_bash_script('git tag "v{}"'.format(new_version_num)) - call_bash_script('gitchangelog > {}'.format(CHANGELOG_PATH)) - call_bash_script('git commit {} -m "Update changelog for {}"'.format(CHANGELOG_PATH, new_version_num)) - - -def upload_sdist(new_version_num): - push_file = 'dist/Appium-Python-Client-{}.tar.gz'.format(new_version_num) - try: - call_bash_script('twine upload "{}"'.format(push_file)) - except Exception as e: - print('Failed to upload {} to pypi. ' - 'Please fix the original error and push it again later. Original error: {}'.format( - push_file, e)) - - -def push_changes_to_master(new_version_num): - call_bash_script('git push origin master') - call_bash_script('git push origin "v{}"'.format(new_version_num)) - - -def ensure_publication(new_version_num): - if os.environ.get('DRY_RUN') is not None: - print('Run with {} mode.'.format(MESSAGE_RED.format('[DRY_RUN]'))) - - print('Are you sure to release as {}?[y/n]'.format(MESSAGE_YELLOW.format(new_version_num))) - for line in sys.stdin: - if line.rstrip().lower() == 'y': - return - exit('Canceled release process.') - - -def build_sdist(): - call_bash_script('{} setup.py sdist'.format(sys.executable)) - - -def validate_release_env(): - if os.system('which twine') != 0: - exit("Please get twine via 'pip install twine'") - if os.system('which gitchangelog') != 0: - exit("Please get twine via 'pip install gitchangelog' or 'pip install git+git://github.com/vaab/gitchangelog.git' for Python 3.7") - - -def build() -> None: - shutil.rmtree(BUILT_APPIUM_DIR_PATH, ignore_errors=True) - status, output = subprocess.getstatusoutput('{} setup.py install'.format(os.getenv('PYTHON_BIN_PATH'))) - if status != 0: - exit(f'Failed to build the package:\n{output}') - - -def get_py_files_in_dir(root_dir: str) -> List[str]: - return [ - file_path[len(root_dir):] - for file_path in glob.glob(f"{root_dir}/**/*.py", recursive=True) + glob.glob(f"{root_dir}/**/*.typed", recursive=True) - ] - - -def assert_files_count_in_package() -> None: - original_files = get_py_files_in_dir(APPIUM_DIR_PATH) - built_files = get_py_files_in_dir(BUILT_APPIUM_DIR_PATH) - - if len(original_files) != len(built_files): - print(f"The count of files in '{APPIUM_DIR_PATH}' and '{BUILT_APPIUM_DIR_PATH}' were different.") - - original_files_set = set(original_files) - built_files_set = set(built_files) - - diff = original_files_set.difference(built_files_set) - if diff: - print(f"'{APPIUM_DIR_PATH}' has '{diff}' files than {BUILT_APPIUM_DIR_PATH}") - diff = built_files_set.difference(original_files_set) - if diff: - print(f"{BUILT_APPIUM_DIR_PATH} has {diff} files than {APPIUM_DIR_PATH}") - - exit(f"Python files in '{BUILT_APPIUM_DIR_PATH}' may differ from '{APPIUM_DIR_PATH}'. " - "Please make sure setup.py is configured properly.") - - -def main(): - validate_release_env() - - get_current_version() - new_version = get_new_version() - - update_version_file(new_version) - - build() - assert_files_count_in_package() - - ensure_publication(new_version) - - commit_version_code(new_version) - build_sdist() - - tag_and_generate_changelog(new_version) - - upload_sdist(new_version) - push_changes_to_master(new_version) - - -if __name__ == '__main__': - main() diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e6f191e2..00000000 --- a/setup.cfg +++ /dev/null @@ -1,17 +0,0 @@ -[pycodestyle] -max-line-length = 120 - -[isort] -multi_line_output = 3 -known_third_party = dateutil,httpretty,pytest,selenium,setuptools,urllib3,mock,sauceclient -known_first_party = test,appium - -[mypy] -check_untyped_defs = True -disallow_untyped_calls = True -disallow_untyped_defs = True -follow_imports = skip -ignore_missing_imports = True -strict_optional = True -warn_redundant_casts = True -warn_unused_ignores = True diff --git a/setup.py b/setup.py index d3b6f507..f32eab92 100644 --- a/setup.py +++ b/setup.py @@ -11,47 +11,35 @@ # WITHOUT 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 io -import os -from setuptools import find_packages, 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 -from appium.common.helper import library_version +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=library_version(), - description='Python client for Appium', - long_description=io.open(os.path.join(os.path.dirname('__file__'), 'README.md'), encoding='utf-8').read(), - long_description_content_type='text/markdown', - keywords=[ - 'appium', - 'selenium', - 'selenium 3', - 'python client', - 'mobile automation' - ], - author='Isaac Murchie', - author_email='isaac@saucelabs.com', - maintainer='Kazuaki Matsuo, Mykola Mokhnach, Mori Atsushi', - url='http://appium.io/', - package_data={'appium': ['webdriver/py.typed']}, + 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='Apache 2.0', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - '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 >= 3.14.1, < 4'] + license=project['license'], + classifiers=project['classifiers'], + install_requires=project['dependencies'], ) diff --git a/test/apps/ApiDemos-debug.apk.zip b/test/apps/ApiDemos-debug.apk.zip deleted file mode 100644 index fcb29ca7..00000000 Binary files a/test/apps/ApiDemos-debug.apk.zip 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 0b112d2e..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/android/activities_tests.py b/test/functional/android/activities_tests.py deleted file mode 100644 index 7f9978be..00000000 --- a/test/functional/android/activities_tests.py +++ /dev/null @@ -1,40 +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. - -from .helper.test_helper import APIDEMO_PKG_NAME, BaseTestCase - - -class TestActivities(BaseTestCase): - def test_current_activity(self) -> None: - activity = self.driver.current_activity - assert '.ApiDemos' == activity - - def test_start_activity_this_app(self) -> None: - self.driver.start_activity(APIDEMO_PKG_NAME, ".ApiDemos") - self._assert_activity_contains('Demos') - - self.driver.start_activity(APIDEMO_PKG_NAME, ".accessibility.AccessibilityNodeProviderActivity") - self._assert_activity_contains('Node') - - def test_start_activity_other_app(self) -> None: - self.driver.start_activity(APIDEMO_PKG_NAME, ".ApiDemos") - self._assert_activity_contains('Demos') - - self.driver.start_activity("com.android.calculator2", ".Calculator") - self._assert_activity_contains('Calculator') - - def _assert_activity_contains(self, activity: str) -> None: - current = self.driver.current_activity - assert activity in current diff --git a/test/functional/android/appium_service_tests.py b/test/functional/android/appium_service_tests.py index 97f3316e..409b8098 100644 --- a/test/functional/android/appium_service_tests.py +++ b/test/functional/android/appium_service_tests.py @@ -13,31 +13,34 @@ # See the License for the specific language governing permissions and # limitations under the License. -from appium.webdriver.appium_service import AppiumService -from appium.webdriver.common.mobileby import MobileBy -from test.functional.android.helper.test_helper import ( - BaseTestCase, - wait_for_element -) +from typing import Generator + +import pytest -DEFAULT_PORT = 4723 +from appium.webdriver.appium_service import AppiumService -class TestAppiumService(BaseTestCase): +@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', + ] + ) - service: AppiumService + yield service - @classmethod - def setup_class(cls) -> None: - cls.service = AppiumService() - cls.service.start(args=['--address', '127.0.0.1', '-p', str(DEFAULT_PORT)]) + service.stop() - def test_appium_service(self) -> None: - assert self.service.is_running - assert self.service.is_listening - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Accessibility') - assert el is not None - @classmethod - def teardown_class(cls) -> None: - cls.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/applications_tests.py b/test/functional/android/applications_tests.py deleted file mode 100644 index ea2216a8..00000000 --- a/test/functional/android/applications_tests.py +++ /dev/null @@ -1,77 +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 os -from time import sleep - -import pytest - -from appium.webdriver.applicationstate import ApplicationState - -from .helper.desired_capabilities import PATH -from .helper.test_helper import APIDEMO_PKG_NAME, BaseTestCase - - -class TestApplications(BaseTestCase): - - def test_background_app(self) -> None: - self.driver.background_app(1) - sleep(3) - self.driver.launch_app() - - def test_is_app_installed(self) -> None: - assert not self.driver.is_app_installed('sdfsdf') - assert self.driver.is_app_installed(APIDEMO_PKG_NAME) - - @pytest.mark.skip('This causes the server to crash. no idea why') - def test_install_app(self) -> None: - assert not self.driver.is_app_installed('io.selendroid.testapp') - self.driver.install_app(PATH(os.path.join('../..', 'apps', 'selendroid-test-app.apk'))) - assert self.driver.is_app_installed('io.selendroid.testapp') - - def test_remove_app(self) -> None: - assert self.driver.is_app_installed(APIDEMO_PKG_NAME) - self.driver.remove_app(APIDEMO_PKG_NAME) - assert not self.driver.is_app_installed(APIDEMO_PKG_NAME) - - def test_close_and_launch_app(self) -> None: - self.driver.close_app() - self.driver.launch_app() - activity = self.driver.current_activity - assert '.ApiDemos' == activity - - def test_app_management(self) -> None: - app_id = self.driver.current_package - assert self.driver.query_app_state(app_id) == ApplicationState.RUNNING_IN_FOREGROUND - self.driver.background_app(-1) - assert self.driver.query_app_state(app_id) < ApplicationState.RUNNING_IN_FOREGROUND - self.driver.activate_app(app_id) - assert self.driver.query_app_state(app_id) == ApplicationState.RUNNING_IN_FOREGROUND - - def test_app_strings(self) -> None: - strings = self.driver.app_strings() - assert u'You can\'t wipe my data, you are a monkey!' == strings[u'monkey_wipe_data'] - - def test_app_strings_with_language(self) -> None: - strings = self.driver.app_strings('en') - assert u'You can\'t wipe my data, you are a monkey!' == strings[u'monkey_wipe_data'] - - def test_app_strings_with_language_and_file(self) -> None: - strings = self.driver.app_strings('en', 'some_file') - assert u'You can\'t wipe my data, you are a monkey!' == strings[u'monkey_wipe_data'] - - def test_reset(self) -> None: - self.driver.reset() - assert self.driver.is_app_installed(APIDEMO_PKG_NAME) 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 08767349..52f99439 100644 --- a/test/functional/android/chrome_tests.py +++ b/test/functional/android/chrome_tests.py @@ -12,22 +12,39 @@ # 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.common.appiumby import AppiumBy +from test.helpers.constants import SERVER_URL_BASE + +from .options import make_options + +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + -from .helper.desired_capabilities import get_desired_capabilities +@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) + yield driver -class TestChrome(object): - def setup_method(self) -> None: - caps = get_desired_capabilities() - caps['browserName'] = 'Chrome' - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', caps) + driver.quit() - def teardown_method(self) -> None: - self.driver.quit() - def test_find_single_element(self) -> None: - self.driver.get('http://10.0.2.2:4723/test/guinea-pig') - self.driver.find_element_by_link_text('i am a link').click() +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 == '' - assert 'I am some other page content' in self.driver.page_source + # Chrome browser's default page + assert '' in driver.page_source diff --git a/test/functional/android/common_tests.py b/test/functional/android/common_tests.py deleted file mode 100644 index 7768c376..00000000 --- a/test/functional/android/common_tests.py +++ /dev/null @@ -1,68 +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. - -from time import sleep - -import pytest -from selenium.common.exceptions import NoSuchElementException - -from appium.webdriver.common.mobileby import MobileBy - -from ..test_helper import is_ci -from .helper.test_helper import ( - APIDEMO_PKG_NAME, - BaseTestCase, - wait_for_element -) - - -class TestCommon(BaseTestCase): - - def test_current_package(self) -> None: - assert APIDEMO_PKG_NAME == self.driver.current_package - - @pytest.mark.skip('Not sure how to set this up to run') - def test_end_test_coverage(self) -> None: - self.driver.end_test_coverage(intent='android.intent.action.MAIN', path='') - sleep(5) - - # TODO Due to unexpected dialog, "System UI isn't responding" - @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI.') - def test_open_notifications(self) -> None: - for word in ['App', 'Notification', 'Status Bar', ':-|']: - wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, - f'new UiSelector().text("{word}")').click() - - self.driver.open_notifications() - sleep(1) - with pytest.raises(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 - assert title - assert body - - self.driver.keyevent(4) - sleep(1) - self.driver.find_element_by_android_uiautomator('new UiSelector().text(":-|")') diff --git a/test/functional/android/context_switching_tests.py b/test/functional/android/context_switching_tests.py deleted file mode 100644 index c358866a..00000000 --- a/test/functional/android/context_switching_tests.py +++ /dev/null @@ -1,59 +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 pytest - -from appium import webdriver -from appium.common.exceptions import NoSuchContextException - -from .helper import desired_capabilities - - -@pytest.mark.skip(reason="Need to fix broken test") -class TestContextSwitching(object): - def setup_method(self) -> None: - desired_caps = desired_capabilities.get_desired_capabilities('selendroid-test-app.apk') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - def teardown_method(self) -> None: - self.driver.quit() - - def test_contexts_list(self) -> None: - self._enter_webview() - contexts = self.driver.contexts - assert 2 == len(contexts) - - def test_move_to_correct_context(self) -> None: - self._enter_webview() - assert 'WEBVIEW_io.selendroid.testapp' == self.driver.current_context - - def test_actually_in_webview(self) -> None: - 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')]") - assert el is not None - - def test_move_back_to_native_context(self) -> None: - self._enter_webview() - self.driver.switch_to.context(None) - assert 'NATIVE_APP' == self.driver.current_context - - def test_set_invalid_context(self) -> None: - with pytest.raises(NoSuchContextException): - self.driver.switch_to.context('invalid name') - - def _enter_webview(self) -> None: - btn = self.driver.find_element_by_name('buttonStartWebviewCD') - btn.click() - self.driver.switch_to.context('WEBVIEW') diff --git a/test/functional/android/file/find_by_image_failure.png b/test/functional/android/file/find_by_image_failure.png deleted file mode 100644 index 5ea155cc..00000000 Binary files a/test/functional/android/file/find_by_image_failure.png and /dev/null differ diff --git a/test/functional/android/file/find_by_image_success.png b/test/functional/android/file/find_by_image_success.png deleted file mode 100644 index 39435fd3..00000000 Binary files a/test/functional/android/file/find_by_image_success.png and /dev/null differ diff --git a/test/functional/android/file/test_file.txt b/test/functional/android/file/test_file.txt deleted file mode 100644 index 4fac12a9..00000000 --- a/test/functional/android/file/test_file.txt +++ /dev/null @@ -1 +0,0 @@ -HEllo diff --git a/test/functional/android/file/test_image.jpg b/test/functional/android/file/test_image.jpg deleted file mode 100644 index 6434234f..00000000 Binary files a/test/functional/android/file/test_image.jpg and /dev/null differ diff --git a/test/functional/android/helper/desired_capabilities.py b/test/functional/android/helper/desired_capabilities.py deleted file mode 100644 index 7ddcd80a..00000000 --- a/test/functional/android/helper/desired_capabilities.py +++ /dev/null @@ -1,40 +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 os -from typing import Any, Dict, Optional - -# Returns abs path relative to this file and not cwd - - -def PATH(p: str) -> str: - return os.path.abspath( - os.path.join(os.path.dirname(__file__), '..', p) - ) - - -def get_desired_capabilities(app: Optional[str] = None) -> Dict[str, Any]: - desired_caps: Dict[str, Any] = { - 'platformName': 'Android', - 'deviceName': 'Android Emulator', - 'newCommandTimeout': 240, - 'automationName': 'UIAutomator2', - 'uiautomator2ServerInstallTimeout': 120000, - 'adbExecTimeout': 120000 - } - - if app is not None: - desired_caps['app'] = PATH(os.path.join('../..', 'apps', app)) - - return desired_caps diff --git a/test/functional/android/helper/test_helper.py b/test/functional/android/helper/test_helper.py deleted file mode 100644 index c964ae8b..00000000 --- a/test/functional/android/helper/test_helper.py +++ /dev/null @@ -1,74 +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 base64 -import os -from typing import TYPE_CHECKING - -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.support.ui import WebDriverWait - -from appium import webdriver -from test.functional.test_helper import is_ci - -from . import desired_capabilities - -if TYPE_CHECKING: - from appium.webdriver.webelement import WebElement - from appium.webdriver.webdriver import WebDriver - -# the emulator is sometimes slow and needs time to think -SLEEPY_TIME = 10 - -# The package name of ApiDemos-debug.apk.zip -APIDEMO_PKG_NAME = 'io.appium.android.apis' - - -def wait_for_element(driver: 'WebDriver', locator: str, value: str, timeout: int = SLEEPY_TIME) -> 'WebElement': - """Wait until the element located - - Args: - driver: WebDriver instance - locator: Locator like WebDriver, Mobile JSON Wire Protocol - (e.g. `appium.webdriver.common.mobileby.MobileBy.ACCESSIBILITY_ID`) - value: Query value to locator - timeout: 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).until( - EC.presence_of_element_located((locator, value)) - ) - - -class BaseTestCase(): - - def setup_method(self, method) -> None: # type: ignore - desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - if is_ci(): - self.driver.start_recording_screen() - - def teardown_method(self, method) -> None: # type: ignore - if is_ci(): - payload = self.driver.stop_recording_screen() - video_path = os.path.join(os.getcwd(), method.__name__ + '.mp4') - with open(video_path, "wb") as fd: - fd.write(base64.b64decode(payload)) - self.driver.quit() diff --git a/test/functional/android/ime_tests.py b/test/functional/android/ime_tests.py deleted file mode 100644 index ea0f6ea0..00000000 --- a/test/functional/android/ime_tests.py +++ /dev/null @@ -1,51 +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. - -from time import sleep - -from .helper.test_helper import BaseTestCase - -ANDROID_LATIN = 'com.android.inputmethod.latin/.LatinIME' # Android L/M/N -GOOGLE_LATIN = 'com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME' # Android O/P - - -class TestIME(BaseTestCase): - def test_available_ime_engines(self) -> None: - engines = self.driver.available_ime_engines - assert isinstance(engines, list) - assert ANDROID_LATIN in engines or GOOGLE_LATIN in engines - - def test_is_ime_active(self) -> None: - assert self.driver.is_ime_active() - - def test_active_ime_engine(self) -> None: - engines = self.driver.available_ime_engines - assert self.driver.active_ime_engine in engines - - def test_activate_ime_engine(self) -> None: - engines = self.driver.available_ime_engines - - self.driver.activate_ime_engine(engines[-1]) - assert self.driver.active_ime_engine == engines[-1] - - def test_deactivate_ime_engine(self) -> None: - engines = self.driver.available_ime_engines - self.driver.activate_ime_engine(engines[-1]) - - assert self.driver.active_ime_engine == engines[-1] - - self.driver.deactivate_ime_engine() - sleep(1) - assert self.driver.active_ime_engine != engines[-1] diff --git a/test/functional/android/multi_action_tests.py b/test/functional/android/multi_action_tests.py deleted file mode 100644 index 6739bf59..00000000 --- a/test/functional/android/multi_action_tests.py +++ /dev/null @@ -1,109 +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. - -from time import sleep - -import pytest - -from appium.webdriver.common.mobileby import MobileBy -from appium.webdriver.common.multi_action import MultiAction -from appium.webdriver.common.touch_action import TouchAction - -from .helper.test_helper import BaseTestCase, is_ci, wait_for_element - - -class TestMultiAction(BaseTestCase): - def test_parallel_actions(self) -> None: - self._move_to_splitting_touches_accros_views() - - 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) -> None: - self._move_to_splitting_touches_accros_views() - - 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 _move_to_splitting_touches_accros_views(self) -> None: - el1 = self.driver.find_element_by_accessibility_id('Content') - el2 = self.driver.find_element_by_accessibility_id('Animation') - self.driver.scroll(el1, el2) - - el = self.driver.find_element_by_accessibility_id('Views') - action = TouchAction(self.driver) - action.tap(el).perform() - - # simulate a swipe/scroll - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Expandable Lists') - action.press(el).move_to(x=100, y=-1000).release().perform() - el = self.driver.find_element_by_accessibility_id('Layouts') - action.press(el).move_to(x=100, y=-1000).release().perform() - - el = self.driver.find_element_by_accessibility_id('Splitting Touches across Views') - action.tap(el).perform() - - wait_for_element(self.driver, MobileBy.ID, 'io.appium.android.apis:id/list1') - - @pytest.mark.skipif(condition=is_ci(), reason='Skip since the test must be watched to check if it works') - def test_driver_multi_tap(self) -> None: - el = self.driver.find_element_by_accessibility_id('Graphics') - action = TouchAction(self.driver) - action.tap(el).perform() - - wait_for_element(self.driver, MobileBy.CLASS_NAME, 'android.widget.TextView') - 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_accessibility_id('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) diff --git a/test/functional/android/network_connection_tests.py b/test/functional/android/network_connection_tests.py deleted file mode 100644 index e0a5fb7d..00000000 --- a/test/functional/android/network_connection_tests.py +++ /dev/null @@ -1,33 +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 pytest - -from appium.webdriver.connectiontype import ConnectionType - -from ..test_helper import is_ci -from .helper.test_helper import BaseTestCase - - -class TestNetworkConnection(BaseTestCase): - def test_get_network_connection(self) -> None: - nc = self.driver.network_connection - assert isinstance(nc, int) - - @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI') - def test_set_network_connection(self) -> None: - nc = self.driver.set_network_connection(ConnectionType.DATA_ONLY) - assert isinstance(nc, int) - assert nc == ConnectionType.DATA_ONLY 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/remote_fs_tests.py b/test/functional/android/remote_fs_tests.py deleted file mode 100644 index a71fe30c..00000000 --- a/test/functional/android/remote_fs_tests.py +++ /dev/null @@ -1,58 +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 base64 -import os -import random -from io import BytesIO -from zipfile import ZipFile - -from .helper.test_helper import BaseTestCase - - -class TestRemoteFs(BaseTestCase): - def test_push_pull_file(self) -> None: - dest_path = '/data/local/tmp/test_push_file.txt' - data = bytes('This is the contents of the file to push to the device.', 'utf-8') - - self.driver.push_file(dest_path, base64.b64encode(data).decode('utf-8')) - data_ret = base64.b64decode(self.driver.pull_file(dest_path)) - - assert data == data_ret - - def test_pull_folder(self) -> None: - data = bytes('random string data {}'.format(random.randint(0, 1000)), 'utf-8') - dest_dir = '/data/local/tmp/' - - for filename in ['1.txt', '2.txt']: - self.driver.push_file(os.path.join(dest_dir, filename), base64.b64encode(data).decode('utf-8')) - - folder = self.driver.pull_folder(dest_dir) - - with ZipFile(BytesIO(base64.b64decode(folder))) as fzip: - for filename in ['1.txt', '2.txt']: - assert filename in fzip.namelist() - - def test_push_file_with_src_path(self) -> None: - test_files = ['test_image.jpg', 'test_file.txt'] - for file_name in test_files: - src_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'file', file_name) - dest_path = os.path.join('/data/local/tmp/', file_name) - - with open(src_path, 'rb') as fr: - original_data = fr.read() - - self.driver.push_file(dest_path, source_path=src_path) - new_data = base64.b64decode(self.driver.pull_file(dest_path)) - assert original_data == new_data diff --git a/test/functional/android/screen_record_tests.py b/test/functional/android/screen_record_tests.py deleted file mode 100644 index 49618a89..00000000 --- a/test/functional/android/screen_record_tests.py +++ /dev/null @@ -1,26 +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. - -from time import sleep - -from .helper.test_helper import BaseTestCase - - -class TestScreenRecord(BaseTestCase): - def test_screen_record(self) -> None: - self.driver.start_recording_screen(timeLimit=10, forcedRestart=True) - sleep(10) - result = self.driver.stop_recording_screen() - assert len(result) > 0 diff --git a/test/functional/android/search_context/find_by_accessibility_id_tests.py b/test/functional/android/search_context/find_by_accessibility_id_tests.py deleted file mode 100644 index 6ef5cf23..00000000 --- a/test/functional/android/search_context/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 pytest - -from appium.webdriver.common.mobileby import MobileBy -from test.functional.android.helper.test_helper import ( - BaseTestCase, - is_ci, - wait_for_element -) - - -class TestFindByAccessibilityID(BaseTestCase): - def test_find_single_element(self) -> None: - wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Accessibility")').click() - wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, - 'new UiSelector().text("Accessibility Node Querying")').click() - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Task Take out Trash') - assert el is not None - - def test_find_multiple_elements(self) -> None: - els = self.driver.find_elements_by_accessibility_id('Accessibility') - assert isinstance(els, list) - - @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI') - def test_element_find_single_element(self) -> None: - wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Accessibility")').click() - wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, - 'new UiSelector().text("Accessibility Node Querying")').click() - el = wait_for_element(self.driver, MobileBy.CLASS_NAME, 'android.widget.ListView') - - sub_el = el.find_element_by_accessibility_id('Task Take out Trash') - assert sub_el is not None - - def test_element_find_multiple_elements(self) -> None: - wait_for_element(self.driver, MobileBy.CLASS_NAME, 'android.widget.ListView') - el = self.driver.find_element_by_class_name('android.widget.ListView') - - sub_els = el.find_elements_by_accessibility_id('Animation') - assert isinstance(sub_els, list) diff --git a/test/functional/android/search_context/find_by_image_tests.py b/test/functional/android/search_context/find_by_image_tests.py deleted file mode 100644 index 86dec00e..00000000 --- a/test/functional/android/search_context/find_by_image_tests.py +++ /dev/null @@ -1,77 +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 base64 - -import pytest -from selenium.common.exceptions import NoSuchElementException, TimeoutException - -from appium import webdriver -from appium.webdriver.common.mobileby import MobileBy -from test.functional.android.helper import desired_capabilities -from test.functional.android.helper.test_helper import wait_for_element - - -class TestFindByImage(object): - - def setup_method(self) -> None: - desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - - # relax template matching - self.driver.update_settings({"fixImageFindScreenshotDims": False, - "fixImageTemplateSize": True, - "autoUpdateImageElementPosition": True}) - - def teardown_method(self) -> None: - self.driver.quit() - - def test_find_based_on_image_template(self) -> None: - image_path = desired_capabilities.PATH('file/find_by_image_success.png') - with open(image_path, 'rb') as png_file: - b64_data = base64.b64encode(png_file.read()).decode('UTF-8') - - el = wait_for_element(self.driver, MobileBy.IMAGE, b64_data) - size = el.size - assert size['width'] is not None - assert size['height'] is not None - loc = el.location - assert loc['x'] is not None - assert loc['y'] is not None - rect = el.rect - assert rect['width'] is not None - assert rect['height'] is not None - assert rect['x'] is not None - assert rect['y'] is not None - assert el.is_displayed() - el.click() - wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, "Alarm") - - def test_find_multiple_elements_by_image_just_returns_one(self) -> None: - wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, "App") - image_path = desired_capabilities.PATH('file/find_by_image_success.png') - els = self.driver.find_elements_by_image(image_path) - els[0].click() - wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, "Alarm") - - def test_find_throws_no_such_element(self) -> None: - image_path = desired_capabilities.PATH('file/find_by_image_failure.png') - with open(image_path, 'rb') as png_file: - b64_data = base64.b64encode(png_file.read()).decode('UTF-8') - - with pytest.raises(TimeoutException): - wait_for_element(self.driver, MobileBy.IMAGE, b64_data, timeout=3) - - with pytest.raises(NoSuchElementException): - self.driver.find_element_by_image(image_path) diff --git a/test/functional/android/search_context/find_by_uiautomator_tests.py b/test/functional/android/search_context/find_by_uiautomator_tests.py deleted file mode 100644 index 2c23cfee..00000000 --- a/test/functional/android/search_context/find_by_uiautomator_tests.py +++ /dev/null @@ -1,46 +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 pytest - -from test.functional.android.helper.test_helper import BaseTestCase - - -@pytest.mark.skip(reason="Need to fix flaky test") -class TestFindByUIAutomator(BaseTestCase): - def test_find_single_element(self) -> None: - el = self.driver.find_element_by_android_uiautomator('new UiSelector().text("Animation")') - assert el is not None - - def test_find_multiple_elements(self) -> None: - els = self.driver.find_elements_by_android_uiautomator('new UiSelector().clickable(true)') - assert isinstance(els, list) - - def test_element_find_single_element(self) -> None: - el = self.driver.find_element_by_class_name('android.widget.ListView') - - sub_el = el.find_element_by_android_uiautomator('new UiSelector().description("Animation")') - assert sub_el is not None - - def test_element_find_multiple_elements(self) -> None: - el = self.driver.find_element_by_class_name('android.widget.ListView') - - sub_els = el.find_elements_by_android_uiautomator('new UiSelector().clickable(true)') - assert isinstance(sub_els, list) - - def test_scroll_into_view(self) -> None: - 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() - # TODO Add assert diff --git a/test/functional/android/search_context/find_by_view_matcher_tests.py b/test/functional/android/search_context/find_by_view_matcher_tests.py deleted file mode 100644 index 5bb0a47d..00000000 --- a/test/functional/android/search_context/find_by_view_matcher_tests.py +++ /dev/null @@ -1,69 +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 os -import unittest - -import pytest -from selenium.common.exceptions import WebDriverException - -from appium import webdriver -from appium.webdriver.common.mobileby import MobileBy -from appium.webdriver.extensions.search_context.android import ( - AndroidSearchContext -) -from test.functional.android.helper.test_helper import ( - BaseTestCase, - desired_capabilities, - is_ci -) - - -class TestFindByViewMatcher(BaseTestCase): - - # Override - def setup_method(self, method) -> None: # type: ignore - desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip') - desired_caps['automationName'] = 'Espresso' - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - if is_ci(): - self.driver.start_recording_screen() - - def test_find_single_element(self) -> None: - el = self.driver.find_element_by_android_view_matcher( - name='withText', args=['Accessibility'], className='ViewMatchers') - assert el.text == 'Accessibility' - - def test_find_single_element_ful_class_name(self) -> None: - el = self.driver.find_element_by_android_view_matcher( - name='withText', args=['Accessibility'], className='androidx.test.espresso.matcher.ViewMatchers') - assert el.text == 'Accessibility' - - def test_find_single_element_using_hamcrest_matcher(self) -> None: - el = self.driver.find_element_by_android_view_matcher( - name='withText', - args={ - 'name': 'containsString', - 'args': 'Animati', - 'class': 'org.hamcrest.Matchers'}, - className='ViewMatchers') - assert el.text == 'Animation' - - # androidx.test.espresso.AmbiguousViewMatcherException: - # 'with text: a string containing "Access"' matches multiple views in the hierarchy. - def test_find_multiple_elements(self) -> None: - value = AndroidSearchContext()._build_data_matcher( - name='withSubstring', args=['Access'], className='ViewMatchers') - with pytest.raises(WebDriverException): - self.driver.find_elements(by=MobileBy.ANDROID_VIEW_MATCHER, value=value) diff --git a/test/functional/android/settings_tests.py b/test/functional/android/settings_tests.py deleted file mode 100644 index ac600a3c..00000000 --- a/test/functional/android/settings_tests.py +++ /dev/null @@ -1,27 +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. - -from .helper.test_helper import BaseTestCase - - -class TestSettings(BaseTestCase): - def test_get_settings(self) -> None: - settings = self.driver.get_settings() - assert settings is not None - - def test_update_settings(self) -> None: - self.driver.update_settings({"waitForIdleTimeout": 10001}) - settings = self.driver.get_settings() - assert settings["waitForIdleTimeout"] == 10001 diff --git a/test/functional/android/touch_action_tests.py b/test/functional/android/touch_action_tests.py deleted file mode 100644 index 42f22fb6..00000000 --- a/test/functional/android/touch_action_tests.py +++ /dev/null @@ -1,196 +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 pytest -from selenium.common.exceptions import NoSuchElementException - -from appium.webdriver.common.mobileby import MobileBy -from appium.webdriver.common.touch_action import TouchAction - -from .helper.test_helper import ( - APIDEMO_PKG_NAME, - BaseTestCase, - is_ci, - wait_for_element -) - - -class TestTouchAction(BaseTestCase): - def test_tap(self) -> None: - el = self.driver.find_element_by_accessibility_id('Animation') - action = TouchAction(self.driver) - action.tap(el).perform() - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Bouncing Balls') - assert el is not None - - def test_tap_x_y(self) -> None: - el = self.driver.find_element_by_accessibility_id('Animation') - action = TouchAction(self.driver) - action.tap(el, 100, 10).perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Bouncing Balls') - assert el is not None - - @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI.') - def test_tap_twice(self) -> None: - el = self.driver.find_element_by_accessibility_id('Text') - action = TouchAction(self.driver) - action.tap(el).perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'LogTextBox') - action.tap(el).perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Add') - action.tap(el, count=2).perform() - - els = self.driver.find_elements_by_class_name('android.widget.TextView') - assert 'This is a test\nThis is a test\n' == els[1].get_attribute("text") - - def test_press_and_immediately_release(self) -> None: - el = self.driver.find_element_by_accessibility_id('Animation') - action = TouchAction(self.driver) - action.press(el).release().perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Bouncing Balls') - assert el is not None - - def test_press_and_immediately_release_x_y(self) -> None: - el = self.driver.find_element_by_accessibility_id('Animation') - action = TouchAction(self.driver) - action.press(el, 100, 10).release().perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Bouncing Balls') - assert el is not None - - def test_press_and_wait(self) -> None: - self._move_to_custom_adapter() - action = TouchAction(self.driver) - - el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, - 'new UiSelector().text("People Names")') - action.press(el).wait(2000).perform() - - # 'Sample menu' only comes up with a long press, not a press - el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, - 'new UiSelector().text("Sample menu")') - assert el is not None - - def test_press_and_moveto(self) -> None: - 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 = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Views') - assert el is not None - - def test_press_and_moveto_x_y(self) -> None: - 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 = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Views') - assert el is not None - - def test_long_press(self) -> None: - self._move_to_custom_adapter() - action = TouchAction(self.driver) - - el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, - 'new UiSelector().text("People Names")') - action.long_press(el).perform() - - # 'Sample menu' only comes up with a long press, not a tap - el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, - 'new UiSelector().text("Sample menu")') - assert el is not None - - @pytest.mark.skipif(condition=is_ci(), reason='Skip since this check is low robust due to hard-coded position.') - def test_long_press_x_y(self) -> None: - self._move_to_custom_adapter() - action = TouchAction(self.driver) - - # the element "People Names" is located at 430:310 (top left corner) - # location can be changed by phone resolusion, OS version - action.long_press(x=430, y=310).perform() - - # 'Sample menu' only comes up with a long press, not a tap - el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, - 'new UiSelector().text("Sample menu")') - assert el is not None - - def test_drag_and_drop(self) -> None: - self._move_to_views() - action = TouchAction(self.driver) - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Drag and Drop') - action.tap(el).perform() - - dd3 = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_dot_3'.format(APIDEMO_PKG_NAME)) - dd2 = self.driver.find_element_by_id('{}:id/drag_dot_2'.format(APIDEMO_PKG_NAME)) - - # dnd is stimulated by longpress-move_to-release - action.long_press(dd3).move_to(dd2).release().perform() - - el = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_result_text'.format(APIDEMO_PKG_NAME)) - assert 'Dropped!' in el.text - - def test_driver_drag_and_drop(self) -> None: - self._move_to_views() - action = TouchAction(self.driver) - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Drag and Drop') - action.tap(el).perform() - - dd3 = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_dot_3'.format(APIDEMO_PKG_NAME)) - dd2 = self.driver.find_element_by_id('{}:id/drag_dot_2'.format(APIDEMO_PKG_NAME)) - - self.driver.drag_and_drop(dd3, dd2) - - el = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_result_text'.format(APIDEMO_PKG_NAME)) - assert 'Dropped!' in el.text - - def test_driver_swipe(self) -> None: - el = self.driver.find_element_by_accessibility_id('Views') - action = TouchAction(self.driver) - action.tap(el).perform() - - with pytest.raises(NoSuchElementException): - self.driver.find_element_by_accessibility_id('ImageView') - - self.driver.swipe(100, 1000, 100, 100, 800) - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'ImageView') - assert el is not None - - def _move_to_views(self) -> None: - el1 = self.driver.find_element_by_accessibility_id('Content') - el2 = self.driver.find_element_by_accessibility_id('Animation') - self.driver.scroll(el1, el2) - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Views') - action = TouchAction(self.driver) - action.tap(el).perform() - - def _move_to_custom_adapter(self) -> None: - self._move_to_views() - action = TouchAction(self.driver) - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Expandable Lists') - action.tap(el).perform() - - el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, '1. Custom Adapter') - action.tap(el).perform() diff --git a/test/functional/android/webelement_tests.py b/test/functional/android/webelement_tests.py deleted file mode 100644 index d9c484c4..00000000 --- a/test/functional/android/webelement_tests.py +++ /dev/null @@ -1,53 +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. - -from appium.webdriver.common.mobileby import MobileBy - -from .helper.test_helper import ( - APIDEMO_PKG_NAME, - BaseTestCase, - wait_for_element -) - - -class TestWebelement(BaseTestCase): - def test_element_location_in_view(self) -> None: - el = self.driver.find_element_by_accessibility_id('Content') - loc = el.location_in_view - assert loc['x'] is not None - assert loc['y'] is not None - - def test_set_text(self) -> None: - self.driver.find_element_by_android_uiautomator( - 'new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("Views").instance(0));').click() - - wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Controls').click() - wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, '1. Light Theme').click() - - el = wait_for_element(self.driver, MobileBy.CLASS_NAME, 'android.widget.EditText') - el.send_keys('original text') - el.set_text('new text') - - assert 'new text' == el.text - - def test_send_keys(self) -> None: - for text in ['App', 'Activity', 'Custom Title']: - wait_for_element(self.driver, MobileBy.XPATH, - f"//android.widget.TextView[@text='{text}']").click() - - el = wait_for_element(self.driver, MobileBy.ID, '{}:id/left_text_edit'.format(APIDEMO_PKG_NAME)) - el.send_keys(' text') - - assert 'Left is best text' == el.text 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/applications_tests.py b/test/functional/ios/applications_tests.py deleted file mode 100644 index c3b3ef1c..00000000 --- a/test/functional/ios/applications_tests.py +++ /dev/null @@ -1,32 +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. - -from appium.webdriver.applicationstate import ApplicationState -from test.functional.ios.helper.test_helper import BaseTestCase - -from .helper import desired_capabilities - - -class TestWebDriver(BaseTestCase): - - def test_app_management(self) -> None: - # this only works in Xcode9+ - if float(desired_capabilities.get_desired_capabilities( - desired_capabilities.BUNDLE_ID)['platformVersion']) < 11: - return - assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND - self.driver.background_app(-1) - assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) < ApplicationState.RUNNING_IN_FOREGROUND - self.driver.activate_app(desired_capabilities.BUNDLE_ID) - assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND diff --git a/test/functional/ios/execute_driver_tests.py b/test/functional/ios/execute_driver_tests.py deleted file mode 100644 index 8c3a64e8..00000000 --- a/test/functional/ios/execute_driver_tests.py +++ /dev/null @@ -1,43 +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 textwrap - -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestExecuteDriver(BaseTestCase): - def test_batch(self) -> None: - script = """ - const status = await driver.status(); - console.warn('warning message'); - return status; - """ - - response = self.driver.execute_driver(script=textwrap.dedent(script)) - assert(response.result['build']) - assert(response.logs['warn'] == ['warning message']) - - def test_batch_combination_python_script(self) -> None: - script = """ - console.warn('warning message'); - const element = await driver.findElement('accessibility id', 'Buttons'); - const rect = await driver.getElementRect(element.ELEMENT); - return [element, rect]; - """ - - response = self.driver.execute_driver(script=textwrap.dedent(script)) - r = response.result[0].rect - - assert(r == response.result[1]) diff --git a/test/functional/ios/file/test_file.txt b/test/functional/ios/file/test_file.txt deleted file mode 100644 index aca4a3b3..00000000 --- a/test/functional/ios/file/test_file.txt +++ /dev/null @@ -1 +0,0 @@ -"We have to stop optimizing for programmers and start optimizing for users." - Jeff Atwood \ No newline at end of file diff --git a/test/functional/ios/file/test_image.jpg b/test/functional/ios/file/test_image.jpg deleted file mode 100644 index 6434234f..00000000 Binary files a/test/functional/ios/file/test_image.jpg and /dev/null differ diff --git a/test/functional/ios/helper/desired_capabilities.py b/test/functional/ios/helper/desired_capabilities.py deleted file mode 100644 index 032df84a..00000000 --- a/test/functional/ios/helper/desired_capabilities.py +++ /dev/null @@ -1,78 +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 os -from typing import Any, Dict, Optional - -# Returns abs path relative to this file and not cwd - - -def PATH(p: str) -> str: return os.path.abspath( - os.path.join(os.path.dirname(__file__), p) -) - - -BUNDLE_ID = 'com.example.apple-samplecode.UICatalog' - - -def get_desired_capabilities(app: Optional[str] = None) -> Dict[str, Any]: - desired_caps: Dict[str, Any] = { - 'deviceName': iphone_device_name(), - 'platformName': 'iOS', - 'platformVersion': '13.6', - 'automationName': 'XCUITest', - 'allowTouchIdEnroll': True, - 'wdaLocalPort': wda_port(), - 'simpleIsVisibleCheck': True - } - - if app is not None: - desired_caps['app'] = PATH(os.path.join('../../..', 'apps', app)) - - return desired_caps - - -class PytestXdistWorker: - NUMBER: Optional[str] = os.getenv('PYTEST_XDIST_WORKER') - COUNT: Optional[str] = os.getenv('PYTEST_XDIST_WORKER_COUNT') # Return 2 if `-n 2` is passed - - @staticmethod - def gw(number: int) -> str: - if PytestXdistWorker.COUNT is None: - return '0' - - if number >= int(PytestXdistWorker.COUNT): - return 'gw0' - - return f'gw{number}' - -# If you run tests with pytest-xdist, you can run tests in parallel. - - -def wda_port() -> int: - if PytestXdistWorker.NUMBER == PytestXdistWorker.gw(1): - return 8101 - - return 8100 - - -# Before running tests, you must have iOS simulators named 'iPhone 8 - 8100' and 'iPhone 8 - 8101' - - -def iphone_device_name() -> str: - if PytestXdistWorker.NUMBER == PytestXdistWorker.gw(0): - return 'iPhone 8 - 8100' - elif PytestXdistWorker.NUMBER == PytestXdistWorker.gw(1): - return 'iPhone 8 - 8101' - - return 'iPhone 8' 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/hw_actions_tests.py b/test/functional/ios/hw_actions_tests.py deleted file mode 100644 index 86264b37..00000000 --- a/test/functional/ios/hw_actions_tests.py +++ /dev/null @@ -1,38 +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. - -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestHwActions(BaseTestCase): - def test_lock(self) -> None: - self.driver.lock(-1) - try: - assert self.driver.is_locked() - finally: - self.driver.unlock() - assert not self.driver.is_locked() - - def test_shake(self) -> None: - # TODO what can we assert about this? - self.driver.shake() - - def test_touch_id(self) -> None: - # nothing to assert, just verify that it doesn't blow up - self.driver.touch_id(True) - self.driver.touch_id(False) - - def test_toggle_touch_id_enrollment(self) -> None: - # nothing to assert, just verify that it doesn't blow up - self.driver.toggle_touch_id_enrollment() diff --git a/test/functional/ios/keyboard_tests.py b/test/functional/ios/keyboard_tests.py deleted file mode 100644 index 0ee303e0..00000000 --- a/test/functional/ios/keyboard_tests.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. - -from time import sleep - -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestKeyboard(BaseTestCase): - def test_hide_keyboard(self) -> None: - self._move_to_textbox() - - el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0] - el.click() - el.set_value('Testing') - - el = self.driver.find_element_by_class_name('UIAKeyboard') - assert el.is_displayed() - - self.driver.hide_keyboard(key_name='Done') - - assert not el.is_displayed() - - def test_hide_keyboard_presskey_strategy(self) -> None: - self._move_to_textbox() - - el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0] - el.click() - el.set_value('Testing') - - el = self.driver.find_element_by_class_name('UIAKeyboard') - assert el.is_displayed() - - self.driver.hide_keyboard(strategy='pressKey', key='Done') - - assert not el.is_displayed() - - def test_hide_keyboard_no_key_name(self) -> None: - self._move_to_textbox() - - el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0] - el.click() - el.set_value('Testing') - - el = self.driver.find_element_by_class_name('UIAKeyboard') - assert el.is_displayed() - - self.driver.hide_keyboard() - sleep(10) - - # currently fails. - assert not el.is_displayed() - - def test_is_keyboard_shown(self) -> None: - self._move_to_textbox() - - el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0] - el.click() - el.set_value('Testing') - assert self.driver.is_keyboard_shown() - - def _move_to_textbox(self) -> None: - el1 = self.driver.find_element_by_accessibility_id('Sliders') - el2 = self.driver.find_element_by_accessibility_id('Buttons') - self.driver.scroll(el1, el2) - - # Click text fields - self.driver.find_element_by_accessibility_id('Text Fields').click() diff --git a/test/functional/ios/safari_tests.py b/test/functional/ios/safari_tests.py index 23d907fc..624979f8 100644 --- a/test/functional/ios/safari_tests.py +++ b/test/functional/ios/safari_tests.py @@ -12,30 +12,68 @@ # 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 -from .helper.desired_capabilities import get_desired_capabilities + driver.quit() -class TestSafari(object): - def setup_method(self) -> None: - desired_caps = get_desired_capabilities() - desired_caps.update({ - 'browserName': 'safari', - 'nativeWebTap': True, - 'safariIgnoreFraudWarning': True - }) +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 - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - def teardown_method(self) -> None: - self.driver.quit() +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') - def test_context(self) -> None: - assert 'NATIVE_APP' == self.driver.contexts[0] - assert self.driver.contexts[1].startswith('WEBVIEW_') - assert 'WEBVIEW_' in self.driver.current_context + driver.get('http://google.com') + for _ in range(5): + time.sleep(0.5) + if 'Google' == driver.title: + return - def test_get(self) -> None: - self.driver.get("http://google.com") - assert 'Google' == self.driver.title + pytest.fail('The title was wrong') diff --git a/test/functional/ios/screen_record_tests.py b/test/functional/ios/screen_record_tests.py deleted file mode 100644 index b821f6d3..00000000 --- a/test/functional/ios/screen_record_tests.py +++ /dev/null @@ -1,25 +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. - -from time import sleep - -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestScreenRecord(BaseTestCase): - def test_screen_record(self) -> None: - self.driver.start_recording_screen() - sleep(10) - result = self.driver.stop_recording_screen() - assert len(result) > 0 diff --git a/test/functional/ios/search_context/find_by_element_webelement_tests.py b/test/functional/ios/search_context/find_by_element_webelement_tests.py deleted file mode 100644 index 96d8ed91..00000000 --- a/test/functional/ios/search_context/find_by_element_webelement_tests.py +++ /dev/null @@ -1,31 +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. - -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestFindByElementWebelement(BaseTestCase): - - def test_find_element_by_path(self) -> None: - el = self.driver.find_element_by_ios_predicate('wdName == "UICatalog"') - assert 'UICatalog' == el.get_attribute('name') - - c_el = el.find_elements_by_ios_predicate('label == "Action Sheets"') - assert 'Action Sheets' == c_el[0].get_attribute('name') - - c_el = el.find_elements_by_ios_class_chain('**/XCUIElementTypeStaticText') - assert 'UICatalog' == c_el[0].get_attribute('name') - - c_el = el.find_elements_by_accessibility_id('UICatalog') - assert 'UICatalog' == c_el[0].get_attribute('name') diff --git a/test/functional/ios/search_context/find_by_ios_class_chain_tests.py b/test/functional/ios/search_context/find_by_ios_class_chain_tests.py deleted file mode 100644 index 7ff0a24d..00000000 --- a/test/functional/ios/search_context/find_by_ios_class_chain_tests.py +++ /dev/null @@ -1,28 +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. - -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestFindByIOClassChain(BaseTestCase): - def test_find_element_by_path(self) -> None: - els = self.driver.find_elements_by_ios_class_chain('XCUIElementTypeWindow/**/XCUIElementTypeStaticText') - assert 35 == len(els) - assert 'UICatalog' == els[0].get_attribute('name') - - def test_find_multiple_elements_by_path(self) -> None: - el = self.driver.find_elements_by_ios_class_chain('XCUIElementTypeWindow/*/*/*') - assert 2 == len(el) - assert 'UICatalog' == el[0].get_attribute('name') - assert el[1].get_attribute('name') is None diff --git a/test/functional/ios/search_context/find_by_ios_predicate_tests.py b/test/functional/ios/search_context/find_by_ios_predicate_tests.py deleted file mode 100644 index 41908479..00000000 --- a/test/functional/ios/search_context/find_by_ios_predicate_tests.py +++ /dev/null @@ -1,49 +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. - -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestFindByIOSPredicate(BaseTestCase): - def test_find_element_by_name(self) -> None: - # Will throw exception if element is not found - self.driver.find_element_by_ios_predicate('wdName == "Buttons"') - - def test_find_multiple_element_by_type(self) -> None: - e = self.driver.find_elements_by_ios_predicate('wdType == "XCUIElementTypeStaticText"') - assert len(e) != 0 - - def test_find_element_by_label(self) -> None: - # Will throw exception if element is not found - self.driver.find_element_by_ios_predicate('label == "Buttons"') - - def test_find_element_by_value(self) -> None: - # Will throw exception if element is not found - self.driver.find_element_by_ios_predicate('wdValue == "Buttons"') - - def test_find_element_by_isvisible(self) -> None: - # Will throw exception if element is not found - self.driver.find_element_by_ios_predicate('wdValue == "Buttons" AND visible == 1') - - # Should not find any elements - e = self.driver.find_elements_by_ios_predicate('wdValue == "Buttons" AND visible == 0') - assert len(e) == 0 - - def test_find_element_by_isenabled(self) -> None: - # Will throw exception if element is not found - self.driver.find_element_by_ios_predicate('wdValue == "Buttons" AND enabled == 1') - - # Should not find any elements - e = self.driver.find_elements_by_ios_predicate('wdValue == "Buttons" AND enabled == 0') - assert len(e) == 0 diff --git a/test/functional/ios/webdriver_tests.py b/test/functional/ios/webdriver_tests.py deleted file mode 100644 index 5f3cecbf..00000000 --- a/test/functional/ios/webdriver_tests.py +++ /dev/null @@ -1,108 +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. - -from typing import TYPE_CHECKING - -import pytest -from selenium.webdriver.support.ui import WebDriverWait - -from appium import webdriver -from appium.webdriver.applicationstate import ApplicationState -from test.functional.ios.helper.test_helper import BaseTestCase -from test.functional.test_helper import get_available_from_port_range - -from ..test_helper import is_ci -from .helper import desired_capabilities - -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - - -class TestWebDriver(BaseTestCase): - - # TODO Due to not created 2nd session somehow - @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI.') - def test_all_sessions(self) -> None: - port = get_available_from_port_range(8200, 8300) - desired_caps = desired_capabilities.get_desired_capabilities('UICatalog.app.zip') - desired_caps['deviceName'] = 'iPhone Xs Max' - desired_caps['wdaLocalPort'] = port - - class session_counts_is_two: - TIMEOUT = 10 - - def __call__(self, driver: 'WebDriver') -> bool: - return len(driver.all_sessions) == 2 - - driver2 = None - try: - driver2 = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - WebDriverWait( - driver2, session_counts_is_two.TIMEOUT).until(session_counts_is_two()) - assert len(self.driver.all_sessions) == 2 - finally: - if driver2 is not None: - driver2.quit() - - def test_app_management(self) -> None: - # this only works in Xcode9+ - if float(desired_capabilities.get_desired_capabilities( - desired_capabilities.BUNDLE_ID)['platformVersion']) < 11: - return - assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND - self.driver.background_app(-1) - assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) < ApplicationState.RUNNING_IN_FOREGROUND - self.driver.activate_app(desired_capabilities.BUNDLE_ID) - assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND - - def test_clear(self) -> None: - self._move_to_textbox() - - el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0] - - # Verify default text - def_text = 'Placeholder text' - text = el.get_attribute('value') - assert text == def_text - - # Input some text, verify - input_text = 'blah' - el.click() - el.send_keys(input_text) - self.driver.hide_keyboard() - - # TODO Needs to get the element again to update value in the element. Remove below one line when it's fixed. - el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0] - text = el.get_attribute('value') - assert text == input_text - - # Clear text, verify - el.clear() - text = el.get_attribute('value') - assert text == def_text - - def test_press_button(self) -> None: - self.driver.press_button("Home") - if float(desired_capabilities.get_desired_capabilities( - desired_capabilities.BUNDLE_ID)['platformVersion']) < 11: - return - assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND - - def _move_to_textbox(self) -> None: - el1 = self.driver.find_element_by_accessibility_id('Sliders') - el2 = self.driver.find_element_by_accessibility_id('Buttons') - self.driver.scroll(el1, el2) - - # Click text fields - self.driver.find_element_by_accessibility_id('Text Fields').click() 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/android/finger_print_tests.py b/test/functional/mac/helper/desired_capabilities.py similarity index 71% rename from test/functional/android/finger_print_tests.py rename to test/functional/mac/helper/desired_capabilities.py index 9b8871d9..648420ef 100644 --- a/test/functional/android/finger_print_tests.py +++ b/test/functional/mac/helper/desired_capabilities.py @@ -1,5 +1,4 @@ #!/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. @@ -13,10 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .helper.test_helper import BaseTestCase +from typing import Any, Dict -class TestFingerPrint(BaseTestCase): - def test_finger_print(self) -> None: - result = self.driver.finger_print(1) - assert result is None +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/ios/helper/test_helper.py b/test/functional/mac/helper/test_helper.py similarity index 55% rename from test/functional/ios/helper/test_helper.py rename to test/functional/mac/helper/test_helper.py index f78912f6..0b7463da 100644 --- a/test/functional/ios/helper/test_helper.py +++ b/test/functional/mac/helper/test_helper.py @@ -12,28 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. - -import base64 -import os - from appium import webdriver -from test.functional.test_helper import is_ci +from appium.options.mac import Mac2Options +from appium.webdriver.client_config import AppiumClientConfig +from test.helpers.constants import SERVER_URL_BASE -from . import desired_capabilities +from .desired_capabilities import get_desired_capabilities class BaseTestCase(object): - def setup_method(self) -> None: - desired_caps = desired_capabilities.get_desired_capabilities('UICatalog.app.zip') - self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) - if is_ci(): - self.driver.start_recording_screen() + 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 is_ci(): - payload = self.driver.stop_recording_screen() - video_path = os.path.join(os.getcwd(), method.__name__ + '.mp4') - with open(video_path, "wb") as fd: - fd.write(base64.b64decode(payload)) + if not hasattr(self, 'driver'): + return + self.driver.quit() diff --git a/test/functional/android/hw_actions_tests.py b/test/functional/mac/webelement_test.py similarity index 52% rename from test/functional/android/hw_actions_tests.py rename to test/functional/mac/webelement_test.py index 4646d254..f2fc4687 100644 --- a/test/functional/android/hw_actions_tests.py +++ b/test/functional/mac/webelement_test.py @@ -1,5 +1,4 @@ #!/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. @@ -13,17 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from time import sleep +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 -from .helper.test_helper import BaseTestCase - -class TestHwActions(BaseTestCase): - def test_lock(self) -> None: - self.driver.lock(-1) - sleep(10) - try: - assert self.driver.is_locked() - finally: - self.driver.unlock() - assert not self.driver.is_locked() +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 index 71315105..90500ea0 100644 --- a/test/functional/test_helper.py +++ b/test/functional/test_helper.py @@ -1,5 +1,16 @@ 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): @@ -37,3 +48,95 @@ def is_ci() -> bool: `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/unit/helper/test_helper.py b/test/unit/helper/test_helper.py index b33d3a92..58e35817 100644 --- a/test/unit/helper/test_helper.py +++ b/test/unit/helper/test_helper.py @@ -18,14 +18,15 @@ import httpretty from appium import webdriver - -# :return: A string of test URL -SERVER_URL_BASE = 'http://localhost:4723/wd/hub' +from appium.options.android import UiAutomator2Options +from appium.options.ios import XCUITestOptions +from test.helpers.constants import SERVER_URL_BASE if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver from httpretty.core import HTTPrettyRequestEmpty + from appium.webdriver.webdriver import WebDriver + def appium_command(command: str) -> str: """Return a command of Appium @@ -33,7 +34,11 @@ def appium_command(command: str) -> str: Returns: str: A string of command URL """ - return f'{SERVER_URL_BASE}{command}' + 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': @@ -45,49 +50,40 @@ def android_w3c_driver() -> 'WebDriver': response_body_json = json.dumps( { - 'value': { - 'sessionId': '1234567890', - 'capabilities': { - 'platform': 'LINUX', - 'desired': { - 'platformName': 'Android', - 'automationName': 'uiautomator2', - 'platformVersion': '7.1.1', - 'deviceName': 'Android Emulator', - 'app': '/test/apps/ApiDemos-debug.apk', - }, + 'sessionId': '1234567890', + 'capabilities': { + 'platform': 'LINUX', + 'desired': { 'platformName': 'Android', 'automationName': 'uiautomator2', 'platformVersion': '7.1.1', - 'deviceName': 'emulator-5554', + 'deviceName': 'Android Emulator', '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' - } - } + }, + '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 - ) + 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' + 'automationName': 'UIAutomator2', } - driver = webdriver.Remote( - SERVER_URL_BASE, - desired_caps - ) + driver = webdriver.Remote(SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps)) return driver @@ -97,38 +93,109 @@ def ios_w3c_driver() -> 'WebDriver': Returns: `webdriver.webdriver.WebDriver`: An instance of WebDriver """ - response_body_json = json.dumps( { - 'value': { - 'sessionId': '1234567890', - 'capabilities': { - 'device': 'iphone', - 'browserName': 'UICatalog', - 'sdkVersion': '11.4', - 'CFBundleIdentifier': 'com.example.apple-samplecode.UICatalog' - } - } + '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 + 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' + 'automationName': 'XCUITest', } - driver = webdriver.Remote( - SERVER_URL_BASE, - desired_caps + 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 diff --git a/test/unit/webdriver/app_test.py b/test/unit/webdriver/app_test.py index ad052226..34767535 100644 --- a/test/unit/webdriver/app_test.py +++ b/test/unit/webdriver/app_test.py @@ -11,137 +11,154 @@ # WITHOUT 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 -) - - -class TestWebDriverApp(object): - - @httpretty.activate - def test_reset(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/app/reset'), - body='{"value": ""}' - ) - result = driver.reset() - - assert {'sessionId': '1234567890'}, get_httpretty_request_body(httpretty.last_request()) - assert isinstance(result, WebDriver) - - @httpretty.activate - def test_install_app(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/device/install_app'), - body='{"value": ""}' - ) - result = driver.install_app('path/to/app') - - assert {'app': 'path/to/app'}, get_httpretty_request_body(httpretty.last_request()) - assert isinstance(result, WebDriver) - - @httpretty.activate - def test_remove_app(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/device/remove_app'), - body='{"value": ""}' - ) - result = driver.remove_app('com.app.id') - - assert {'app': 'com.app.id'}, get_httpretty_request_body(httpretty.last_request()) - assert isinstance(result, WebDriver) - - @httpretty.activate - def test_app_installed(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/device/app_installed'), - body='{"value": true}' - ) - result = driver.is_app_installed("com.app.id") - assert {'app': "com.app.id"}, get_httpretty_request_body(httpretty.last_request()) - assert result is True - - @httpretty.activate - def test_terminate_app(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/device/terminate_app'), - body='{"value": true}' - ) - result = driver.terminate_app("com.app.id") - assert {'app': "com.app.id"}, get_httpretty_request_body(httpretty.last_request()) - assert result is True - - @httpretty.activate - def test_activate_app(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/device/activate_app'), - body='{"value": ""}' - ) - result = driver.activate_app("com.app.id") - - assert {'app': 'com.app.id'}, get_httpretty_request_body(httpretty.last_request()) - assert isinstance(result, WebDriver) - - @httpretty.activate - def test_background_app(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/app/background'), - body='{"value": ""}' - ) - result = driver.background_app(0) - assert {'app': 0}, get_httpretty_request_body(httpretty.last_request()) - assert isinstance(result, WebDriver) - - @httpretty.activate - def test_launch_app(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/app/launch'), - body='{"value": }' - ) - assert isinstance(driver.launch_app(), WebDriver) - - @httpretty.activate - def test_close_app(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/app/close'), - body='{"value": }' - ) - assert isinstance(driver.close_app(), WebDriver) - - @httpretty.activate - def test_query_app_state(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/device/app_state'), - body='{"value": 3 }' - ) - result = driver.query_app_state('com.app.id') - - assert {'app': 3}, get_httpretty_request_body(httpretty.last_request()) - assert result is ApplicationState.RUNNING_IN_BACKGROUND +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/context_test.py b/test/unit/webdriver/context_test.py index 150fddac..80c2c0f7 100644 --- a/test/unit/webdriver/context_test.py +++ b/test/unit/webdriver/context_test.py @@ -14,17 +14,39 @@ import httpretty -from test.unit.helper.test_helper import android_w3c_driver, appium_command +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/context'), - body='{"value": "NATIVE"}' + httpretty.GET, appium_command('/session/1234567890/contexts'), body='{"value": ["NATIVE_APP", "CHROMIUM"]}' ) - assert driver.current_context == 'NATIVE' + + 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 index 01c0210e..2374ba6c 100644 --- a/test/unit/webdriver/device/activities_test.py +++ b/test/unit/webdriver/device/activities_test.py @@ -14,65 +14,22 @@ import httpretty -from test.unit.helper.test_helper import ( - android_w3c_driver, - appium_command, - get_httpretty_request_body -) +from test.unit.helper.test_helper import android_w3c_driver, appium_command class TestWebDriverActivities(object): - - @httpretty.activate - def test_start_activity(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/device/start_activity'), - body='{"value": ""}' - ) - driver.start_activity('com.example.myapp', '.ExampleActivity') - - d = get_httpretty_request_body(httpretty.last_request()) - assert d['appPackage'] == 'com.example.myapp' - assert d['appActivity'] == '.ExampleActivity' - - @httpretty.activate - def test_start_activity_with_opts(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/device/start_activity'), - body='{"value": ""}' - ) - driver.start_activity( - app_package='com.example.myapp', - app_activity='.ExampleActivity', - app_wait_package='com.example.waitapp', - intent_action='android.intent.action.MAIN', - intent_category='android.intent.category.LAUNCHER', - intent_flags='0x10200000', - optional_intent_arguments='--es "activity" ".ExampleActivity"', - dont_stop_app_on_reset=True - ) - - d = get_httpretty_request_body(httpretty.last_request()) - assert d['appPackage'] == 'com.example.myapp' - assert d['appActivity'] == '.ExampleActivity' - assert d['appWaitPackage'] == 'com.example.waitapp' - assert d['intentAction'] == 'android.intent.action.MAIN' - assert d['intentCategory'] == 'android.intent.category.LAUNCHER' - assert d['intentFlags'] == '0x10200000' - assert d['optionalIntentArguments'] == '--es "activity" ".ExampleActivity"' - assert d['dontStopAppOnReset'] is True - @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"}' + body='{"value": ".ExampleActivity"}', + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": ".ExampleActivity"}', ) assert driver.current_activity == '.ExampleActivity' @@ -82,6 +39,11 @@ def test_wait_activity(self): httpretty.register_uri( httpretty.GET, appium_command('/session/1234567890/appium/device/current_activity'), - body='{"value": ".ExampleActivity"}' + 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 index 01176029..7e21797a 100644 --- a/test/unit/webdriver/device/clipboard_test.py +++ b/test/unit/webdriver/device/clipboard_test.py @@ -15,42 +15,41 @@ 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 -) +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/appium/device/set_clipboard'), - body='{"value": ""}' + appium_command('/session/1234567890/execute/sync'), + body='{"value": ""}', ) - driver.set_clipboard(bytes(str('http://appium.io/'), 'UTF-8'), - ClipboardContentType.URL, 'label for android') + 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['content'] == 'aHR0cDovL2FwcGl1bS5pby8=' - assert d['contentType'] == 'url' - assert d['label'] == 'label for android' + 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/appium/device/set_clipboard'), - body='{"value": ""}' + appium_command('/session/1234567890/execute/sync'), + body='{"value": ""}', ) driver.set_clipboard_text('hello') d = get_httpretty_request_body(httpretty.last_request()) - assert d['content'] == 'aGVsbG8=' - assert d['contentType'] == 'plaintext' + 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 index 395979b9..43b069a0 100644 --- a/test/unit/webdriver/device/common_test.py +++ b/test/unit/webdriver/device/common_test.py @@ -15,19 +15,16 @@ import httpretty from appium.webdriver.webdriver import WebDriver -from test.unit.helper.test_helper import android_w3c_driver, appium_command +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/appium/device/open_notifications') - ) + 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): @@ -35,6 +32,11 @@ def test_current_package(self): httpretty.register_uri( httpretty.GET, appium_command('/session/1234567890/appium/device/current_package'), - body='{"value": ".ExamplePackage"}' + 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 index 95c9f6a5..8c6f5e0b 100644 --- a/test/unit/webdriver/device/device_time_test.py +++ b/test/unit/webdriver/device/device_time_test.py @@ -14,22 +14,22 @@ import httpretty -from test.unit.helper.test_helper import ( - android_w3c_driver, - appium_command, - get_httpretty_request_body -) +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"}' + 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' @@ -39,7 +39,12 @@ def test_get_device_time(self): httpretty.register_uri( httpretty.GET, appium_command('/session/1234567890/appium/device/system_time'), - body='{"value": "2019-01-05T14:46:44+09:00"}' + 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' @@ -49,9 +54,14 @@ def test_get_formatted_device_time(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/device/system_time'), - body='{"value": "2019-01-08"}' + 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['format'] == 'YYYY-MM-DD' + 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 index cd78b044..bce0b4e4 100644 --- a/test/unit/webdriver/device/display_test.py +++ b/test/unit/webdriver/device/display_test.py @@ -18,13 +18,11 @@ 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.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 index cd553003..e6facf86 100644 --- a/test/unit/webdriver/device/fingerprint_test.py +++ b/test/unit/webdriver/device/fingerprint_test.py @@ -14,15 +14,11 @@ import httpretty -from test.unit.helper.test_helper import ( - android_w3c_driver, - appium_command, - get_httpretty_request_body -) +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() @@ -31,8 +27,13 @@ def test_finger_print(self): 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 driver.finger_print(1) is None + assert isinstance(driver.finger_print(1), WebDriver) d = get_httpretty_request_body(httpretty.last_request()) - assert d['fingerprintId'] == 1 + 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 index f8e44df3..cd776a94 100644 --- a/test/unit/webdriver/device/gsm_test.py +++ b/test/unit/webdriver/device/gsm_test.py @@ -14,21 +14,12 @@ import httpretty -from appium.webdriver.extensions.android.gsm import ( - GsmCallActions, - GsmSignalStrength, - GsmVoiceState -) +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 -) +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() @@ -36,11 +27,15 @@ def test_make_gsm_call(self): 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['phoneNumber'] == '5551234567' - assert d['action'] == GsmCallActions.CALL + assert d['args'][0]['phoneNumber'] == '5551234567' + assert d['args'][0]['action'] == GsmCallActions.CALL @httpretty.activate def test_set_gsm_signal(self): @@ -49,11 +44,14 @@ def test_set_gsm_signal(self): 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['signalStrength'] == GsmSignalStrength.GREAT - assert d['signalStrengh'] == GsmSignalStrength.GREAT + assert d['args'][0]['strength'] == GsmSignalStrength.GREAT @httpretty.activate def test_set_gsm_voice(self): @@ -62,7 +60,11 @@ def test_set_gsm_voice(self): 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['state'] == GsmVoiceState.ROAMING + 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 index faf9743a..526f1826 100644 --- a/test/unit/webdriver/device/keyboard_test.py +++ b/test/unit/webdriver/device/keyboard_test.py @@ -15,35 +15,27 @@ import httpretty from appium.webdriver.webdriver import WebDriver -from test.unit.helper.test_helper import ( - android_w3c_driver, - appium_command, - get_httpretty_request_body -) +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver -class TestWebDriverKeyboard(object): - +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/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.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['keycode'] == 86 + assert d.get('keycode', d['args'][0]['keycode']) == 86 @httpretty.activate def test_long_press_keycode(self): @@ -51,20 +43,20 @@ def test_long_press_keycode(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/device/long_press_keycode'), - body='{"value": "86"}' + 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['keycode'] == 86 + 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.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 @@ -73,15 +65,15 @@ def test_press_keycode_with_flags(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/device/press_keycode'), - body='{keycode: 86, metastate: 2097153, flags: 44}' + 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) + driver.press_keycode(86, metastate=0x00000001 | 0x00200000, flags=0x20 | 0x00000004 | 0x00000008), + WebDriver, + ) @httpretty.activate def test_long_press_keycode_with_flags(self): @@ -89,12 +81,56 @@ def test_long_press_keycode_with_flags(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/device/long_press_keycode'), - body='{keycode: 86, metastate: 2097153, flags: 44}' + 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) + 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 index 8e25e4a1..fb0112bc 100644 --- a/test/unit/webdriver/device/location_test.py +++ b/test/unit/webdriver/device/location_test.py @@ -15,75 +15,76 @@ import httpretty from appium.webdriver.webdriver import WebDriver -from test.unit.helper.test_helper import ( - android_w3c_driver, - appium_command, - get_httpretty_request_body -) +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/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), WebDriver) + 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'), WebDriver) + 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), WebDriver) + 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}}' + body='{"value": {"latitude": 11.1, "longitude": 22.2, "altitude": 33.3}}', ) val = driver.location assert abs(val['latitude'] - 11.1) <= FLT_EPSILON diff --git a/test/unit/webdriver/device/lock_test.py b/test/unit/webdriver/device/lock_test.py index 31ec5d2c..b5469854 100644 --- a/test/unit/webdriver/device/lock_test.py +++ b/test/unit/webdriver/device/lock_test.py @@ -15,67 +15,117 @@ import httpretty from appium.webdriver.webdriver import WebDriver -from test.unit.helper.test_helper import ( - android_w3c_driver, - appium_command, - get_httpretty_request_body -) +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver -class TestWebDriverLock(object): - +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/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['seconds'] == 1 + 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/appium/device/lock'), body='{"value": ""}') + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') driver.lock() - d = get_httpretty_request_body(httpretty.last_request()) - assert d == {} - @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.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/is_locked'), - body='{"value": true}' + 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 = android_w3c_driver() + 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 index e9b9e67b..2d87a8d4 100644 --- a/test/unit/webdriver/device/power_test.py +++ b/test/unit/webdriver/device/power_test.py @@ -16,15 +16,10 @@ 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 -) +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() @@ -32,10 +27,14 @@ def test_set_power_capacity(self): 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['percent'] == 50 + assert d['args'][0]['percent'] == 50 @httpretty.activate def test_set_power_ac(self): @@ -44,7 +43,11 @@ def test_set_power_ac(self): 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['state'] == Power.AC_ON + 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 index dace0ffc..4de3a85f 100644 --- a/test/unit/webdriver/device/remote_fs_test.py +++ b/test/unit/webdriver/device/remote_fs_test.py @@ -19,15 +19,10 @@ 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 -) +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() @@ -35,14 +30,18 @@ def test_push_file(self): 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['path'] == dest_path - assert d['data'] == str(data) + 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): @@ -51,6 +50,10 @@ def test_push_file_invalid_arg_exception_without_src_path_and_base64data(self): 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): @@ -63,6 +66,10 @@ def test_push_file_invalid_arg_exception_with_src_file_not_found(self): 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' @@ -75,14 +82,19 @@ def test_pull_file(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/device/pull_file'), - body='{"value": "SGVsbG9Xb3JsZA=="}' + 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['path'] == dest_path + assert d.get('path', d['args'][0]['remotePath']) == dest_path @httpretty.activate def test_pull_folder(self): @@ -90,11 +102,16 @@ def test_pull_folder(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/device/pull_folder'), - body='{"value": "base64EncodedZippedFolderData"}' + 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['path'] == dest_path + 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 index 7604bec2..0371852f 100644 --- a/test/unit/webdriver/device/shake_test.py +++ b/test/unit/webdriver/device/shake_test.py @@ -20,7 +20,6 @@ class TestWebDriverShake(object): - @httpretty.activate def test_shake(self): driver = android_w3c_driver() @@ -28,4 +27,8 @@ def test_shake(self): 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 index 48ab371c..1323f50c 100644 --- a/test/unit/webdriver/device/sms_test.py +++ b/test/unit/webdriver/device/sms_test.py @@ -15,15 +15,10 @@ import httpretty from appium.webdriver.webdriver import WebDriver -from test.unit.helper.test_helper import ( - android_w3c_driver, - appium_command, - get_httpretty_request_body -) +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() @@ -31,8 +26,12 @@ def test_send_sms(self): 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['phoneNumber'] == '555-123-4567' - assert d['message'] == 'Hey lol' + 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 index 0373cba9..79e7c680 100644 --- a/test/unit/webdriver/device/system_bars_test.py +++ b/test/unit/webdriver/device/system_bars_test.py @@ -18,18 +18,23 @@ 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=''' {"value": - {"statusBar": - {"visible": true, "x": 0, "y": 0, "width": 1080, "height": 1920}, - "navigationBar": - {"visible": true, "x": 0, "y": 0, "width": 1080, "height": 126}}}''' + body=body, + ) + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body=body, ) d = driver.get_system_bars() diff --git a/test/unit/webdriver/execute_driver_test.py b/test/unit/webdriver/execute_driver_test.py index 3f438c52..250b8cc8 100644 --- a/test/unit/webdriver/execute_driver_test.py +++ b/test/unit/webdriver/execute_driver_test.py @@ -16,15 +16,10 @@ import httpretty -from test.unit.helper.test_helper import ( - android_w3c_driver, - appium_command, - get_httpretty_request_body -) +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() @@ -35,7 +30,7 @@ def test_batch(self): '{"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":[]}}}' + '"error":[],"warn":["warning message"],"log":[]}}}', ) script = """ @@ -65,7 +60,7 @@ def test_batch_with_timeout(self): '{"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":[]}}}' + '"error":[],"warn":["warning message"],"log":[]}}}', ) script = """ 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/ime_test.py b/test/unit/webdriver/ime_test.py deleted file mode 100644 index a20aa685..00000000 --- a/test/unit/webdriver/ime_test.py +++ /dev/null @@ -1,83 +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 json - -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 TestWebDriverIme(object): - - @httpretty.activate - def test_available_ime_engines(self): - ANDROID_LATIN = 'com.android.inputmethod.latin/.LatinIME' - GOOGLE_LATIN = 'com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME' - - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.GET, - appium_command('/session/1234567890/ime/available_engines'), - body=json.dumps({'value': [ANDROID_LATIN, GOOGLE_LATIN]}) - ) - assert driver.available_ime_engines == [ANDROID_LATIN, GOOGLE_LATIN] - - @httpretty.activate - def test_is_ime_active(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.GET, - appium_command('/session/1234567890/ime/activated'), - body=json.dumps({'value': True}) - ) - assert driver.is_ime_active() is True - - @httpretty.activate - def test_activate_ime_engine(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/ime/activate'), - ) - engine = 'com.android.inputmethod.latin/.LatinIME' - assert isinstance(driver.activate_ime_engine(engine), WebDriver) - - d = get_httpretty_request_body(httpretty.last_request()) - assert d['engine'] == 'com.android.inputmethod.latin/.LatinIME' - - @httpretty.activate - def test_deactivate_ime_engine(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/ime/deactivate'), - ) - assert isinstance(driver.deactivate_ime_engine(), WebDriver) - - @httpretty.activate - def test_active_ime_engine(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.GET, - appium_command('/session/1234567890/ime/active_engine'), - body=json.dumps({'value': 'com.android.inputmethod.latin/.LatinIME'}) - ) - assert driver.active_ime_engine == 'com.android.inputmethod.latin/.LatinIME' diff --git a/test/unit/webdriver/log_events_test.py b/test/unit/webdriver/log_events_test.py index be5a514f..14d41442 100644 --- a/test/unit/webdriver/log_events_test.py +++ b/test/unit/webdriver/log_events_test.py @@ -17,22 +17,17 @@ import httpretty from appium.webdriver.webdriver import WebDriver -from test.unit.helper.test_helper import ( - appium_command, - get_httpretty_request_body, - ios_w3c_driver -) +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]}}) + body=json.dumps({'value': {'appium:funEvent': [12347]}}), ) events = driver.get_events() assert events['appium:funEvent'] == [12347] @@ -46,7 +41,7 @@ def test_get_events_args(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/events'), - body=json.dumps({'value': {'appium:funEvent': [12347]}}) + body=json.dumps({'value': {'appium:funEvent': [12347]}}), ) events_to_filter = ['appium:funEvent'] events = driver.get_events(events_to_filter) @@ -58,11 +53,7 @@ def test_get_events_args(self): @httpretty.activate def test_log_event(self): driver = ios_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/log_event'), - body="" - ) + 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) 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/multi_action_test.py b/test/unit/webdriver/multi_action_test.py deleted file mode 100644 index 90b404aa..00000000 --- a/test/unit/webdriver/multi_action_test.py +++ /dev/null @@ -1,58 +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 pytest - -from appium.webdriver.common.multi_action import MultiAction -from appium.webdriver.common.touch_action import TouchAction - - -class TestMultiAction(object): - @pytest.fixture - def multi_action(self): - return MultiAction(DriverStub()) - - def test_json(self, multi_action): - json = { - 'actions': [ - [ - {'action': 'press', 'options': {'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': {}} - ] - ] - } - 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() - multi_action.add(t1, t2) - assert json == multi_action.json_wire_gestures - - -class DriverStub(object): - def execute(self, _action, _params): - print("driver.execute called") - - -class ElementStub(object): - def __init__(self, e_id): - self._id = e_id - - @property - def id(self): - return self._id diff --git a/test/unit/webdriver/nativekey_test.py b/test/unit/webdriver/nativekey_test.py index 5841a9a8..733f6159 100644 --- a/test/unit/webdriver/nativekey_test.py +++ b/test/unit/webdriver/nativekey_test.py @@ -17,7 +17,6 @@ class TestAndroidKey: - def test_has_some_codes(self): assert AndroidKey.ENTER == 66 assert AndroidKey.BACK == 4 diff --git a/test/unit/webdriver/network_test.py b/test/unit/webdriver/network_test.py index 5f682d30..a9cf7ea9 100644 --- a/test/unit/webdriver/network_test.py +++ b/test/unit/webdriver/network_test.py @@ -17,37 +17,34 @@ 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 -) +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.GET, - appium_command('/session/1234567890/network_connection'), - body='{"value": 2}' + 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/network_connection'), - body='{"value": ""}' + 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['parameters']['type'] == 2 + assert d['args'][0]['wifi'] is True @httpretty.activate def test_set_network_speed(self): @@ -56,10 +53,14 @@ def test_set_network_speed(self): 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['netspeed'] == NetSpeed.LTE + assert d['args'][0]['speed'] == NetSpeed.LTE @httpretty.activate def test_toggle_wifi(self): @@ -68,4 +69,9 @@ def test_toggle_wifi(self): 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 index 656544ca..d4534c90 100644 --- a/test/unit/webdriver/performance_test.py +++ b/test/unit/webdriver/performance_test.py @@ -14,29 +14,28 @@ import httpretty -from test.unit.helper.test_helper import ( - android_w3c_driver, - appium_command, - get_httpretty_request_body -) +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"]]}' + 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['packageName'] == 'my.app.package' - assert d['dataType'] == 'cpuinfo' - assert d['dataReadTimeout'] == 5 + assert d['args'][0]['packageName'] == 'my.app.package' + assert d['args'][0]['dataType'] == 'cpuinfo' @httpretty.activate def test_get_performance_data_types(self): @@ -44,6 +43,11 @@ def test_get_performance_data_types(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/performanceData/types'), - body='{"value": ["cpuinfo", "memoryinfo", "batteryinfo", "networkinfo"]}' + 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 index 1f0d8e83..da470ceb 100644 --- a/test/unit/webdriver/screen_record_test.py +++ b/test/unit/webdriver/screen_record_test.py @@ -9,15 +9,10 @@ import httpretty -from test.unit.helper.test_helper import ( - android_w3c_driver, - appium_command, - get_httpretty_request_body -) +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver -class TestWebDriverScreenRecord(object): - +class TestWebDriverScreenRecordAndroid(object): @httpretty.activate def test_start_recording_screen(self): driver = android_w3c_driver() @@ -38,7 +33,38 @@ def test_stop_recording_screen(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/stop_recording_screen'), - body='{"value": "b64_video_data"}' + 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' diff --git a/test/unit/webdriver/search_context/android_test.py b/test/unit/webdriver/search_context/android_test.py index 47df61b6..879d78f5 100644 --- a/test/unit/webdriver/search_context/android_test.py +++ b/test/unit/webdriver/search_context/android_test.py @@ -16,15 +16,67 @@ 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 -) +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): @@ -32,10 +84,12 @@ def test_find_element_by_android_data_matcher(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/element'), - body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}' + 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'}), ) - el = driver.find_element_by_android_data_matcher( - name='title', args=['title', 'Animation'], className='class name') d = get_httpretty_request_body(httpretty.last_request()) assert d['using'] == '-android datamatcher' @@ -51,9 +105,12 @@ def test_find_elements_by_android_data_matcher(self): 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"}]}' + 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']}) ) - els = driver.find_elements_by_android_data_matcher(name='title', args=['title', 'Animation']) d = get_httpretty_request_body(httpretty.last_request()) assert d['using'] == '-android datamatcher' @@ -66,12 +123,8 @@ def test_find_elements_by_android_data_matcher(self): @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_android_data_matcher() + 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' @@ -79,16 +132,18 @@ def test_find_elements_by_android_data_matcher_no_value(self): assert len(els) == 0 @httpretty.activate - def test_find_element_by_android_data_matcher(self): + def test_find_child_element_by_android_data_matcher(self): driver = android_w3c_driver() - element = MobileWebElement(driver, 'element_id', w3c=True) + 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"}}' + 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'}), ) - el = element.find_element_by_android_data_matcher( - name='title', args=['title', 'Animation'], className='class name') d = get_httpretty_request_body(httpretty.last_request()) assert d['using'] == '-android datamatcher' @@ -99,15 +154,18 @@ def test_find_element_by_android_data_matcher(self): assert el.id == 'child-element-id' @httpretty.activate - def test_find_elements_by_android_data_matcher(self): + def test_find_child_elements_by_android_data_matcher(self): driver = android_w3c_driver() - element = MobileWebElement(driver, 'element_id', w3c=True) + 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"}]}' + 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']}) ) - els = element.find_elements_by_android_data_matcher(name='title', args=['title', 'Animation']) d = get_httpretty_request_body(httpretty.last_request()) assert d['using'] == '-android datamatcher' @@ -118,15 +176,13 @@ def test_find_elements_by_android_data_matcher(self): assert els[1].id == 'child-element-id2' @httpretty.activate - def test_find_elements_by_android_data_matcher_no_value(self): + def test_find_child_elements_by_android_data_matcher_no_value(self): driver = android_w3c_driver() - element = MobileWebElement(driver, 'element_id', w3c=True) + element = MobileWebElement(driver, 'element_id') httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/element/element_id/elements'), - body='{"value": []}' + httpretty.POST, appium_command('/session/1234567890/element/element_id/elements'), body='{"value": []}' ) - els = element.find_elements_by_android_data_matcher() + els = element.find_elements(by=AppiumBy.ANDROID_DATA_MATCHER, value='{}') d = get_httpretty_request_body(httpretty.last_request()) assert d['using'] == '-android datamatcher' 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/search_context/windows_test.py b/test/unit/webdriver/search_context/windows_test.py deleted file mode 100644 index fb78e5f0..00000000 --- a/test/unit/webdriver/search_context/windows_test.py +++ /dev/null @@ -1,40 +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 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 TestWebDriverWindowsSearchContext(object): - - @httpretty.activate - def test_find_element_by_windows_uiautomation(self): - driver = android_w3c_driver() - element = MobileWebElement(driver, 'element_id', w3c=True) - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/element/element_id/element'), - body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "win-element-id"}}' - ) - el = element.find_element_by_windows_uiautomation('win_element') - - d = get_httpretty_request_body(httpretty.last_request()) - assert d['using'] == '-windows uiautomation' - assert el.id == 'win-element-id' diff --git a/test/unit/webdriver/settings_test.py b/test/unit/webdriver/settings_test.py index 59a41585..de9a0e00 100644 --- a/test/unit/webdriver/settings_test.py +++ b/test/unit/webdriver/settings_test.py @@ -15,22 +15,15 @@ import httpretty from appium.webdriver.webdriver import WebDriver -from test.unit.helper.test_helper import ( - android_w3c_driver, - appium_command, - get_httpretty_request_body -) +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}}' + httpretty.GET, appium_command('/session/1234567890/appium/settings'), body='{"value": {"sample": true}}' ) assert driver.get_settings()['sample'] is True @@ -41,7 +34,7 @@ def test_update_settings_bool(self): httpretty.POST, appium_command('/session/1234567890/appium/settings'), ) - assert isinstance(driver.update_settings({"sample": True}), WebDriver) + assert isinstance(driver.update_settings({'sample': True}), WebDriver) d = get_httpretty_request_body(httpretty.last_request()) assert d['settings']['sample'] is True @@ -50,9 +43,7 @@ def test_update_settings_bool(self): 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"}}' + httpretty.GET, appium_command('/session/1234567890/appium/settings'), body='{"value": {"sample": "string"}}' ) assert driver.get_settings()['sample'] == 'string' @@ -63,7 +54,7 @@ def test_update_settings_string(self): httpretty.POST, appium_command('/session/1234567890/appium/settings'), ) - assert isinstance(driver.update_settings({"sample": 'string'}), WebDriver) + 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/touch_action_test.py b/test/unit/webdriver/touch_action_test.py deleted file mode 100644 index 0ee3ec96..00000000 --- a/test/unit/webdriver/touch_action_test.py +++ /dev/null @@ -1,124 +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 pytest - -from appium.webdriver.common.touch_action import TouchAction - - -class TestTouchAction(object): - - @pytest.fixture - def touch_action(self): - return TouchAction(DriverStub()) - - def test_tap_json(self, touch_action): - json = [ - {'action': 'tap', 'options': {'count': 1, 'element': 1}} - ] - touch_action.tap(ElementStub(1)) - assert json == touch_action.json_wire_gestures - - def test_tap_x_y_json(self, touch_action): - json = [ - {'action': 'tap', 'options': {'x': 3, 'y': 4, 'count': 1, 'element': 1}} - ] - touch_action.tap(ElementStub(1), 3, 4) - assert json == touch_action.json_wire_gestures - - def test_press_json(self, touch_action): - json = [ - {'action': 'press', 'options': {'element': 1}} - ] - touch_action.press(ElementStub(1)) - assert json == touch_action.json_wire_gestures - - def test_press_pressure_json(self, touch_action): - json = [ - {'action': 'press', 'options': {'element': 1, 'pressure': 1.0}} - ] - touch_action.press(ElementStub(1), pressure=1.0) - assert json == touch_action.json_wire_gestures - - def test_press_x_y_json(self, touch_action): - json = [ - {'action': 'press', 'options': {'element': 1, 'x': 3, 'y': 4}} - ] - touch_action.press(ElementStub(1), 3, 4) - assert json == touch_action.json_wire_gestures - - def test_long_press_json(self, touch_action): - json = [ - {'action': 'longPress', 'options': {'element': 1, 'duration': 2000}} - ] - touch_action.long_press(ElementStub(1), duration=2000) - assert json == touch_action.json_wire_gestures - - def test_long_press_x_y_json(self, touch_action): - json = [ - {'action': 'longPress', 'options': {'element': 1, 'x': 3, 'y': 4, 'duration': 1000}} - ] - touch_action.long_press(ElementStub(1), 3, 4) - assert json == touch_action.json_wire_gestures - - def test_wait_json(self, touch_action): - json = [ - {'action': 'wait', 'options': {'ms': 10}} - ] - touch_action.wait(10) - assert json == touch_action.json_wire_gestures - - def test_wait_without_ms_json(self, touch_action): - json = [ - {'action': 'wait', 'options': {'ms': 0}} - ] - touch_action.wait() - assert json == touch_action.json_wire_gestures - - def test_move_to_json(self, touch_action): - json = [ - {'action': 'moveTo', 'options': {'element': 1, 'x': 3, 'y': 4}} - ] - touch_action.move_to(ElementStub(1), 3, 4) - assert json == touch_action.json_wire_gestures - - def test_release_json(self, touch_action): - json = [ - {'action': 'release', 'options': {}} - ] - touch_action.release() - assert json == touch_action.json_wire_gestures - - def test_perform_json(self, touch_action): - json_tap = [ - {'action': 'tap', 'options': {'element': 1, 'count': 1}} - ] - touch_action.tap(ElementStub(1)) - assert json_tap == touch_action.json_wire_gestures - touch_action.perform() - assert [] == touch_action.json_wire_gestures - - -class DriverStub(object): - def execute(self, _action, _params): - print("driver.execute called") - - -class ElementStub(object): - def __init__(self, e_id, _x=None, _y=None, _count=None): - self._id = e_id - - @property - def id(self): - return self._id diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index 87e45d18..443885d5 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -15,111 +15,76 @@ import json import httpretty +import urllib3 from mock import patch -from appium import version as appium_version from appium import webdriver -from appium.webdriver.webdriver 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, - ios_w3c_driver + get_httpretty_request_body, + ios_w3c_driver, + ios_w3c_driver_with_extensions, ) -class TestWebDriverWebDriver(object): - +class TestWebDriverWebDriver: @httpretty.activate def test_create_session(self): httpretty.register_uri( httpretty.POST, - 'http://localhost:4723/wd/hub/session', - body='{ "value": { "sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}}}' + 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', - 'automationName': 'UIAutomator2' } - driver = webdriver.Remote( - 'http://localhost:4723/wd/hub', - desired_caps - ) + driver = webdriver.Remote(SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps)) - assert len(httpretty.HTTPretty.latest_requests) == 1 + # 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 'appium/python {} (selenium'.format(appium_version.version) in request.headers['user-agent'] + 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.get('desiredCapabilities') is not None - - assert driver.session_id == 'session-id' - assert driver.w3c - assert driver.command_executor.w3c - - @httpretty.activate - def test_create_session_forceMjsonwp(self): - httpretty.register_uri( - httpretty.POST, - 'http://localhost:4723/wd/hub/session', - body='{ "capabilities": {"deviceName": "Android Emulator"}, "status": 0, "sessionId": "session-id"}' - ) - - desired_caps = { + assert request_json['capabilities']['alwaysMatch'] == { 'platformName': 'Android', - 'deviceName': 'Android Emulator', - 'app': 'path/to/app', - 'automationName': 'UIAutomator2', - 'forceMjsonwp': True + 'appium:deviceName': 'Android Emulator', + 'appium:app': 'path/to/app', + 'appium:automationName': 'UIAutomator2', } - driver = webdriver.Remote( - 'http://localhost:4723/wd/hub', - desired_caps - ) - - assert len(httpretty.HTTPretty.latest_requests) == 1 - - request = httpretty.HTTPretty.latest_requests[0] - assert request.headers['content-type'] == 'application/json;charset=UTF-8' - assert 'appium/python {} (selenium'.format(appium_version.version) in request.headers['user-agent'] - - request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8')) - assert request_json.get('capabilities') is None - assert request_json.get('desiredCapabilities') is not None + assert request_json.get('desiredCapabilities') is None assert driver.session_id == 'session-id' - assert driver.w3c is False - assert driver.command_executor.w3c is False @httpretty.activate def test_create_session_change_session_id(self): httpretty.register_uri( httpretty.POST, - 'http://localhost:4723/wd/hub/session', - body='{ "value": { "sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}}}' + f'{SERVER_URL_BASE}/session', + body='{ "sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"} }', ) httpretty.register_uri( httpretty.GET, - 'http://localhost:4723/wd/hub/session/another-session-id/title', - body='{ "value": "title on another session id"}' + f'{SERVER_URL_BASE}/session/another-session-id/title', + body='{ "value": "title on another session id"}', ) - desired_caps = { - 'platformName': 'Android', - 'deviceName': 'Android Emulator', - 'app': 'path/to/app', - 'automationName': 'UIAutomator2' - } - driver = webdriver.Remote( - 'http://localhost:4723/wd/hub', - desired_caps - ) + 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' @@ -133,99 +98,121 @@ def test_create_session_change_session_id(self): def test_create_session_register_uridirect(self): httpretty.register_uri( httpretty.POST, - 'http://localhost:4723/wd/hub/session', - body=json.dumps({'value': { - 'sessionId': 'session-id', - 'capabilities': { - 'deviceName': 'Android Emulator', - 'directConnectProtocol': 'http', - 'directConnectHost': 'localhost2', - 'directConnectPort': 4800, - 'directConnectPath': '/special/path/wd/hub', + 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']}) + body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}), ) desired_caps = { 'platformName': 'Android', 'deviceName': 'Android Emulator', 'app': 'path/to/app', - 'automationName': 'UIAutomator2' + 'automationName': 'UIAutomator2', } + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True) driver = webdriver.Remote( - 'http://localhost:4723/wd/hub', - desired_caps, - direct_connection=True + SERVER_URL_BASE, + options=UiAutomator2Options().load_capabilities(desired_caps), + client_config=client_config, ) - assert 'http://localhost2:4800/special/path/wd/hub' == driver.command_executor._url + 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, - 'http://localhost:4723/wd/hub/session', - body=json.dumps({'value': { - 'sessionId': 'session-id', - 'capabilities': { - 'deviceName': 'Android Emulator', - 'directConnectProtocol': 'http', - 'directConnectHost': 'localhost2', - 'directConnectPort': 4800 + 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, - 'http://localhost:4723/wd/hub/session/session-id/contexts', - body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}) + 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' + 'automationName': 'UIAutomator2', } + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True) driver = webdriver.Remote( - 'http://localhost:4723/wd/hub', - desired_caps, - direct_connection=True + SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps), client_config=client_config ) - assert 'http://localhost:4723/wd/hub' == driver.command_executor._url + 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_get_all_sessions(self): - driver = ios_w3c_driver() + 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.GET, - appium_command('/sessions'), - body=json.dumps({'value': {'deviceName': 'iPhone Simulator', 'events': {'simStarted': [1234567891]}}}) + httpretty.POST, + f'{SERVER_URL_BASE}/session', + body=json.dumps( + { + 'sessionId': 'session-id', + 'capabilities': { + 'deviceName': 'Android Emulator', + }, + } + ), ) - session = driver.all_sessions - assert len(session) != 1 - @httpretty.activate - def test_get_session(self): - driver = ios_w3c_driver() httpretty.register_uri( httpretty.GET, - appium_command('/session/1234567890'), - body=json.dumps({'value': {'deviceName': 'iPhone Simulator', 'events': {'simStarted': [1234567890]}}}) + f'{SERVER_URL_BASE}/session/session-id/contexts', + body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}), ) - session = driver.session - assert session['deviceName'] == 'iPhone Simulator' - assert session['events']['simStarted'] == [1234567890] + + 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): @@ -233,7 +220,7 @@ def test_get_events(self): httpretty.register_uri( httpretty.GET, appium_command('/session/1234567890'), - body=json.dumps({'value': {'events': {'simStarted': [1234567890]}}}) + body=json.dumps({'value': {'events': {'simStarted': [1234567890]}}}), ) events = driver.events assert events['simStarted'] == [1234567890] @@ -241,53 +228,247 @@ def test_get_events(self): @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': {}}) - ) + 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({}) - ) + 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") + @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'), - body=exceptionCallback + appium_command('/session/1234567890/path/to/custom/url'), + body=json.dumps({'value': {}}), ) - events = driver.events - mock_warning.assert_called_once() - assert events == {} + 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, desired_capabilities, direct_connection=False): + def __init__(self, command_executor, options=None): super().__init__( command_executor=command_executor, - desired_capabilities=desired_capabilities, - direct_connection=direct_connection + options=options, ) class SubSubWebDriver(SubWebDriver): - def __init__(self, command_executor, desired_capabilities, direct_connection=False): + def __init__(self, command_executor, options=None): super().__init__( command_executor=command_executor, - desired_capabilities=desired_capabilities, - direct_connection=direct_connection + options=options, ) @@ -295,69 +476,52 @@ class TestSubModuleWebDriver(object): def android_w3c_driver(self, driver_class): response_body_json = json.dumps( { - 'value': { - 'sessionId': '1234567890', - 'capabilities': { - 'platform': 'LINUX', - 'desired': { - 'platformName': 'Android', - 'automationName': 'uiautomator2', - 'platformVersion': '7.1.1', - 'deviceName': 'Android Emulator', - 'app': '/test/apps/ApiDemos-debug.apk', - }, + 'sessionId': '1234567890', + 'capabilities': { + 'platform': 'LINUX', + 'desired': { 'platformName': 'Android', 'automationName': 'uiautomator2', 'platformVersion': '7.1.1', - 'deviceName': 'emulator-5554', + 'deviceName': 'Android Emulator', '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' - } - } + }, + '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 - ) + 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' + 'automationName': 'UIAutomator2', } - driver = driver_class( - 'http://localhost:4723/wd/hub', - desired_caps - ) + 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"}' - ) + 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"}' - ) + httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890/context'), body='{"value": "NATIVE"}') assert driver.current_context == 'NATIVE' @httpretty.activate diff --git a/test/unit/webdriver/webelement_test.py b/test/unit/webdriver/webelement_test.py index 88347b57..23268c3a 100644 --- a/test/unit/webdriver/webelement_test.py +++ b/test/unit/webdriver/webelement_test.py @@ -13,45 +13,34 @@ # limitations under the License. import json -import os 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 -) +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body class TestWebElement(object): - @httpretty.activate - def test_set_value(self): + def test_status(self): driver = android_w3c_driver() + response = {'ready': True, 'message': {'build': {'version': '2.0.0', 'revision': None}}} httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/appium/element/element_id/value') + httpretty.GET, + appium_command('/status'), + body=json.dumps({'value': response}), ) + s = driver.get_status() - element = MobileWebElement(driver, 'element_id', w3c=True) - value = 'happy testing' - element.set_value(value) - - d = get_httpretty_request_body(httpretty.last_request()) - assert d['value'] == [value] + 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') - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/element/element_id/value')) - element = MobileWebElement(driver, 'element_id', w3c=True) + element = MobileWebElement(driver, 'element_id') element.send_keys('happy testing') d = get_httpretty_request_body(httpretty.last_request()) @@ -62,13 +51,10 @@ 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') - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/element/element_id/value')) try: - element = MobileWebElement(driver, 'element_id', w3c=True) + element = MobileWebElement(driver, 'element_id') element.send_keys(tmp_f.name) finally: tmp_f.close() @@ -76,25 +62,45 @@ def test_send_key_with_file(self): 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 - } + 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}) + body=json.dumps({'value': rect_dict}), ) - element = MobileWebElement(driver, 'element_id', w3c=True) + element = MobileWebElement(driver, 'element_id') ef = element.get_attribute('rect') - d = httpretty.last_request() + 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/tox.ini b/tox.ini deleted file mode 100644 index 99a6b1a3..00000000 --- a/tox.ini +++ /dev/null @@ -1,14 +0,0 @@ -[tox] -skipsdist = True -envlist = - py37, - py38, - py39-dev - -[testenv] -deps = - pipenv -commands = - pipenv lock --clear - pipenv install - ./ci.sh 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" }, +]