From df1a6230eff16ef82c77abd3837207799fafde0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 11:22:56 +0200 Subject: [PATCH 01/28] chore: initialize project with uv --- .gitignore | 283 +++++++++++++++++++++++++ pyproject.toml | 22 ++ src/android_device_manager/__init__.py | 2 + src/android_device_manager/py.typed | 0 4 files changed, 307 insertions(+) create mode 100644 .gitignore create mode 100644 pyproject.toml create mode 100644 src/android_device_manager/__init__.py create mode 100644 src/android_device_manager/py.typed diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6df56ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,283 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,pycharm+all +# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,pycharm+all + +### PyCharm+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,pycharm+all diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..413485b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "android-device-manager" +version = "0.0.0" +description = "Android Device Manager is a Python library for creating, launching, and managing Android device programmatically. " +authors = [ + { name = "Jérémy Woirhaye", email = "jerem.woirhaye@gmail.com" } +] +maintainers = [ + { name = "Jérémy Woirhaye", email = "jerem.woirhaye@gmail.com" } +] + +requires-python = ">=3.10" +dependencies = [] + +[project.urls] +Homepage = "https://github.com/jwoirhaye/android-device-manager-python" +Source = "https://github.com/jwoirhaye/android-device-manager-python" +Issues = "https://github.com/jwoirhaye/android-device-manager-python/issues" + +[build-system] +requires = ["uv_build>=0.8.5,<0.9.0"] +build-backend = "uv_build" diff --git a/src/android_device_manager/__init__.py b/src/android_device_manager/__init__.py new file mode 100644 index 0000000..ffc3371 --- /dev/null +++ b/src/android_device_manager/__init__.py @@ -0,0 +1,2 @@ +def hello() -> str: + return "Hello from android-device-manager!" diff --git a/src/android_device_manager/py.typed b/src/android_device_manager/py.typed new file mode 100644 index 0000000..e69de29 From b63f6ba1e6a499cc793bdfce2a7f937119bf5efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 11:45:39 +0200 Subject: [PATCH 02/28] feat(avd): implement basic AVD lifecycle (create, delete) --- examples/create_device.py | 17 ++ src/android_device_manager/__init__.py | 5 +- src/android_device_manager/android_device.py | 101 ++++++++ src/android_device_manager/avd/__init__.py | 7 + src/android_device_manager/avd/config.py | 18 ++ src/android_device_manager/avd/exceptions.py | 39 ++++ src/android_device_manager/avd/manager.py | 220 ++++++++++++++++++ src/android_device_manager/exceptions.py | 22 ++ src/android_device_manager/utils/__init__.py | 0 .../utils/android_sdk.py | 105 +++++++++ .../utils/sdk_manager.py | 82 +++++++ .../utils/validation.py | 15 ++ uv.lock | 8 + 13 files changed, 637 insertions(+), 2 deletions(-) create mode 100644 examples/create_device.py create mode 100644 src/android_device_manager/android_device.py create mode 100644 src/android_device_manager/avd/__init__.py create mode 100644 src/android_device_manager/avd/config.py create mode 100644 src/android_device_manager/avd/exceptions.py create mode 100644 src/android_device_manager/avd/manager.py create mode 100644 src/android_device_manager/exceptions.py create mode 100644 src/android_device_manager/utils/__init__.py create mode 100644 src/android_device_manager/utils/android_sdk.py create mode 100644 src/android_device_manager/utils/sdk_manager.py create mode 100644 src/android_device_manager/utils/validation.py create mode 100644 uv.lock diff --git a/examples/create_device.py b/examples/create_device.py new file mode 100644 index 0000000..a56077e --- /dev/null +++ b/examples/create_device.py @@ -0,0 +1,17 @@ +from android_device_manager import AndroidDevice +from android_device_manager.avd.config import AVDConfiguration + +device = AndroidDevice( + AVDConfiguration( + name="test_from_lib", + package="system-images;android-36;google_apis;x86_64" + ) +) + +device.create() + +print(device._avd_manager.list()) + +device.delete() + +print(device._avd_manager.list()) diff --git a/src/android_device_manager/__init__.py b/src/android_device_manager/__init__.py index ffc3371..9d981cd 100644 --- a/src/android_device_manager/__init__.py +++ b/src/android_device_manager/__init__.py @@ -1,2 +1,3 @@ -def hello() -> str: - return "Hello from android-device-manager!" +from .android_device import AndroidDevice + +__all__ = ["AndroidDevice"] diff --git a/src/android_device_manager/android_device.py b/src/android_device_manager/android_device.py new file mode 100644 index 0000000..184a3c5 --- /dev/null +++ b/src/android_device_manager/android_device.py @@ -0,0 +1,101 @@ +import logging +import subprocess +from enum import Enum +from pathlib import Path +from time import sleep +from typing import Optional, List, Union, Dict + + +from .avd.config import AVDConfiguration +from .avd.exceptions import AVDCreationError, AVDDeletionError +from .avd.manager import AVDManager +from .exceptions import AndroidDeviceError + +from .utils.android_sdk import AndroidSDK + +logger = logging.getLogger(__name__) + + +class AndroidDeviceState(Enum): + """ + Enumeration of possible states for an AndroidDevice. + + Attributes: + NOT_CREATED: The AVD does not exist yet. + CREATED: The AVD exists but the emulator is not running. + RUNNING: The emulator is running and fully booted. + STOPPED: The emulator was running but is now stopped. + ERROR: An error occurred during an operation. + """ + + NOT_CREATED = "not_created" + CREATED = "created" + RUNNING = "running" + STOPPED = "stopped" + ERROR = "error" + +class AndroidDevice: + def __init__( + self, + avd_config: AVDConfiguration, + android_sdk: Optional[AndroidSDK] = None, + ): + self._avd_config = avd_config + self.state = AndroidDeviceState.NOT_CREATED + + self._android_sdk = android_sdk or AndroidSDK() + self._avd_manager = AVDManager(self._android_sdk) + + @property + def name(self) -> str: + """ + The name of the managed AVD. + + Returns: + str: The name of the AVD. + """ + return self._avd_config.name + + def create(self, force: bool = False) -> None: + """ + Create the AVD if it does not exist. + + Args: + force (bool): If True, overwrite any existing AVD with the same name. + + Raises: + AVDCreationError: If the AVD cannot be created. + """ + logger.info(f"Creating AVD '{self.name}'...") + try: + if not self._avd_manager.exist(self.name): + self._avd_manager.create(self._avd_config, force=force) + self.state = AndroidDeviceState.CREATED + logger.info(f"AVD '{self.name}' created.") + else: + logger.info(f"AVD '{self.name}' already exists.") + self.state = AndroidDeviceState.CREATED + except AVDCreationError as e: + self.state = AndroidDeviceState.ERROR + raise e + except Exception as e: + self.state = AndroidDeviceState.ERROR + logger.error(f"Failed to create AVD '{self.name}': {e}") + raise AVDCreationError(self.name, f"Failed to create AVD : {e}") from e + + def delete(self): + """ + Delete the AVD. + + Raises: + AVDDeletionError: If deletion fails. + """ + logger.info(f"Deleting AVD '{self.name}'...") + try: + self._avd_manager.delete(self.name) + self.state = AndroidDeviceState.NOT_CREATED + logger.info(f"AVD '{self.name}' deleted.") + except Exception as e: + self.state = AndroidDeviceState.ERROR + logger.error(f"Failed to delete AVD '{self.name}': {e}") + raise AVDDeletionError(self.name, f"Failed to delete AVD: {e}") from e \ No newline at end of file diff --git a/src/android_device_manager/avd/__init__.py b/src/android_device_manager/avd/__init__.py new file mode 100644 index 0000000..b751a1a --- /dev/null +++ b/src/android_device_manager/avd/__init__.py @@ -0,0 +1,7 @@ +from .manager import AVDManager +from .config import AVDConfiguration + +__all__ = [ + "AVDManager", + "AVDConfiguration", +] \ No newline at end of file diff --git a/src/android_device_manager/avd/config.py b/src/android_device_manager/avd/config.py new file mode 100644 index 0000000..dd9eb78 --- /dev/null +++ b/src/android_device_manager/avd/config.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass + + +@dataclass +class AVDConfiguration: + """ + Configuration for an Android Virtual Device (AVD). + + This dataclass encapsulates the minimal configuration needed to define + an Android Virtual Device, such as its name and the associated system image package. + + Attributes: + name (str): The name of the AVD (must be unique within the Android SDK). + package (str): The system image package path (e.g. "system-images;android-34;google_apis;x86_64"). + """ + + name: str + package: str diff --git a/src/android_device_manager/avd/exceptions.py b/src/android_device_manager/avd/exceptions.py new file mode 100644 index 0000000..af5f388 --- /dev/null +++ b/src/android_device_manager/avd/exceptions.py @@ -0,0 +1,39 @@ +from ..exceptions import AndroidDeviceManagerError + + +class AVDCreationError(AndroidDeviceManagerError): + """ + Raised when the creation of an Android Virtual Device (AVD) fails. + + Attributes: + name (str): The name of the AVD for which creation failed. + message (str): Details about the cause of the failure. + + Args: + name (str): The name of the AVD. + message (str): Description of the creation error. + """ + + def __init__(self, name: str, message: str): + super().__init__(f"AVD '{name}': {message}") + self.name = name + self.message = message + + +class AVDDeletionError(AndroidDeviceManagerError): + """ + Raised when the deletion of an Android Virtual Device (AVD) fails. + + Attributes: + name (str): The name of the AVD for which deletion failed. + message (str): Details about the cause of the failure. + + Args: + name (str): The name of the AVD. + message (str): Description of the deletion error. + """ + + def __init__(self, name: str, message: str): + super().__init__(f"AVD '{name}': {message}") + self.name = name + self.message = message diff --git a/src/android_device_manager/avd/manager.py b/src/android_device_manager/avd/manager.py new file mode 100644 index 0000000..507431d --- /dev/null +++ b/src/android_device_manager/avd/manager.py @@ -0,0 +1,220 @@ +import logging +import subprocess +from typing import List + +from ..avd.config import AVDConfiguration +from ..avd.exceptions import AVDCreationError, AVDDeletionError +from ..utils.android_sdk import AndroidSDK +from ..utils.sdk_manager import SDKManager +from ..utils.validation import is_valid_avd_name + +logger = logging.getLogger(__name__) + + +class AVDManager: + """ + High-level manager for Android Virtual Devices (AVDs). + """ + + def __init__(self, sdk: AndroidSDK): + """ + Initialize the AVDManager. + + Args: + sdk (AndroidSDK): The Android SDK abstraction for resolving paths. + """ + self.sdk = sdk + self.avd_manager_path = self.sdk.avdmanager_path + self._sdk_manager = SDKManager(sdk) + + def create(self, config: AVDConfiguration, force: bool = False) -> bool: + """ + Create a new AVD with the specified configuration. + + Args: + config: AVDConfiguration instance + force: Overwrite existing AVD if True + + Returns: + bool: True if AVD was created successfully + + Raises: + AVDCreationError: If creation fails + """ + try: + logger.info(f"Creating AVD '{config.name}' with package '{config.package}'") + + if not is_valid_avd_name(config.name): + raise AVDCreationError( + config.name, + f"Invalid AVD name '{config.name}'. Must start with a letter and contain only letters, digits, underscores or hyphens.", + ) + + if not force and self.exist(config.name): + raise AVDCreationError( + config.name, + f"AVD '{config.name}' already exists. Use force=True to overwrite.", + ) + + if not self._sdk_manager.is_system_image_installed(config.package): + raise AVDCreationError( + config.name, + f"System image '{config.package}' is not available. Please install it first.", + ) + + args = [ + "-s", + "create", + "avd", + "--name", + config.name, + "--package", + config.package, + ] + if force: + args.append("--force") + + result = self._run_avd_command(args, timeout=120) + if result.returncode != 0: + error_msg = result.stderr.strip() if result.stderr else "Unknown error" + raise AVDCreationError( + config.name, + f"Failed to create AVD '{config.name}': {error_msg}", + ) + + logger.info(f"AVD '{config.name}' created successfully") + return True + except subprocess.TimeoutExpired: + raise AVDCreationError( + config.name, f"Timeout while creating AVD '{config.name}'" + ) + except AVDCreationError: + raise + except Exception as e: + logger.error( + f"Unexpected error creating AVD '{config.name}': {e}", exc_info=True + ) + + raise AVDCreationError(config.name, str(e)) from e + + def delete(self, name: str) -> bool: + """ + Delete an existing AVD by name. + + Args: + name: Name of the AVD to delete + + Returns: + bool: True if AVD was deleted successfully + + Raises: + AVDDeletionError: If AVD deletion fails + """ + try: + logger.info(f"Deleting AVD '{name}'") + + if not self.exist(name): + logger.warning(f"AVD '{name}' does not exist") + return True + + args = ["delete", "avd", "--name", name] + result = self._run_avd_command(args, timeout=60) + + if result.returncode != 0: + error_msg = result.stderr.strip() if result.stderr else "Unknown error" + raise AVDDeletionError(name, error_msg) + + logger.info(f"AVD '{name}' deleted successfully") + return True + + except subprocess.TimeoutExpired: + raise AVDDeletionError(name, f"Timeout while deleting AVD '{name}'") + except AVDDeletionError: + raise + except Exception as e: + raise AVDDeletionError( + name, f"Unexpected error deleting AVD '{name}': {str(e)}" + ) from e + + def list(self) -> List[str]: + """ + List all available AVD names. + + Returns: + List[str]: Names of all available AVDs + """ + try: + cmd = ["list", "avd", "-c"] + result = self._run_avd_command(cmd, timeout=30, check=True) + return self._parse_avd_list(result.stdout) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to list AVDs: {e.stderr}") + return [] + except subprocess.TimeoutExpired: + logger.error("Timeout while listing AVDs") + return [] + except Exception as e: + logger.error(f"Unexpected error listing AVDs: {str(e)}") + return [] + + def exist(self, name: str) -> bool: + return name in self.list() + + @staticmethod + def _parse_avd_list(output: str) -> List[str]: + """ + Parse the output from 'avdmanager list avd -c' to get AVD names. + + Args: + output: Raw output string from avdmanager + + Returns: + List[str]: List of AVD names + """ + return [line.strip() for line in output.strip().splitlines() if line.strip()] + + def _run_avd_command( + self, + args: List[str], + timeout: int = 60, + check: bool = True, + ) -> subprocess.CompletedProcess: + """ + Internal helper to run an avdmanager CLI command. + + Args: + args (List[str]): CLI arguments to pass (excluding avdmanager itself). + timeout (int): Timeout in seconds for the command (default: 60). + check (bool): If True, CalledProcessError is raised for non-zero return code. + + Returns: + subprocess.CompletedProcess: The result of the subprocess.run call. + + Raises: + subprocess.TimeoutExpired: If the command times out. + subprocess.CalledProcessError: If the command fails (when check=True). + Exception: For any other unexpected error. + """ + cmd = [str(self.avd_manager_path)] + args + + logger.debug(f"Running avdmanager command: {' '.join(cmd)}") + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout, + check=check, + ) + logger.debug(f"stdout: {result.stdout.strip()}") + logger.debug(f"stderr: {result.stderr.strip()}") + return result + except subprocess.TimeoutExpired: + logger.error(f"Timeout expired for command: {' '.join(cmd)}") + raise + except subprocess.CalledProcessError as e: + logger.error(f"Command failed: {e.stderr.strip() if e.stderr else e}") + raise + except Exception as e: + logger.error(f"Unexpected error running command: {e}") + raise diff --git a/src/android_device_manager/exceptions.py b/src/android_device_manager/exceptions.py new file mode 100644 index 0000000..91569cb --- /dev/null +++ b/src/android_device_manager/exceptions.py @@ -0,0 +1,22 @@ +class AndroidDeviceManagerError(Exception): + """ + Base exception for all errors raised by the android-device-manager library. + + All custom exceptions in this library inherit from this class, + allowing users to catch all library-specific errors with a single except block. + """ + +class AndroidDeviceError(AndroidDeviceManagerError): + """ + This exception is raised when an error occurs while interacting with + an AndroidDevice. + """ + +class AndroidSDKNotFound(AndroidDeviceManagerError): + """ + Raised when the Android SDK or a required SDK tool cannot be found. + """ + + +class SDKManagerError(AndroidDeviceManagerError): + """Base exception for SDK Manager operations""" diff --git a/src/android_device_manager/utils/__init__.py b/src/android_device_manager/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/android_device_manager/utils/android_sdk.py b/src/android_device_manager/utils/android_sdk.py new file mode 100644 index 0000000..3c33c59 --- /dev/null +++ b/src/android_device_manager/utils/android_sdk.py @@ -0,0 +1,105 @@ +import logging +import os +from pathlib import Path +from typing import Optional + +from ..exceptions import AndroidSDKNotFound + +logger = logging.getLogger(__name__) + + +class AndroidSDK: + """Utility class for locating and querying the Android SDK.""" + + def __init__(self, sdk_path: Optional[Path] = None): + """ + Initialize the SDK object. + + Args: + sdk_path (Optional[Path]): Path to the Android SDK. If None, the SDK path will be auto-detected. + + Raises: + SDKNotFoundError: If the SDK cannot be found or is invalid. + """ + self.sdk_path = sdk_path or self._find_sdk_path() + if not self.sdk_path or not self.is_valid(): + raise AndroidSDKNotFound(str(self.sdk_path)) + + @property + def avdmanager_path(self) -> Path: + """ + Returns the path to `avdmanager`. + + Returns: + Path: Absolute path to the avdmanager executable. + """ + return self.sdk_path / "cmdline-tools" / "latest" / "bin" / "avdmanager" + + @property + def emulator_path(self) -> Path: + """ + Returns the path to the emulator binary. + + Returns: + Path: Absolute path to the emulator executable. + """ + return self.sdk_path / "emulator" / "emulator" + + @property + def adb_path(self) -> Path: + """ + Returns the path to the adb binary. + + Returns: + Path: Absolute path to the adb executable. + """ + return self.sdk_path / "platform-tools" / "adb" + + @property + def sdkmanager_path(self) -> Path: + """ + Returns the path to `sdkmanager`. + + Returns: + Path: Absolute path to the sdkmanager executable. + """ + return self.sdk_path / "cmdline-tools" / "latest" / "bin" / "sdkmanager" + + def is_valid(self) -> bool: + """ + Checks if the SDK has the required tools. + + Returns: + bool: True if all required tools exist, False otherwise. + """ + required = [ + self.avdmanager_path, + self.emulator_path, + self.adb_path, + self.sdkmanager_path, + ] + return all(tool.exists() for tool in required) + + @staticmethod + def _find_sdk_path() -> Optional[Path]: + """ + Attempts to locate the Android SDK path on the machine. + + Returns: + Optional[Path]: Path if found, otherwise None. + """ + for env_var in ("ANDROID_HOME", "ANDROID_SDK_ROOT"): + p = os.environ.get(env_var) + if p: + path = Path(p) + if path.exists(): + return path + candidates = [ + Path.home() / "Android" / "Sdk", + Path("/usr/local/android-sdk"), + Path("/opt/android-sdk"), + ] + for path in candidates: + if path.exists(): + return path + return None diff --git a/src/android_device_manager/utils/sdk_manager.py b/src/android_device_manager/utils/sdk_manager.py new file mode 100644 index 0000000..2e2ba3d --- /dev/null +++ b/src/android_device_manager/utils/sdk_manager.py @@ -0,0 +1,82 @@ +import logging +import subprocess + +from ..exceptions import SDKManagerError +from ..utils.android_sdk import AndroidSDK + +logger = logging.getLogger(__name__) + + +class SDKManager: + """Wrapper around the `sdkmanager` CLI to interact with the Android SDK.""" + + def __init__(self, sdk: AndroidSDK): + self.sdkmanager_path = sdk.sdkmanager_path + + def is_system_image_installed(self, package: str) -> bool: + """Checks whether a given Android system image is installed. + + It runs `sdkmanager --list` and parses the output to find if the + system image is listed in the "Installed packages" section. + + Args: + package (str): The full system image package path + (e.g., "system-images;android-30;google_apis;x86"). + + Returns: + bool: True if the system image is installed, False otherwise. + + Raises: + SDKManagerError: If the command fails or parsing fails. + """ + try: + result = subprocess.run( + [self.sdkmanager_path, "--list"], + capture_output=True, + text=True, + check=True, + ) + + output = result.stdout + lines = output.split("\n") + + in_installed_section = False + + for line in lines: + line = line.strip() + + if ( + not line + or line.startswith("[=") + or line.startswith("Loading") + or line.startswith("Computing") + ): + continue + + if line.startswith("Installed packages:"): + in_installed_section = True + continue + + if line.startswith("Available Packages:"): + break + + if line.startswith("Path") or line.startswith("-------"): + continue + + if in_installed_section: + parts = line.split("|") + if len(parts) >= 1: + package_path = parts[0].strip() + if package_path == package: + logger.debug(f"System image is installed: {package}") + return True + + logger.debug(f"System image is not installed: {package}") + return False + + except subprocess.CalledProcessError as e: + logger.error(f"Failed to execute sdkmanager --list: {e}") + raise SDKManagerError(f"Failed to list SDK packages: {e}") + except Exception as e: + logger.error(f"Error checking system image installation: {e}") + raise SDKManagerError(f"Error checking system image installation: {e}") diff --git a/src/android_device_manager/utils/validation.py b/src/android_device_manager/utils/validation.py new file mode 100644 index 0000000..a679500 --- /dev/null +++ b/src/android_device_manager/utils/validation.py @@ -0,0 +1,15 @@ +import re + + +def is_valid_avd_name(name: str) -> bool: + """ + Validate that the AVD name follows Android Studio's naming rules. + + Rules: + - Must start with a letter [a-zA-Z] + - Can contain letters, numbers, underscores and hyphens + - No spaces or other special characters + """ + if not name: + return False + return bool(re.match(r"^[a-zA-Z][a-zA-Z0-9_-]*$", name)) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..cb44752 --- /dev/null +++ b/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "android-device-manager" +version = "0.0.0" +source = { editable = "." } From d69e5096ada27970bd51bb47f0ab7d10a2ce8c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 12:01:51 +0200 Subject: [PATCH 03/28] feat(adb): implement AdbClient for ADB communication with emulator Includes methods to: - get system properties via get_prop() - wait for emulator boot completion - kill the emulator instance - execute shell commands with timeout and error handling --- src/android_device_manager/adb/__init__.py | 0 src/android_device_manager/adb/client.py | 189 +++++++++++++++++++ src/android_device_manager/adb/exceptions.py | 36 ++++ src/android_device_manager/constants.py | 45 +++++ 4 files changed, 270 insertions(+) create mode 100644 src/android_device_manager/adb/__init__.py create mode 100644 src/android_device_manager/adb/client.py create mode 100644 src/android_device_manager/adb/exceptions.py create mode 100644 src/android_device_manager/constants.py diff --git a/src/android_device_manager/adb/__init__.py b/src/android_device_manager/adb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/android_device_manager/adb/client.py b/src/android_device_manager/adb/client.py new file mode 100644 index 0000000..4a79579 --- /dev/null +++ b/src/android_device_manager/adb/client.py @@ -0,0 +1,189 @@ +import logging +import subprocess +from typing import List, Optional, Union + +from ..adb.exceptions import ADBError, ADBTimeoutError +from ..constants import AndroidProp +from ..utils.android_sdk import AndroidSDK + +logger = logging.getLogger(__name__) + + +class AdbClient: + """ + A client for interacting with an Android emulator/device via the Android Debug Bridge (ADB). + """ + + def __init__(self, emulator_port: int, android_sdk: Optional[AndroidSDK]=None): + + """ + Initialize the AdbClient. + + Args: + emulator_port (int): The TCP port number of the emulator (e.g., 5554). + android_sdk (AndroidSDK): The Android SDK abstraction providing the adb path. + """ + self._port = emulator_port + self._serial = f"emulator-{self._port}" + self._android_sdk = android_sdk or AndroidSDK() + self._adb_path = self._android_sdk.adb_path + + def get_prop( + self, key: str | AndroidProp, timeout: int = 10, check: bool = True + ) -> str: + """ + Get a single Android system property via adb. + + Args: + key (str or AndroidProp): The name of the property, or an AndroidProp Enum. + timeout (int): Timeout in seconds. + check (bool): Raise if the command fails. + + Returns: + str: Value of the property, or '' if not found. + + Raises: + ADBError: If the adb command fails. + """ + if isinstance(key, AndroidProp): + key = key.value + result = self.shell(["getprop", key], timeout=timeout, check=check) + return result.stdout.strip() + + def wait_for_boot(self, timeout: int = 120) -> bool: + """ + Wait for the emulator to fully boot (until 'sys.boot_completed' is set). + + Args: + timeout (int): Maximum time to wait in seconds (default: 120). + + Returns: + bool: True if the device booted successfully before the timeout. + + Raises: + TimeoutError: If the device did not boot in the specified time. + """ + import time + + start_time = time.time() + while time.time() - start_time < timeout: + result_boot_completed = self.get_prop( + AndroidProp.BOOT_COMPLETED, check=False + ) + if result_boot_completed == "1": + return True + raise ADBTimeoutError( + + f"Device {self._serial} did not boot within {timeout} seconds." + ) + + def kill_emulator(self): + """ + Kill (terminate) the emulator instance via ADB. + + Raises: + ADBError: If the emulator could not be killed. + """ + logger.info(f"Killing emulator with serial: {self._serial}") + try: + self._run_adb_command(["emu", "kill"]) + logger.info(f"Successfully killed emulator {self._serial}") + except ADBError as e: + raise ADBError(f"Failed to kill emulator {self._serial}: {str(e)}") + + def shell( + self, cmd: list[str], timeout: int = 30, check: bool = True + ) -> subprocess.CompletedProcess: + """ + Execute a shell command on the device/emulator via ADB. + + Args: + cmd (List[str]): The shell command as a list of arguments. Example: ["ls", "/sdcard"] + timeout (int): Timeout for the command (default: 30). + check (bool): If True, raise an exception for non-zero exit code. + + Returns: + subprocess.CompletedProcess: The result object (stdout, stderr, etc.). + + Raises: + ADBError: If the command fails (and check=True). + ADBTimeoutError: On timeout. + """ + args = ["shell"] + cmd + return self._run_adb_command(args, timeout=timeout, check=check) + + def _run_adb_command( + self, args: list[str], timeout: int = 30, check: bool = True + ) -> subprocess.CompletedProcess: + """ + Run an ADB command for the associated emulator/device with error handling. + + Args: + args (List[str]): List of ADB command arguments (excluding adb and -s). + timeout (int): Timeout in seconds for the command (default: 30). + check (bool): If True, CalledProcessError is raised for non-zero return codes. + + Returns: + subprocess.CompletedProcess: The result object from subprocess.run. + + Raises: + ADBError: If the command fails (non-zero return code or unexpected error). + ADBTimeoutError: If the command times out. + + """ + cmd = [str(self._adb_path), "-s", self._serial] + args + logger.debug(f"Executing ADB command: {' '.join(cmd)}") + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=timeout, check=check + ) + logger.debug("ADB stdout: %s", result.stdout.strip()) + logger.debug("ADB stderr: %s", result.stderr.strip()) + return result + except subprocess.CalledProcessError as e: + logger.error( + "ADB (%s): command failed: %r (exit code %d)\nstdout: %s\nstderr: %s", + self._serial, + e.cmd, + e.returncode, + (e.stdout or "").strip(), + (e.stderr or "").strip(), + ) + raise ADBError( + f"ADB command failed on {self._serial}: {e.cmd} (exit code {e.returncode})\n" + f"stderr: {(e.stderr or '').strip()}", + return_code=e.returncode, + cmd=e.cmd, + stdout=e.stdout, + stderr=e.stderr, + serial=self._serial, + ) from e + + except subprocess.TimeoutExpired as e: + logger.error( + "ADB (%s): command timed out after %ds: %r\nPartial stdout: %s\nPartial stderr: %s", + self._serial, + timeout, + e.cmd, + e.stdout, + e.stderr, + ) + raise ADBTimeoutError( + + f"ADB command timed out after {timeout} seconds on {self._serial}: {e.cmd}" + ) from e + + except Exception as e: + logger.exception( + "ADB (%s): Unexpected error while running ADB command", self._serial + ) + raise ADBError( + f"Unexpected error on {self._serial}: {str(e)}", + cmd=cmd, + serial=self._serial, + ) from e + + def __repr__(self): + return f"" + diff --git a/src/android_device_manager/adb/exceptions.py b/src/android_device_manager/adb/exceptions.py new file mode 100644 index 0000000..5aa8a08 --- /dev/null +++ b/src/android_device_manager/adb/exceptions.py @@ -0,0 +1,36 @@ +from ..exceptions import AndroidDeviceManagerError + +class ADBTimeoutError(AndroidDeviceManagerError): + """ + Raised when a timeout occurs during an ADB operation. + """ + +class ADBError(AndroidDeviceManagerError): + """ + Raised for any error encountered while running an ADB (Android Debug Bridge) command. + + Attributes: + return_code (Optional[int]): The process return code, if available. + cmd (Optional[Any]): The command that was executed. + stdout (Optional[str]): The standard output from the failed command. + stderr (Optional[str]): The standard error from the failed command. + serial (Optional[str]): The emulator/device serial associated with the error, if relevant. + + Args: + message (str): A descriptive error message. + return_code (Optional[int]): The process return code. + cmd (Optional[Any]): The command executed (as a list or string). + stdout (Optional[str]): Output from stdout. + stderr (Optional[str]): Output from stderr. + serial (Optional[str]): The serial of the target device. + """ + + def __init__( + self, message, return_code=None, cmd=None, stdout=None, stderr=None, serial=None + ): + super().__init__(message) + self.return_code = return_code + self.cmd = cmd + self.stdout = stdout + self.stderr = stderr + self.serial = serial diff --git a/src/android_device_manager/constants.py b/src/android_device_manager/constants.py new file mode 100644 index 0000000..f4b793a --- /dev/null +++ b/src/android_device_manager/constants.py @@ -0,0 +1,45 @@ +from enum import Enum + +DEFAULT_EMULATOR_PORT_START = 5554 +DEFAULT_EMULATOR_PORT_END = 5682 +EMULATOR_PORT_STEP = 2 +DEFAULT_EMULATOR_START_DELAY = 2 + + +class AndroidProp(Enum): + """ + Common Android system properties for use with adb shell getprop. + """ + + ANDROID_VERSION = "ro.build.version.release" + """Android OS version string (e.g. "12", "13")""" + API_LEVEL = "ro.build.version.sdk" + """Android API level (e.g. "34")""" + DEVICE_MODEL = "ro.product.model" + """Device model name (e.g. "Pixel 5")""" + MANUFACTURER = "ro.product.manufacturer" + """Device manufacturer (e.g. "Google")""" + BRAND = "ro.product.brand" + """Device brand (e.g. "Pixel")""" + BOARD = "ro.product.board" + """Device board (e.g. "goldfish_x86_64")""" + BOOTLOADER = "ro.bootloader" + """ Bootloader version""" + FINGERPRINT = "ro.build.fingerprint" + """Full build fingerprint (unique ID for the build)""" + BUILD_ID = "ro.build.display.id" + """Build display ID (e.g. "TQ3A.230805.001")""" + HARDWARE = "ro.hardware" + """Hardware name (e.g. "ranchu")""" + BOOT_COMPLETED = "sys.boot_completed" + """Indicates if system boot completed (should be "1" when ready)""" + BOOTANIM = "init.svc.bootanim" + """Boot animation service status (should be "stopped" when fully booted)""" + FIRST_BOOT_COMPLETED = "sys.bootstat.first_boot_completed" + """First boot completed marker (value "1" when fully booted)""" + SERIAL = "ro.serialno" + """Device serial number""" + + def __str__(self): + return self.value + From 85162f7d3a94b68c3e23a9d523c2d4a5568e4ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 12:05:24 +0200 Subject: [PATCH 04/28] feat(emulator): implement EmulatorManager to start and stop AVD instances --- .../emulator/__init__.py | 7 + src/android_device_manager/emulator/config.py | 71 +++++++++ .../emulator/exceptions.py | 16 ++ .../emulator/manager.py | 144 ++++++++++++++++++ 4 files changed, 238 insertions(+) create mode 100644 src/android_device_manager/emulator/__init__.py create mode 100644 src/android_device_manager/emulator/config.py create mode 100644 src/android_device_manager/emulator/exceptions.py create mode 100644 src/android_device_manager/emulator/manager.py diff --git a/src/android_device_manager/emulator/__init__.py b/src/android_device_manager/emulator/__init__.py new file mode 100644 index 0000000..f7a3de1 --- /dev/null +++ b/src/android_device_manager/emulator/__init__.py @@ -0,0 +1,7 @@ +from .config import EmulatorConfiguration +from .manager import EmulatorManager + +__all__ = [ + "EmulatorManager", + "EmulatorConfiguration", +] \ No newline at end of file diff --git a/src/android_device_manager/emulator/config.py b/src/android_device_manager/emulator/config.py new file mode 100644 index 0000000..5acdacd --- /dev/null +++ b/src/android_device_manager/emulator/config.py @@ -0,0 +1,71 @@ +from dataclasses import dataclass +from typing import Optional, List + + +@dataclass +class EmulatorConfiguration: + """ + Configuration options for the Android emulator. + + This dataclass encapsulates various parameters that can be passed + to the Android emulator at startup. Each field corresponds to a common emulator option. + + Attributes: + no_window (bool): If True, launch the emulator without a window (headless). + no_audio (bool): If True, disable audio in the emulator. + gpu (str): GPU emulation mode (default "auto", e.g., "host", "swiftshader_indirect"). + memory (Optional[int]): Memory (in MB) to allocate for the emulator. + cores (Optional[int]): Number of CPU cores for the emulator. + wipe_data (bool): If True, wipe user data when starting the emulator. + no_snapshot (bool): If True, disable snapshots. + cold_boot (bool): If True, force cold boot (do not load quick-boot snapshot). + netdelay (str): Network delay profile (e.g., "none", "gsm", "edge", "umts"). + netspeed (str): Network speed profile (e.g., "full", "gsm", "edge"). + verbose (bool): If True, enable verbose output. + """ + + no_window: bool = False + no_audio: bool = False + gpu: str = "auto" + memory: Optional[int] = None + cores: Optional[int] = None + wipe_data: bool = False + no_snapshot: bool = False + cold_boot: bool = False + netdelay: str = "none" + netspeed: str = "full" + verbose: bool = False + + def to_args(self) -> List[str]: + """ + Convert the configuration to a list of command-line arguments for the emulator. + + Returns: + List[str]: The list of emulator CLI arguments corresponding to this configuration. + """ + args = [] + + if self.no_window: + args.append("-no-window") + if self.no_audio: + args.append("-no-audio") + if self.gpu != "auto": + args.extend(["-gpu", self.gpu]) + if self.memory: + args.extend(["-memory", str(self.memory)]) + if self.cores: + args.extend(["-cores", str(self.cores)]) + if self.wipe_data: + args.append("-wipe-data") + if self.no_snapshot: + args.append("-no-snapshot") + if self.cold_boot: + args.append("-cold-boot") + if self.netdelay != "none": + args.extend(["-netdelay", self.netdelay]) + if self.netspeed != "full": + args.extend(["-netspeed", self.netspeed]) + if self.verbose: + args.append("-verbose") + + return args diff --git a/src/android_device_manager/emulator/exceptions.py b/src/android_device_manager/emulator/exceptions.py new file mode 100644 index 0000000..efb00f7 --- /dev/null +++ b/src/android_device_manager/emulator/exceptions.py @@ -0,0 +1,16 @@ +from ..exceptions import AndroidDeviceManagerError + + +class EmulatorPortAllocationError(AndroidDeviceManagerError): + """ + Exception raised when the emulator fails to allocate a valid port. + + This error typically occurs when there are no available ports + or when the requested port is already in use. + """ + + +class EmulatorStartError(AndroidDeviceManagerError): + """ + Exception raised when the Android emulator fails to start properly. + """ diff --git a/src/android_device_manager/emulator/manager.py b/src/android_device_manager/emulator/manager.py new file mode 100644 index 0000000..e418261 --- /dev/null +++ b/src/android_device_manager/emulator/manager.py @@ -0,0 +1,144 @@ +import logging +import socket +import subprocess +import time +from typing import Optional + +from ..constants import ( + EMULATOR_PORT_STEP, + DEFAULT_EMULATOR_PORT_END, + DEFAULT_EMULATOR_PORT_START, + DEFAULT_EMULATOR_START_DELAY, +) +from ..emulator.config import EmulatorConfiguration +from ..emulator.exceptions import ( + EmulatorPortAllocationError, + EmulatorStartError, +) +from ..utils.android_sdk import AndroidSDK + +logger = logging.getLogger(__name__) + + +class EmulatorManager: + """ + Manager for starting and stopping Android emulator instances. + """ + + def __init__(self, sdk: AndroidSDK): + """ + Initialize the EmulatorManager. + + Args: + sdk (AndroidSDK): The SDK wrapper containing the path to the emulator. + """ + self._emulator_path = sdk.emulator_path + self._process: Optional[subprocess.Popen] = None + + def start_emulator( + self, avd_name: str, emulator_config: Optional[EmulatorConfiguration] = None + ) -> int: + """ + Start an Android emulator for a given AVD. + + Args: + avd_name (str): The name of the AVD to start. + emulator_config (Optional[EmulatorConfiguration]): Optional configuration for the emulator. + + Returns: + int: The port on which the emulator is running. + + Raises: + EmulatorPortAllocationError: If no free emulator port can be found. + EmulatorStartError: If the emulator fails to start. + """ + logger.info(f"Starting emulator for AVD '{avd_name}'") + + free_port = self._find_free_emulator_port() + if free_port is None: + raise EmulatorPortAllocationError( + f"No free emulator port found to emulate AVD {avd_name}" + ) + + cmd = [str(self._emulator_path), "-avd", avd_name, "-port", str(free_port)] + + if emulator_config: + cmd.extend(emulator_config.to_args()) + + try: + logger.debug(f"Executing command: {' '.join(cmd)}") + self._process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + + time.sleep(DEFAULT_EMULATOR_START_DELAY) + + if self._process.poll() is not None: + stdout, stderr = self._process.communicate() + logger.error(f"Emulator output: {stdout}") + logger.error(f"Emulator error: {stderr}") + raise EmulatorStartError( + f"Emulator '{avd_name}' failed to start. Check the logs for details." + ) + + logger.info( + f"Emulator '{avd_name}' started successfully on port {free_port}" + ) + return free_port + + except subprocess.CalledProcessError as e: + raise EmulatorStartError( + f"Failed to start emulator '{avd_name}': {e.stderr}" + ) + except Exception as e: + raise EmulatorStartError( + f"Unexpected error starting emulator '{avd_name}': {str(e)}" + ) + + def stop_emulator(self) -> None: + """ + Stop the currently running emulator process. + + Terminates the emulator process if it is still running. If the process does not stop + gracefully within 10 seconds, it is forcibly killed. + """ + if self._process and self._process.poll() is None: + logger.info("Stopping emulator process") + self._process.terminate() + try: + self._process.wait(timeout=10) + except subprocess.TimeoutExpired: + logger.warning("Emulator process did not stop gracefully, killing") + self._process.kill() + self._process = None + + @staticmethod + def _find_free_emulator_port( + start: int = DEFAULT_EMULATOR_PORT_START, end: int = DEFAULT_EMULATOR_PORT_END + ) -> Optional[int]: + """ + Find a free even-numbered TCP port suitable for an Android emulator. + + Args: + start (int): Starting port number to search. + end (int): Ending port number to search. + + Returns: + Optional[int]: Free port number if found, else None. + """ + for port in range(start, end + 1, EMULATOR_PORT_STEP): + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("localhost", port)) + logger.debug(f"Found free port: {port}") + return port + except OSError: + logger.debug(f"Port {port} is already in use") + continue + + logger.warning(f"No free ports found in range {start}-{end}") + return None + + def __del__(self): + """Cleanup emulator process on destruction.""" + self.stop_emulator() From c1f00343161409a9650eb14027d9751f83d6d2b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 12:06:52 +0200 Subject: [PATCH 05/28] feat(android-device): add start and stop methods to control emulator lifecycle - start(): boots the emulator, waits for sys.boot_completed, updates internal state - stop(): kills the emulator via ADB and cleans up the emulator process - integrates AdbClient and EmulatorManager to coordinate startup and shutdown --- examples/create_device.py | 24 ++++--- src/android_device_manager/android_device.py | 67 +++++++++++++++++--- src/android_device_manager/avd/__init__.py | 4 +- src/android_device_manager/exceptions.py | 2 + 4 files changed, 79 insertions(+), 18 deletions(-) diff --git a/examples/create_device.py b/examples/create_device.py index a56077e..8a8a97a 100644 --- a/examples/create_device.py +++ b/examples/create_device.py @@ -1,17 +1,25 @@ +import logging + from android_device_manager import AndroidDevice from android_device_manager.avd.config import AVDConfiguration +from android_device_manager.emulator.config import EmulatorConfiguration -device = AndroidDevice( - AVDConfiguration( - name="test_from_lib", - package="system-images;android-36;google_apis;x86_64" +logging.basicConfig(level=logging.INFO) + + +avd_configuration = AVDConfiguration( + name="test_from_lib", package="system-images;android-36;google_apis;x86_64" ) -) -device.create() +emulator_configuration = EmulatorConfiguration() -print(device._avd_manager.list()) +device = AndroidDevice( + avd_config=avd_configuration, + emulator_config=emulator_configuration, +) +device.create() +device.start() +device.stop() device.delete() -print(device._avd_manager.list()) diff --git a/src/android_device_manager/android_device.py b/src/android_device_manager/android_device.py index 184a3c5..e79c440 100644 --- a/src/android_device_manager/android_device.py +++ b/src/android_device_manager/android_device.py @@ -1,16 +1,15 @@ import logging -import subprocess from enum import Enum -from pathlib import Path -from time import sleep -from typing import Optional, List, Union, Dict - +from typing import Optional +from .adb.client import AdbClient +from .adb.exceptions import ADBError from .avd.config import AVDConfiguration from .avd.exceptions import AVDCreationError, AVDDeletionError from .avd.manager import AVDManager -from .exceptions import AndroidDeviceError - +from .emulator.config import EmulatorConfiguration +from .emulator.exceptions import EmulatorStartError +from .emulator.manager import EmulatorManager from .utils.android_sdk import AndroidSDK logger = logging.getLogger(__name__) @@ -34,17 +33,22 @@ class AndroidDeviceState(Enum): STOPPED = "stopped" ERROR = "error" + class AndroidDevice: def __init__( self, avd_config: AVDConfiguration, + emulator_config: Optional[EmulatorConfiguration] = None, android_sdk: Optional[AndroidSDK] = None, ): self._avd_config = avd_config + self._emulator_config = emulator_config self.state = AndroidDeviceState.NOT_CREATED self._android_sdk = android_sdk or AndroidSDK() self._avd_manager = AVDManager(self._android_sdk) + self._emulator_manager = EmulatorManager(self._android_sdk) + self._adb_client: Optional[AdbClient] = None @property def name(self) -> str: @@ -98,4 +102,51 @@ def delete(self): except Exception as e: self.state = AndroidDeviceState.ERROR logger.error(f"Failed to delete AVD '{self.name}': {e}") - raise AVDDeletionError(self.name, f"Failed to delete AVD: {e}") from e \ No newline at end of file + raise AVDDeletionError(self.name, f"Failed to delete AVD: {e}") from e + + def start(self): + """ + Start the emulator for the current AVD and wait for it to boot. + + Raises: + EmulatorStartError: If the emulator fails to start. + ADBError: If there is an error communicating with the device. + TimeoutError: If the emulator does not boot within the allowed time. + """ + logger.info(f"Starting emulator for AVD '{self.name}'...") + try: + port = self._emulator_manager.start_emulator( + avd_name=self.name, + emulator_config=self._emulator_config, + ) + self._adb_client = AdbClient(port, self._android_sdk) + self._adb_client.wait_for_boot() + self.state = AndroidDeviceState.RUNNING + logger.info(f"Emulator for AVD '{self.name}' is running (port {port}).") + except (EmulatorStartError, ADBError, TimeoutError) as e: + self.state = AndroidDeviceState.ERROR + logger.error(f"Failed to start emulator for '{self.name}': {e}") + raise + + def stop(self): + """ + Stop the running emulator and release resources. + + Raises: + Exception: If stopping the emulator fails. + """ + logger.info(f"Stopping emulator for AVD '{self.name}'...") + try: + if self._adb_client: + self._adb_client.kill_emulator() + self._adb_client = None + self._emulator_manager.stop_emulator() + self.state = AndroidDeviceState.STOPPED + logger.info(f"Emulator for AVD '{self.name}' stopped.") + except Exception as e: + self.state = AndroidDeviceState.ERROR + logger.error(f"Failed to stop emulator for '{self.name}': {e}") + raise + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/src/android_device_manager/avd/__init__.py b/src/android_device_manager/avd/__init__.py index b751a1a..d575610 100644 --- a/src/android_device_manager/avd/__init__.py +++ b/src/android_device_manager/avd/__init__.py @@ -1,7 +1,7 @@ -from .manager import AVDManager from .config import AVDConfiguration +from .manager import AVDManager __all__ = [ "AVDManager", "AVDConfiguration", -] \ No newline at end of file +] diff --git a/src/android_device_manager/exceptions.py b/src/android_device_manager/exceptions.py index 91569cb..a5d8126 100644 --- a/src/android_device_manager/exceptions.py +++ b/src/android_device_manager/exceptions.py @@ -6,12 +6,14 @@ class AndroidDeviceManagerError(Exception): allowing users to catch all library-specific errors with a single except block. """ + class AndroidDeviceError(AndroidDeviceManagerError): """ This exception is raised when an error occurs while interacting with an AndroidDevice. """ + class AndroidSDKNotFound(AndroidDeviceManagerError): """ Raised when the Android SDK or a required SDK tool cannot be found. From a8292a2f7cfdbc72a373368d748facbcc615e01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 12:08:13 +0200 Subject: [PATCH 06/28] style: format codebase using `uvx ruff format` --- examples/create_device.py | 5 ++--- src/android_device_manager/adb/client.py | 10 +++------- src/android_device_manager/adb/exceptions.py | 2 ++ src/android_device_manager/android_device.py | 2 +- src/android_device_manager/constants.py | 1 - src/android_device_manager/emulator/__init__.py | 2 +- 6 files changed, 9 insertions(+), 13 deletions(-) diff --git a/examples/create_device.py b/examples/create_device.py index 8a8a97a..a230d26 100644 --- a/examples/create_device.py +++ b/examples/create_device.py @@ -8,8 +8,8 @@ avd_configuration = AVDConfiguration( - name="test_from_lib", package="system-images;android-36;google_apis;x86_64" - ) + name="test_from_lib", package="system-images;android-36;google_apis;x86_64" +) emulator_configuration = EmulatorConfiguration() @@ -22,4 +22,3 @@ device.start() device.stop() device.delete() - diff --git a/src/android_device_manager/adb/client.py b/src/android_device_manager/adb/client.py index 4a79579..6b8e7c8 100644 --- a/src/android_device_manager/adb/client.py +++ b/src/android_device_manager/adb/client.py @@ -1,6 +1,6 @@ import logging import subprocess -from typing import List, Optional, Union +from typing import List, Optional from ..adb.exceptions import ADBError, ADBTimeoutError from ..constants import AndroidProp @@ -14,8 +14,7 @@ class AdbClient: A client for interacting with an Android emulator/device via the Android Debug Bridge (ADB). """ - def __init__(self, emulator_port: int, android_sdk: Optional[AndroidSDK]=None): - + def __init__(self, emulator_port: int, android_sdk: Optional[AndroidSDK] = None): """ Initialize the AdbClient. @@ -73,7 +72,6 @@ def wait_for_boot(self, timeout: int = 120) -> bool: if result_boot_completed == "1": return True raise ADBTimeoutError( - f"Device {self._serial} did not boot within {timeout} seconds." ) @@ -92,7 +90,7 @@ def kill_emulator(self): raise ADBError(f"Failed to kill emulator {self._serial}: {str(e)}") def shell( - self, cmd: list[str], timeout: int = 30, check: bool = True + self, cmd: list[str], timeout: int = 30, check: bool = True ) -> subprocess.CompletedProcess: """ Execute a shell command on the device/emulator via ADB. @@ -170,7 +168,6 @@ def _run_adb_command( e.stderr, ) raise ADBTimeoutError( - f"ADB command timed out after {timeout} seconds on {self._serial}: {e.cmd}" ) from e @@ -186,4 +183,3 @@ def _run_adb_command( def __repr__(self): return f"" - diff --git a/src/android_device_manager/adb/exceptions.py b/src/android_device_manager/adb/exceptions.py index 5aa8a08..2c22f63 100644 --- a/src/android_device_manager/adb/exceptions.py +++ b/src/android_device_manager/adb/exceptions.py @@ -1,10 +1,12 @@ from ..exceptions import AndroidDeviceManagerError + class ADBTimeoutError(AndroidDeviceManagerError): """ Raised when a timeout occurs during an ADB operation. """ + class ADBError(AndroidDeviceManagerError): """ Raised for any error encountered while running an ADB (Android Debug Bridge) command. diff --git a/src/android_device_manager/android_device.py b/src/android_device_manager/android_device.py index e79c440..8fbe3be 100644 --- a/src/android_device_manager/android_device.py +++ b/src/android_device_manager/android_device.py @@ -149,4 +149,4 @@ def stop(self): raise def __repr__(self): - return f"" \ No newline at end of file + return f"" diff --git a/src/android_device_manager/constants.py b/src/android_device_manager/constants.py index f4b793a..e56cc6f 100644 --- a/src/android_device_manager/constants.py +++ b/src/android_device_manager/constants.py @@ -42,4 +42,3 @@ class AndroidProp(Enum): def __str__(self): return self.value - diff --git a/src/android_device_manager/emulator/__init__.py b/src/android_device_manager/emulator/__init__.py index f7a3de1..3bd4d59 100644 --- a/src/android_device_manager/emulator/__init__.py +++ b/src/android_device_manager/emulator/__init__.py @@ -4,4 +4,4 @@ __all__ = [ "EmulatorManager", "EmulatorConfiguration", -] \ No newline at end of file +] From 7760c98b59d03e2377bfd074c2d130bff1805518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 13:08:55 +0200 Subject: [PATCH 07/28] feat(android-device): add context manager support (__enter__, __exit__) --- src/android_device_manager/android_device.py | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/android_device_manager/android_device.py b/src/android_device_manager/android_device.py index 8fbe3be..cf29450 100644 --- a/src/android_device_manager/android_device.py +++ b/src/android_device_manager/android_device.py @@ -148,5 +148,27 @@ def stop(self): logger.error(f"Failed to stop emulator for '{self.name}': {e}") raise + def __enter__(self): + """ + Context manager entry: ensure device is created and started. + """ + if self.state == AndroidDeviceState.NOT_CREATED: + self.create() + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """ + Context manager exit: stop the emulator and (optionally) delete the AVD. + """ + try: + self.stop() + except Exception as e: + logger.warning(f"Error while stopping emulator: {e}") + try: + self.delete() + except Exception as e: + logger.warning(f"Error while deleting AVD: {e}") + def __repr__(self): return f"" From 6f51a23c1439afaac300306337e7fd4cbcfb2b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 13:53:44 +0200 Subject: [PATCH 08/28] test: add unit test suite --- pyproject.toml | 10 + tests/adb/__init__.py | 0 tests/adb/test_client.py | 93 ++++++ tests/avd/__init__.py | 0 tests/avd/test_config.py | 14 + tests/avd/test_manager.py | 142 ++++++++++ tests/conftest.py | 48 ++++ tests/emulator/__init__.py | 0 tests/emulator/test_config.py | 483 ++++++++++++++++++++++++++++++++ tests/emulator/test_manager.py | 145 ++++++++++ tests/test_android_device.py | 132 +++++++++ tests/utils/__init__.py | 0 tests/utils/test_android_sdk.py | 68 +++++ tests/utils/test_sdk_manager.py | 81 ++++++ tests/utils/test_validation.py | 29 ++ uv.lock | 133 +++++++++ 16 files changed, 1378 insertions(+) create mode 100644 tests/adb/__init__.py create mode 100644 tests/adb/test_client.py create mode 100644 tests/avd/__init__.py create mode 100644 tests/avd/test_config.py create mode 100644 tests/avd/test_manager.py create mode 100644 tests/conftest.py create mode 100644 tests/emulator/__init__.py create mode 100644 tests/emulator/test_config.py create mode 100644 tests/emulator/test_manager.py create mode 100644 tests/test_android_device.py create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/test_android_sdk.py create mode 100644 tests/utils/test_sdk_manager.py create mode 100644 tests/utils/test_validation.py diff --git a/pyproject.toml b/pyproject.toml index 413485b..f829d26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,3 +20,13 @@ Issues = "https://github.com/jwoirhaye/android-device-manager-python/issues" [build-system] requires = ["uv_build>=0.8.5,<0.9.0"] build-backend = "uv_build" + +[tool.pytest.ini_options] +markers = [ + "no_fake_sdk: disable the fixture fake_sdk for this test", +] + +[dependency-groups] +dev = [ + "pytest>=8.4.1", +] diff --git a/tests/adb/__init__.py b/tests/adb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/adb/test_client.py b/tests/adb/test_client.py new file mode 100644 index 0000000..360b171 --- /dev/null +++ b/tests/adb/test_client.py @@ -0,0 +1,93 @@ +import subprocess +from unittest import mock + +import pytest + +from android_device_manager.adb.client import AdbClient +from android_device_manager.adb.exceptions import ADBError, ADBTimeoutError + +def test_init_sets_serial_and_adb_path(fake_sdk): + client = AdbClient(5554, fake_sdk) + assert client._port == 5554 + assert client._serial == "emulator-5554" + assert client._adb_path == fake_sdk.adb_path + + +def test_run_adb_command_success(fake_sdk): + client = AdbClient(5554, fake_sdk) + mock_result = mock.Mock() + mock_result.stdout = "success" + mock_result.stderr = "" + mock_result.returncode = 0 + + with mock.patch("subprocess.run", return_value=mock_result) as m_run: + result = client._run_adb_command(["shell", "echo", "ok"]) + m_run.assert_called_once() + assert result.stdout == "success" + + +def test_run_adb_command_called_process_error(fake_sdk): + client = AdbClient(5554, fake_sdk) + e = subprocess.CalledProcessError( + returncode=1, cmd="adb shell", output="out", stderr="fail" + ) + with mock.patch("subprocess.run", side_effect=e): + with pytest.raises(ADBError) as excinfo: + client._run_adb_command(["shell", "fail"]) + assert "ADB command failed" in str(excinfo.value) + + +def test_run_adb_command_timeoutexpired(fake_sdk): + client = AdbClient(5554, fake_sdk) + e = subprocess.TimeoutExpired( + cmd="adb shell", timeout=10, output="partial out", stderr="timeout" + ) + with mock.patch("subprocess.run", side_effect=e): + with pytest.raises(ADBTimeoutError) as excinfo: + client._run_adb_command(["shell", "sleep"]) + assert "timed out" in str(excinfo.value) + + +def test_run_adb_command_unexpected_error(fake_sdk): + client = AdbClient(5554, fake_sdk) + with mock.patch("subprocess.run", side_effect=RuntimeError("BOOM")): + with pytest.raises(ADBError) as excinfo: + client._run_adb_command(["shell", "unknown"]) + assert "Unexpected error" in str(excinfo.value) + + +def test_wait_for_boot_success(fake_sdk): + client = AdbClient(5554, fake_sdk) + result = mock.Mock() + result.stdout = "0" + + with mock.patch.object( + client, + "_run_adb_command", + side_effect=[mock.Mock(stdout="0"), mock.Mock(stdout="1")], + ): + assert client.wait_for_boot(timeout=5) is True + + +def test_wait_for_boot_timeout(fake_sdk): + client = AdbClient(5554, fake_sdk) + + with mock.patch.object( + client, "_run_adb_command", return_value=mock.Mock(stdout="0") + ): + with pytest.raises(ADBTimeoutError): + client.wait_for_boot(timeout=1) + + +def test_kill_emulator_success(fake_sdk): + client = AdbClient(5554, fake_sdk) + with mock.patch.object(client, "_run_adb_command", return_value=mock.Mock()): + client.kill_emulator() + + +def test_kill_emulator_adberror(fake_sdk): + client = AdbClient(5554, fake_sdk) + with mock.patch.object(client, "_run_adb_command", side_effect=ADBError("fail")): + with pytest.raises(ADBError) as excinfo: + client.kill_emulator() + assert "Failed to kill emulator" in str(excinfo.value) diff --git a/tests/avd/__init__.py b/tests/avd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/avd/test_config.py b/tests/avd/test_config.py new file mode 100644 index 0000000..57bab13 --- /dev/null +++ b/tests/avd/test_config.py @@ -0,0 +1,14 @@ +from android_device_manager.avd import AVDConfiguration + + +def test_init(): + """Test creation and attribute assignment of AVDConfiguration.""" + name = "test_avd" + package = "system-images;android-36;google_apis;x86_64" + + config = AVDConfiguration(name, package) + + assert config.name is not None + assert config.name == name + assert config.package is not None + assert config.package == package diff --git a/tests/avd/test_manager.py b/tests/avd/test_manager.py new file mode 100644 index 0000000..fc34f43 --- /dev/null +++ b/tests/avd/test_manager.py @@ -0,0 +1,142 @@ +from unittest import mock + +import pytest + +from android_device_manager.avd.config import AVDConfiguration +from android_device_manager.avd.exceptions import AVDCreationError, AVDDeletionError +from android_device_manager.avd.manager import AVDManager + + +@pytest.fixture +def fake_sdk(): + return mock.Mock() + + +@pytest.fixture +def fake_sdk_manager(): + return mock.Mock() + + +@pytest.fixture +def avd_manager(fake_sdk, fake_sdk_manager, monkeypatch): + monkeypatch.setattr( + "android_device_manager.avd.manager.SDKManager", lambda sdk: fake_sdk_manager + ) + monkeypatch.setattr( + "android_device_manager.avd.manager.is_valid_avd_name", lambda name: True + ) + + with mock.patch.object(AVDManager, "_run_avd_command"): + yield AVDManager(fake_sdk) + + +def test_create_success(avd_manager, fake_sdk_manager): + fake_sdk_manager.is_system_image_installed.return_value = True + avd_manager.exist = mock.Mock(return_value=False) + + avd_manager._run_avd_command.return_value = mock.Mock( + returncode=0, stderr="", stdout="" + ) + config = AVDConfiguration( + name="MyAVD", package="system-images;android-34;google_apis;x86_64" + ) + assert avd_manager.create(config) is True + + +def test_create_invalid_name(avd_manager, monkeypatch): + monkeypatch.setattr( + "android_device_manager.avd.manager.is_valid_avd_name", lambda name: False + ) + config = AVDConfiguration( + name="bad name", package="system-images;android-34;google_apis;x86_64" + ) + with pytest.raises(AVDCreationError) as e: + avd_manager.create(config) + assert "Invalid AVD name" in str(e.value) + + +def test_create_existing_avd(avd_manager, fake_sdk_manager): + avd_manager.exist = mock.Mock(return_value=True) + config = AVDConfiguration( + name="MyAVD", package="system-images;android-34;google_apis;x86_64" + ) + with pytest.raises(AVDCreationError) as e: + avd_manager.create(config) + assert "already exists" in str(e.value) + + +def test_create_image_not_installed(avd_manager, fake_sdk_manager): + avd_manager.exist = mock.Mock(return_value=False) + fake_sdk_manager.is_system_image_installed.return_value = False + config = AVDConfiguration( + name="MyAVD", package="system-images;android-34;google_apis;x86_64" + ) + with pytest.raises(AVDCreationError) as e: + avd_manager.create(config) + assert "is not available" in str(e.value) + + +def test_create_run_error(avd_manager, fake_sdk_manager): + avd_manager.exist = mock.Mock(return_value=False) + fake_sdk_manager.is_system_image_installed.return_value = True + avd_manager._run_avd_command.return_value = mock.Mock( + returncode=1, stderr="fail", stdout="" + ) + config = AVDConfiguration( + name="MyAVD", package="system-images;android-34;google_apis;x86_64" + ) + with pytest.raises(AVDCreationError) as e: + avd_manager.create(config) + assert "Failed to create AVD" in str(e.value) + + +def test_create_timeout(avd_manager, fake_sdk_manager): + avd_manager.exist = mock.Mock(return_value=False) + fake_sdk_manager.is_system_image_installed.return_value = True + avd_manager._run_avd_command.side_effect = Exception("timeout") + config = AVDConfiguration( + name="MyAVD", package="system-images;android-34;google_apis;x86_64" + ) + with pytest.raises(AVDCreationError): + avd_manager.create(config) + + +def test_delete_success(avd_manager): + avd_manager.exist = mock.Mock(return_value=True) + avd_manager._run_avd_command.return_value = mock.Mock( + returncode=0, stderr="", stdout="" + ) + assert avd_manager.delete("MyAVD") is True + + +def test_delete_avd_not_exists(avd_manager): + avd_manager.exist = mock.Mock(return_value=False) + + assert avd_manager.delete("NotExist") is True + + +def test_delete_run_error(avd_manager): + avd_manager.exist = mock.Mock(return_value=True) + avd_manager._run_avd_command.return_value = mock.Mock( + returncode=1, stderr="fail", stdout="" + ) + with pytest.raises(AVDDeletionError): + avd_manager.delete("MyAVD") + + +def test_list_and_exist(avd_manager): + avd_manager._run_avd_command.return_value = mock.Mock(stdout="foo\nbar\nbaz\n") + result = avd_manager.list() + assert result == ["foo", "bar", "baz"] + avd_manager._run_avd_command.return_value = mock.Mock(stdout="foo\nbar\n") + assert avd_manager.exist("foo") is True + assert avd_manager.exist("notfound") is False + + +def test_parse_avd_list_static(): + from android_device_manager.avd.manager import AVDManager + + output = "foo\nbar\nbaz\n" + assert AVDManager._parse_avd_list(output) == ["foo", "bar", "baz"] + output = "\n \n" + assert AVDManager._parse_avd_list(output) == [] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..833f275 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,48 @@ +from unittest import mock + +import pytest + +from android_device_manager.utils.android_sdk import AndroidSDK +from android_device_manager.utils.sdk_manager import SDKManager + + +@pytest.fixture(autouse=True) +def fake_sdk_path(monkeypatch, tmp_path, request): + """ + Fixture that creates a fake Android SDK for tests. + Returns the path to the fake SDK. + + Runs automatically for all tests unless disabled + with @pytest.mark.no_fake_sdk. + """ + if "no_fake_sdk" in request.keywords: + yield None + return + + sdk_root = tmp_path / "android-sdk" + (sdk_root / "cmdline-tools" / "latest" / "bin").mkdir(parents=True) + (sdk_root / "platform-tools").mkdir(parents=True) + (sdk_root / "emulator").mkdir(parents=True) + + for tool in ("avdmanager", "sdkmanager"): + (sdk_root / "cmdline-tools" / "latest" / "bin" / tool).touch() + (sdk_root / "platform-tools" / "adb").touch() + (sdk_root / "emulator" / "emulator").touch() + + monkeypatch.setenv("ANDROID_SDK_ROOT", str(sdk_root)) + monkeypatch.setattr( + "android_device_manager.utils.android_sdk.AndroidSDK._find_sdk_path", + lambda self: sdk_root, + ) + + yield sdk_root + +@pytest.fixture +def fake_sdk(fake_sdk_path): + return AndroidSDK(fake_sdk_path) + + +@pytest.fixture +def manager(fake_sdk): + return SDKManager(fake_sdk) + diff --git a/tests/emulator/__init__.py b/tests/emulator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/emulator/test_config.py b/tests/emulator/test_config.py new file mode 100644 index 0000000..3f70e7e --- /dev/null +++ b/tests/emulator/test_config.py @@ -0,0 +1,483 @@ +import pytest + +from android_device_manager.emulator.config import EmulatorConfiguration + + +class TestEmulatorConfiguration: + """Test suite for EmulatorConfiguration dataclass.""" + + def test_default_initialization(self): + """Test EmulatorConfiguration with default values.""" + config = EmulatorConfiguration() + + assert config.no_window is False + assert config.no_audio is False + assert config.gpu == "auto" + assert config.memory is None + assert config.cores is None + assert config.wipe_data is False + assert config.no_snapshot is False + assert config.cold_boot is False + assert config.netdelay == "none" + assert config.netspeed == "full" + assert config.verbose is False + + def test_custom_initialization(self): + """Test EmulatorConfiguration with custom values.""" + config = EmulatorConfiguration( + no_window=True, + no_audio=True, + gpu="host", + memory=2048, + cores=4, + wipe_data=True, + no_snapshot=True, + cold_boot=True, + netdelay="gsm", + netspeed="edge", + verbose=True, + ) + + assert config.no_window is True + assert config.no_audio is True + assert config.gpu == "host" + assert config.memory == 2048 + assert config.cores == 4 + assert config.wipe_data is True + assert config.no_snapshot is True + assert config.cold_boot is True + assert config.netdelay == "gsm" + assert config.netspeed == "edge" + assert config.verbose is True + + def test_partial_initialization(self): + """Test EmulatorConfiguration with some custom values.""" + config = EmulatorConfiguration(no_window=True, memory=1024, netdelay="umts") + + + assert config.no_window is True + assert config.memory == 1024 + assert config.netdelay == "umts" + + + assert config.no_audio is False + assert config.gpu == "auto" + assert config.cores is None + assert config.netspeed == "full" + + def test_to_args_default_config(self): + """Test to_args() with default configuration.""" + config = EmulatorConfiguration() + args = config.to_args() + + + assert args == [] + + def test_to_args_all_boolean_flags(self): + """Test to_args() with all boolean flags enabled.""" + config = EmulatorConfiguration( + no_window=True, + no_audio=True, + wipe_data=True, + no_snapshot=True, + cold_boot=True, + verbose=True, + ) + args = config.to_args() + + expected_args = [ + "-no-window", + "-no-audio", + "-wipe-data", + "-no-snapshot", + "-cold-boot", + "-verbose", + ] + + + for arg in expected_args: + assert arg in args + + + assert len(args) == len(expected_args) + + def test_to_args_with_values(self): + """Test to_args() with parameters that take values.""" + config = EmulatorConfiguration( + gpu="host", memory=2048, cores=4, netdelay="gsm", netspeed="edge" + ) + args = config.to_args() + + expected_pairs = [ + ("-gpu", "host"), + ("-memory", "2048"), + ("-cores", "4"), + ("-netdelay", "gsm"), + ("-netspeed", "edge"), + ] + + + arg_pairs = [(args[i], args[i + 1]) for i in range(0, len(args), 2)] + + assert len(arg_pairs) == len(expected_pairs) + for pair in expected_pairs: + assert pair in arg_pairs + + def test_to_args_mixed_configuration(self): + """Test to_args() with mixed boolean flags and value parameters.""" + config = EmulatorConfiguration( + no_window=True, + gpu="swiftshader_indirect", + memory=1024, + wipe_data=True, + netdelay="umts", + verbose=True, + ) + args = config.to_args() + + + assert "-no-window" in args + assert "-wipe-data" in args + assert "-verbose" in args + assert "-gpu" in args + assert "swiftshader_indirect" in args + assert "-memory" in args + assert "1024" in args + assert "-netdelay" in args + assert "umts" in args + + def test_to_args_gpu_auto_not_included(self): + """Test that GPU=auto (default) is not included in args.""" + config = EmulatorConfiguration(gpu="auto") + args = config.to_args() + + assert "-gpu" not in args + assert "auto" not in args + assert args == [] + + def test_to_args_gpu_custom_included(self): + """Test that custom GPU value is included in args.""" + config = EmulatorConfiguration(gpu="host") + args = config.to_args() + + assert "-gpu" in args + assert "host" in args + + def test_to_args_netdelay_none_not_included(self): + """Test that netdelay=none (default) is not included in args.""" + config = EmulatorConfiguration(netdelay="none") + args = config.to_args() + + assert "-netdelay" not in args + assert "none" not in args + assert args == [] + + def test_to_args_netspeed_full_not_included(self): + """Test that netspeed=full (default) is not included in args.""" + config = EmulatorConfiguration(netspeed="full") + args = config.to_args() + + assert "-netspeed" not in args + assert "full" not in args + assert args == [] + + def test_to_args_none_values_not_included(self): + """Test that None values are not included in args.""" + config = EmulatorConfiguration(memory=None, cores=None) + args = config.to_args() + + assert "-memory" not in args + assert "-cores" not in args + assert args == [] + + def test_immutability_after_creation(self): + """Test that configuration can be modified after creation (dataclass is mutable).""" + config = EmulatorConfiguration() + + + config.no_window = True + config.memory = 1024 + + assert config.no_window is True + assert config.memory == 1024 + + def test_equality_comparison(self): + """Test equality comparison between configurations.""" + config1 = EmulatorConfiguration(no_window=True, memory=1024) + config2 = EmulatorConfiguration(no_window=True, memory=1024) + config3 = EmulatorConfiguration(no_window=False, memory=1024) + + assert config1 == config2 + assert config1 != config3 + assert config2 != config3 + + def test_string_representation(self): + """Test string representation of configuration.""" + config = EmulatorConfiguration(no_window=True, memory=1024) + repr_str = repr(config) + + assert "EmulatorConfiguration" in repr_str + assert "no_window=True" in repr_str + assert "memory=1024" in repr_str + + def test_dataclass_fields_access(self): + """Test that we can access dataclass fields programmatically.""" + from dataclasses import fields + + config = EmulatorConfiguration() + field_names = [field.name for field in fields(config)] + + expected_fields = [ + "no_window", + "no_audio", + "gpu", + "memory", + "cores", + "wipe_data", + "no_snapshot", + "cold_boot", + "netdelay", + "netspeed", + "verbose", + ] + + assert len(field_names) == len(expected_fields) + for field_name in expected_fields: + assert field_name in field_names + + +class TestEmulatorConfigurationValidation: + """Test suite for EmulatorConfiguration validation and edge cases.""" + + def test_memory_negative_value(self): + """Test configuration with negative memory value.""" + + config = EmulatorConfiguration(memory=-100) + args = config.to_args() + + assert "-memory" in args + assert "-100" in args + + def test_cores_negative_value(self): + """Test configuration with negative cores value.""" + config = EmulatorConfiguration(cores=-1) + args = config.to_args() + + assert "-cores" in args + assert "-1" in args + + def test_large_memory_value(self): + """Test configuration with very large memory value.""" + config = EmulatorConfiguration(memory=999999) + args = config.to_args() + + assert "-memory" in args + assert "999999" in args + + def test_empty_string_values(self): + """Test configuration with empty string values.""" + config = EmulatorConfiguration(gpu="", netdelay="", netspeed="") + args = config.to_args() + + + assert "-gpu" in args + assert "" in args + assert "-netdelay" in args + assert "-netspeed" in args + + def test_special_characters_in_strings(self): + """Test configuration with special characters in string values.""" + config = EmulatorConfiguration( + gpu="test-gpu_with.special/chars", + netdelay="custom:delay", + netspeed="custom@speed", + ) + args = config.to_args() + + assert "-gpu" in args + assert "test-gpu_with.special/chars" in args + assert "-netdelay" in args + assert "custom:delay" in args + assert "-netspeed" in args + assert "custom@speed" in args + + +@pytest.mark.parametrize( + "field_name,field_value,expected_in_args", + [ + ("no_window", True, "-no-window"), + ("no_audio", True, "-no-audio"), + ("wipe_data", True, "-wipe-data"), + ("no_snapshot", True, "-no-snapshot"), + ("cold_boot", True, "-cold-boot"), + ("verbose", True, "-verbose"), + ], +) +def test_boolean_flags_parametrized(field_name, field_value, expected_in_args): + """Test that boolean flags are correctly converted to arguments.""" + kwargs = {field_name: field_value} + config = EmulatorConfiguration(**kwargs) + args = config.to_args() + + assert expected_in_args in args + + +@pytest.mark.parametrize( + "field_name,field_value,expected_flag,expected_value", + [ + ("gpu", "host", "-gpu", "host"), + ("gpu", "swiftshader_indirect", "-gpu", "swiftshader_indirect"), + ("memory", 1024, "-memory", "1024"), + ("memory", 2048, "-memory", "2048"), + ("cores", 2, "-cores", "2"), + ("cores", 8, "-cores", "8"), + ("netdelay", "gsm", "-netdelay", "gsm"), + ("netdelay", "edge", "-netdelay", "edge"), + ("netspeed", "gsm", "-netspeed", "gsm"), + ("netspeed", "edge", "-netspeed", "edge"), + ], +) +def test_value_parameters_parametrized( + field_name, field_value, expected_flag, expected_value +): + """Test that value parameters are correctly converted to flag-value pairs.""" + kwargs = {field_name: field_value} + config = EmulatorConfiguration(**kwargs) + args = config.to_args() + + assert expected_flag in args + assert expected_value in args + + + flag_index = args.index(expected_flag) + assert args[flag_index + 1] == expected_value + + +@pytest.mark.parametrize( + "field_name,default_value", + [ + ("gpu", "auto"), + ("netdelay", "none"), + ("netspeed", "full"), + ], +) +def test_default_values_not_in_args(field_name, default_value): + """Test that default values are not included in args.""" + kwargs = {field_name: default_value} + config = EmulatorConfiguration(**kwargs) + args = config.to_args() + + + assert args == [] + + +@pytest.mark.parametrize("memory_value", [1, 512, 1024, 2048, 4096, 8192]) +def test_memory_values_range(memory_value): + """Test various memory values.""" + config = EmulatorConfiguration(memory=memory_value) + args = config.to_args() + + assert "-memory" in args + assert str(memory_value) in args + + +@pytest.mark.parametrize("cores_value", [1, 2, 4, 8, 16]) +def test_cores_values_range(cores_value): + """Test various core count values.""" + config = EmulatorConfiguration(cores=cores_value) + args = config.to_args() + + assert "-cores" in args + assert str(cores_value) in args + + +class TestEmulatorConfigurationIntegration: + """Integration tests for EmulatorConfiguration with realistic scenarios.""" + + def test_headless_configuration(self): + """Test typical headless emulator configuration.""" + config = EmulatorConfiguration( + no_window=True, no_audio=True, gpu="swiftshader_indirect", memory=2048 + ) + args = config.to_args() + + expected_elements = [ + "-no-window", + "-no-audio", + "-gpu", + "swiftshader_indirect", + "-memory", + "2048", + ] + + for element in expected_elements: + assert element in args + + def test_development_configuration(self): + """Test typical development emulator configuration.""" + config = EmulatorConfiguration(gpu="host", memory=4096, cores=4, verbose=True) + args = config.to_args() + + expected_elements = [ + "-gpu", + "host", + "-memory", + "4096", + "-cores", + "4", + "-verbose", + ] + + for element in expected_elements: + assert element in args + + def test_testing_configuration(self): + """Test configuration suitable for automated testing.""" + config = EmulatorConfiguration( + no_window=True, + no_audio=True, + wipe_data=True, + no_snapshot=True, + cold_boot=True, + netspeed="full", + netdelay="none", + ) + args = config.to_args() + + + expected_flags = [ + "-no-window", + "-no-audio", + "-wipe-data", + "-no-snapshot", + "-cold-boot", + ] + unexpected_flags = ["-netspeed", "-netdelay"] + + for flag in expected_flags: + assert flag in args + + for flag in unexpected_flags: + assert flag not in args + + def test_performance_configuration(self): + """Test high-performance emulator configuration.""" + config = EmulatorConfiguration( + gpu="host", memory=8192, cores=8, netspeed="full", netdelay="none" + ) + args = config.to_args() + + + assert "-gpu" in args and "host" in args + assert "-memory" in args and "8192" in args + assert "-cores" in args and "8" in args + assert "-netspeed" not in args + assert "-netdelay" not in args + + def test_slow_network_simulation(self): + """Test configuration for simulating slow network conditions.""" + config = EmulatorConfiguration(netspeed="gsm", netdelay="gsm") + args = config.to_args() + + assert "-netspeed" in args and "gsm" in args + assert "-netdelay" in args and "gsm" in args diff --git a/tests/emulator/test_manager.py b/tests/emulator/test_manager.py new file mode 100644 index 0000000..fedfc16 --- /dev/null +++ b/tests/emulator/test_manager.py @@ -0,0 +1,145 @@ +import subprocess +from unittest import mock + +import pytest + +from android_device_manager.emulator.config import EmulatorConfiguration +from android_device_manager.emulator.exceptions import ( + EmulatorPortAllocationError, + EmulatorStartError, +) +from android_device_manager.emulator.manager import EmulatorManager + + +@pytest.fixture +def fake_sdk(): + sdk = mock.Mock() + sdk.emulator_path = "/fake/emulator" + return sdk + + +@pytest.fixture +def manager(fake_sdk): + return EmulatorManager(fake_sdk) + + +def test_start_emulator_success(manager, monkeypatch): + monkeypatch.setattr(manager, "_find_free_emulator_port", lambda *a, **kw: 5554) + popen_mock = mock.Mock() + popen_mock.poll.return_value = None + with mock.patch("subprocess.Popen", return_value=popen_mock): + with mock.patch("time.sleep"): + port = manager.start_emulator("TestAVD") + assert port == 5554 + assert manager._process is popen_mock + + +def test_start_emulator_port_allocation_error(manager, monkeypatch): + monkeypatch.setattr(manager, "_find_free_emulator_port", lambda *a, **kw: None) + with pytest.raises(EmulatorPortAllocationError): + manager.start_emulator("TestAVD") + + +def test_start_emulator_failure_on_launch(manager, monkeypatch): + monkeypatch.setattr(manager, "_find_free_emulator_port", lambda *a, **kw: 5554) + popen_mock = mock.Mock() + + popen_mock.poll.return_value = 1 + popen_mock.communicate.return_value = ("stdout output", "stderr output") + with mock.patch("subprocess.Popen", return_value=popen_mock): + with mock.patch("time.sleep"): + with pytest.raises(EmulatorStartError) as excinfo: + manager.start_emulator("TestAVD") + assert "failed to start" in str(excinfo.value).lower() + + +def test_start_emulator_other_exception(manager, monkeypatch): + monkeypatch.setattr(manager, "_find_free_emulator_port", lambda *a, **kw: 5554) + with mock.patch("subprocess.Popen", side_effect=RuntimeError("fail")): + with pytest.raises(EmulatorStartError) as excinfo: + manager.start_emulator("TestAVD") + assert "unexpected error" in str(excinfo.value).lower() + + +def test_start_emulator_with_config(manager, monkeypatch): + monkeypatch.setattr(manager, "_find_free_emulator_port", lambda *a, **kw: 5560) + popen_mock = mock.Mock() + popen_mock.poll.return_value = None + config = EmulatorConfiguration(no_window=True, gpu="swiftshader_indirect") + with mock.patch("subprocess.Popen", return_value=popen_mock) as m_popen: + with mock.patch("time.sleep"): + port = manager.start_emulator("TestAVD", emulator_config=config) + + args = m_popen.call_args[0][0] + assert "-no-window" in args + assert "-gpu" in args and "swiftshader_indirect" in args + assert port == 5560 + + +def test_stop_emulator_graceful(manager): + process = mock.Mock() + process.poll.return_value = None + manager._process = process + manager.stop_emulator() + process.terminate.assert_called_once() + process.wait.assert_called_once_with(timeout=10) + assert manager._process is None + + +def test_stop_emulator_kill_on_timeout(manager): + process = mock.Mock() + process.poll.return_value = None + process.wait.side_effect = subprocess.TimeoutExpired(cmd="fake", timeout=10) + manager._process = process + manager.stop_emulator() + process.terminate.assert_called_once() + process.kill.assert_called_once() + assert manager._process is None + + +def test_stop_emulator_nothing_to_do(manager): + manager._process = None + + manager.stop_emulator() + + +def test_find_free_emulator_port(monkeypatch): + from android_device_manager.emulator.manager import EmulatorManager + + called = {} + + def fake_bind(addr): + called["called"] = True + + class FakeSocket: + def __enter__(self): + return self + + def __exit__(self, *a): + pass + + def bind(self, addr): + fake_bind(addr) + + monkeypatch.setattr("socket.socket", lambda *a, **kw: FakeSocket()) + port = EmulatorManager._find_free_emulator_port(5554, 5556) + assert port == 5554 + assert called["called"] + + +def test_find_free_emulator_port_none(monkeypatch): + from android_device_manager.emulator.manager import EmulatorManager + + class FakeSocket: + def __enter__(self): + return self + + def __exit__(self, *a): + pass + + def bind(self, addr): + raise OSError("fail") + + monkeypatch.setattr("socket.socket", lambda *a, **kw: FakeSocket()) + port = EmulatorManager._find_free_emulator_port(5554, 5556) + assert port is None diff --git a/tests/test_android_device.py b/tests/test_android_device.py new file mode 100644 index 0000000..b0cd4bc --- /dev/null +++ b/tests/test_android_device.py @@ -0,0 +1,132 @@ +from unittest import mock +import pytest + +from android_device_manager.adb.exceptions import ADBError +from android_device_manager.android_device import AndroidDevice, AndroidDeviceState +from android_device_manager.avd.config import AVDConfiguration +from android_device_manager.avd.exceptions import AVDCreationError, AVDDeletionError +from android_device_manager.emulator.config import EmulatorConfiguration +from android_device_manager.emulator.exceptions import EmulatorStartError + + +@pytest.fixture +def avd_config(): + return AVDConfiguration( + name="TestAVD", package="system-images;android-34;google_apis;x86_64" + ) + + +@pytest.fixture +def emulator_config(): + return EmulatorConfiguration(no_window=True) + + +@pytest.fixture +def device(avd_config, emulator_config, fake_sdk_path): + """ + Fixture returning an AndroidDevice with all managers mocked. + Ensures SDK path resolution is patched to avoid AndroidSDKNotFound. + """ + with ( + mock.patch("android_device_manager.android_device.AVDManager"), + mock.patch("android_device_manager.android_device.EmulatorManager"), + mock.patch("android_device_manager.android_device.AdbClient"), + ): + yield AndroidDevice(avd_config, emulator_config) + + +def test_device_init_state(device): + assert device.state == AndroidDeviceState.NOT_CREATED + + +def test_create_avd_new(device): + device._avd_manager.exist.return_value = False + device._avd_manager.create.return_value = True + device.create() + device._avd_manager.create.assert_called_once() + assert device.state == AndroidDeviceState.CREATED + + +def test_create_avd_exists(device): + device._avd_manager.exist.return_value = True + device.create() + device._avd_manager.create.assert_not_called() + assert device.state == AndroidDeviceState.CREATED + + +def test_create_avd_error(device): + device._avd_manager.exist.return_value = False + device._avd_manager.create.side_effect = AVDCreationError("TestAVD", "fail") + with pytest.raises(AVDCreationError): + device.create() + assert device.state == AndroidDeviceState.ERROR + + +def test_start_success(device): + device._emulator_manager.start_emulator.return_value = 5554 + device._adb_client = mock.Mock() + + with mock.patch("android_device_manager.android_device.AdbClient") as MockAdbClient: + MockAdbClient.return_value.wait_for_boot.return_value = True + device.start() + assert device.state == AndroidDeviceState.RUNNING + + +@pytest.mark.parametrize( + "exc", [EmulatorStartError("fail"), ADBError("fail"), TimeoutError()] +) +def test_start_error(device, exc): + device._emulator_manager.start_emulator.side_effect = exc + with pytest.raises(type(exc)): + device.start() + assert device.state == AndroidDeviceState.ERROR + + +def test_stop_success(device): + device._adb_client = mock.Mock() + device.state = AndroidDeviceState.RUNNING + device.stop() + assert device._adb_client is None + assert device.state == AndroidDeviceState.STOPPED + + +def test_stop_error(device): + device._adb_client = mock.Mock() + device._adb_client.kill_emulator.side_effect = Exception("fail") + with pytest.raises(Exception): + device.stop() + assert device.state == AndroidDeviceState.ERROR + + +def test_delete_success(device): + device._avd_manager.delete.return_value = True + device.delete() + device._avd_manager.delete.assert_called_once() + assert device.state == AndroidDeviceState.NOT_CREATED + + +def test_delete_error(device): + device._avd_manager.delete.side_effect = Exception("fail") + with pytest.raises(AVDDeletionError): + device.delete() + assert device.state == AndroidDeviceState.ERROR + + +def test_context_manager_success(device): + device._avd_manager.exist.return_value = False + device._avd_manager.create.return_value = True + device._emulator_manager.start_emulator.return_value = 5554 + with mock.patch("android_device_manager.android_device.AdbClient") as MockAdbClient: + MockAdbClient.return_value.wait_for_boot.return_value = True + with ( + mock.patch.object(device, "stop") as m_stop, + mock.patch.object(device, "delete") as m_delete, + ): + with device as d: + assert d is device + assert device.state in ( + AndroidDeviceState.CREATED, + AndroidDeviceState.RUNNING, + ) + m_stop.assert_called() + m_delete.assert_called() diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/test_android_sdk.py b/tests/utils/test_android_sdk.py new file mode 100644 index 0000000..d71d4f3 --- /dev/null +++ b/tests/utils/test_android_sdk.py @@ -0,0 +1,68 @@ +from pathlib import Path +from unittest import mock + +import pytest + +from android_device_manager.exceptions import AndroidSDKNotFound +from android_device_manager.utils.android_sdk import AndroidSDK + +def test_sdk_manual_path(fake_sdk_path): + """Test initialization with an explicit, complete SDK path.""" + sdk = AndroidSDK(fake_sdk_path) + assert sdk.sdk_path == fake_sdk_path + assert sdk.is_valid() + assert sdk.adb_path.exists() + assert sdk.avdmanager_path.exists() + assert sdk.emulator_path.exists() + assert sdk.sdkmanager_path.exists() + + +def test_sdk_invalid_path(tmp_path): + """Test initialization with a path that lacks required tools.""" + bad_path = tmp_path / "empty-sdk" + bad_path.mkdir() + with pytest.raises(AndroidSDKNotFound): + AndroidSDK(bad_path) + + +def test_sdk_auto_env(fake_sdk_path): + """Test auto-detection via environment variable.""" + sdk = AndroidSDK() + assert sdk.is_valid() + assert sdk.sdk_path == fake_sdk_path + + +def test_sdk_auto_env_not_found(monkeypatch): + """Test auto-detection when nothing is found.""" + monkeypatch.delenv("ANDROID_SDK_ROOT", raising=False) + monkeypatch.delenv("ANDROID_HOME", raising=False) + + with mock.patch( + "android_device_manager.utils.android_sdk.Path.exists", return_value=False + ): + with pytest.raises(AndroidSDKNotFound): + AndroidSDK() + + +@pytest.mark.no_fake_sdk +def test_sdk_path_candidates(tmp_path, monkeypatch): + """Test detection via default candidates (HOME/Android/Sdk, etc.).""" + home_sdk = tmp_path / "Android" / "Sdk" + (home_sdk / "cmdline-tools" / "latest" / "bin").mkdir(parents=True) + (home_sdk / "platform-tools").mkdir(parents=True) + (home_sdk / "emulator").mkdir(parents=True) + (home_sdk / "cmdline-tools" / "latest" / "bin" / "avdmanager").touch() + (home_sdk / "cmdline-tools" / "latest" / "bin" / "sdkmanager").touch() + (home_sdk / "platform-tools" / "adb").touch() + (home_sdk / "emulator" / "emulator").touch() + + monkeypatch.setattr(Path, "home", lambda: tmp_path) + + monkeypatch.setattr( + "android_device_manager.utils.android_sdk.AndroidSDK._find_sdk_path", + lambda self: home_sdk, + ) + + sdk = AndroidSDK() + assert sdk.sdk_path == home_sdk + assert sdk.is_valid() diff --git a/tests/utils/test_sdk_manager.py b/tests/utils/test_sdk_manager.py new file mode 100644 index 0000000..ddd0adf --- /dev/null +++ b/tests/utils/test_sdk_manager.py @@ -0,0 +1,81 @@ +import subprocess +from unittest import mock + +import pytest + +from android_device_manager.exceptions import SDKManagerError +from android_device_manager.utils.sdk_manager import SDKManager + + + + +def make_sdkmanager_list_output(installed=None, available=None): + installed = installed or [] + available = available or [] + lines = ( + [ + "Installed packages:", + "-------------------", + "Path | Version | Description | Location", + "-------|---------|-------------|----------", + ] + + [f"{pkg} | 1.0 | desc | /some/path" for pkg in installed] + + ["", "Available Packages:", "-------------------"] + + available + ) + return "\n".join(lines) + + +def test_is_system_image_installed_true(manager): + output = make_sdkmanager_list_output( + installed=["system-images;android-34;google_apis;x86_64"] + ) + proc = mock.Mock() + proc.stdout = output + with mock.patch("subprocess.run", return_value=proc): + assert ( + manager.is_system_image_installed( + "system-images;android-34;google_apis;x86_64" + ) + is True + ) + + +def test_is_system_image_installed_false(manager): + output = make_sdkmanager_list_output(installed=["foo", "bar"]) + proc = mock.Mock() + proc.stdout = output + with mock.patch("subprocess.run", return_value=proc): + assert manager.is_system_image_installed("not-present") is False + + +def test_is_system_image_installed_section_skips(manager): + + output = make_sdkmanager_list_output( + installed=[], available=["system-images;android-34;google_apis;x86_64"] + ) + proc = mock.Mock() + proc.stdout = output + with mock.patch("subprocess.run", return_value=proc): + assert ( + manager.is_system_image_installed( + "system-images;android-34;google_apis;x86_64" + ) + is False + ) + + +def test_is_system_image_installed_calledprocesserror(manager): + with mock.patch( + "subprocess.run", side_effect=subprocess.CalledProcessError(1, ["foo"]) + ): + with pytest.raises(SDKManagerError) as excinfo: + manager.is_system_image_installed("foo") + assert "Failed to list SDK packages" in str(excinfo.value) + + +def test_is_system_image_installed_generic_error(manager): + with mock.patch("subprocess.run", side_effect=RuntimeError("fail")): + with pytest.raises(SDKManagerError) as excinfo: + manager.is_system_image_installed("foo") + assert "Error checking system image installation" in str(excinfo.value) diff --git a/tests/utils/test_validation.py b/tests/utils/test_validation.py new file mode 100644 index 0000000..67d3150 --- /dev/null +++ b/tests/utils/test_validation.py @@ -0,0 +1,29 @@ +import pytest + +from android_device_manager.utils.validation import is_valid_avd_name + + +@pytest.mark.parametrize( + "name,expected", + [ + ("Pixel_6", True), + ("A_1-2", True), + ("avd", True), + ("a", True), + ("My-AVD_123", True), + ("", False), + ("1start", False), + ("_underscore", False), + ("-tiret", False), + ("with space", False), + ("foo.bar", False), + ("foo/bar", False), + ("foo\\bar", False), + ("MyAVD!", False), + (" avd", False), + ("A" * 256, True), + ], +) +def test_is_valid_avd_name(name, expected): + """Test that is_valid_avd_name returns the expected result for various names.""" + assert is_valid_avd_name(name) == expected diff --git a/uv.lock b/uv.lock index cb44752..5920ed8 100644 --- a/uv.lock +++ b/uv.lock @@ -6,3 +6,136 @@ requires-python = ">=3.10" name = "android-device-manager" version = "0.0.0" source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.4.1" }] + +[[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 = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +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 = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[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 = "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 = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] From 8f7bcf7e1aa32b9e7669521c55e67026383ae5a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 13:56:24 +0200 Subject: [PATCH 09/28] test: add unit test suite --- src/android_device_manager/adb/client.py | 6 +++--- tests/adb/test_client.py | 1 + tests/conftest.py | 2 -- tests/test_android_device.py | 1 + tests/utils/test_android_sdk.py | 1 + tests/utils/test_sdk_manager.py | 3 --- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/android_device_manager/adb/client.py b/src/android_device_manager/adb/client.py index 6b8e7c8..382fe9b 100644 --- a/src/android_device_manager/adb/client.py +++ b/src/android_device_manager/adb/client.py @@ -1,6 +1,6 @@ import logging import subprocess -from typing import List, Optional +from typing import Optional from ..adb.exceptions import ADBError, ADBTimeoutError from ..constants import AndroidProp @@ -96,7 +96,7 @@ def shell( Execute a shell command on the device/emulator via ADB. Args: - cmd (List[str]): The shell command as a list of arguments. Example: ["ls", "/sdcard"] + cmd (list[str]): The shell command as a list of arguments. Example: ["ls", "/sdcard"] timeout (int): Timeout for the command (default: 30). check (bool): If True, raise an exception for non-zero exit code. @@ -117,7 +117,7 @@ def _run_adb_command( Run an ADB command for the associated emulator/device with error handling. Args: - args (List[str]): List of ADB command arguments (excluding adb and -s). + args (list[str]): List of ADB command arguments (excluding adb and -s). timeout (int): Timeout in seconds for the command (default: 30). check (bool): If True, CalledProcessError is raised for non-zero return codes. diff --git a/tests/adb/test_client.py b/tests/adb/test_client.py index 360b171..85f803d 100644 --- a/tests/adb/test_client.py +++ b/tests/adb/test_client.py @@ -6,6 +6,7 @@ from android_device_manager.adb.client import AdbClient from android_device_manager.adb.exceptions import ADBError, ADBTimeoutError + def test_init_sets_serial_and_adb_path(fake_sdk): client = AdbClient(5554, fake_sdk) assert client._port == 5554 diff --git a/tests/conftest.py b/tests/conftest.py index 833f275..c584ed7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,3 @@ -from unittest import mock - import pytest from android_device_manager.utils.android_sdk import AndroidSDK diff --git a/tests/test_android_device.py b/tests/test_android_device.py index b0cd4bc..0d10b0b 100644 --- a/tests/test_android_device.py +++ b/tests/test_android_device.py @@ -1,4 +1,5 @@ from unittest import mock + import pytest from android_device_manager.adb.exceptions import ADBError diff --git a/tests/utils/test_android_sdk.py b/tests/utils/test_android_sdk.py index d71d4f3..174cb92 100644 --- a/tests/utils/test_android_sdk.py +++ b/tests/utils/test_android_sdk.py @@ -6,6 +6,7 @@ from android_device_manager.exceptions import AndroidSDKNotFound from android_device_manager.utils.android_sdk import AndroidSDK + def test_sdk_manual_path(fake_sdk_path): """Test initialization with an explicit, complete SDK path.""" sdk = AndroidSDK(fake_sdk_path) diff --git a/tests/utils/test_sdk_manager.py b/tests/utils/test_sdk_manager.py index ddd0adf..798d600 100644 --- a/tests/utils/test_sdk_manager.py +++ b/tests/utils/test_sdk_manager.py @@ -4,9 +4,6 @@ import pytest from android_device_manager.exceptions import SDKManagerError -from android_device_manager.utils.sdk_manager import SDKManager - - def make_sdkmanager_list_output(installed=None, available=None): From 714d120c574fac132b19afbec23bd72e883a8a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 14:08:51 +0200 Subject: [PATCH 10/28] ci: add initial CI pipeline with uv and GitHub Actions Includes: - uv installation via official setup action - dependency sync with uv - linting with ruff - testing with pytest and coverage support --- .github/workflows/ci.yml | 40 ++++++++++++++ pyproject.toml | 1 + uv.lock | 110 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fd911f6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: [main, develop,feature/ci-initial-pipeline] + pull_request: + branches: [main, develop] + +jobs: + build-test: + name: Build & Test with uv + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: [ "3.10", "3.11", "3.12" ] + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + + - name: Sync dependencies and install project + run: uv sync + + - name: Run lint with ruff + run: uvx ruff check . + + - name: Run tests + run: uv run pytest --cov + + - name: Upload coverage report + uses: codecov/codecov-action@v4 + with: + files: coverage.xml + fail_ci_if_error: true diff --git a/pyproject.toml b/pyproject.toml index f829d26..8ff79ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,4 +29,5 @@ markers = [ [dependency-groups] dev = [ "pytest>=8.4.1", + "pytest-cov>=6.2.1", ] diff --git a/uv.lock b/uv.lock index 5920ed8..006e0d3 100644 --- a/uv.lock +++ b/uv.lock @@ -10,12 +10,16 @@ source = { editable = "." } [package.dev-dependencies] dev = [ { name = "pytest" }, + { name = "pytest-cov" }, ] [package.metadata] [package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = ">=8.4.1" }] +dev = [ + { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest-cov", specifier = ">=6.2.1" }, +] [[package]] name = "colorama" @@ -26,6 +30,96 @@ 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.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754, upload-time = "2025-08-04T00:35:17.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/5f/5ce748ab3f142593698aff5f8a0cf020775aa4e24b9d8748b5a56b64d3f8/coverage-7.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65", size = 215003, upload-time = "2025-08-04T00:33:02.977Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ed/507088561217b000109552139802fa99c33c16ad19999c687b601b3790d0/coverage-7.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8", size = 215391, upload-time = "2025-08-04T00:33:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/79/1b/0f496259fe137c4c5e1e8eaff496fb95af88b71700f5e57725a4ddbe742b/coverage-7.10.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ae385e1d58fbc6a9b1c315e5510ac52281e271478b45f92ca9b5ad42cf39643f", size = 242367, upload-time = "2025-08-04T00:33:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8e/5a8835fb0122a2e2a108bf3527931693c4625fdc4d953950a480b9625852/coverage-7.10.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f0cbe5f7dd19f3a32bac2251b95d51c3b89621ac88a2648096ce40f9a5aa1e7", size = 243627, upload-time = "2025-08-04T00:33:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/c3/96/6a528429c2e0e8d85261764d0cd42e51a429510509bcc14676ee5d1bb212/coverage-7.10.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd17f427f041f6b116dc90b4049c6f3e1230524407d00daa2d8c7915037b5947", size = 245485, upload-time = "2025-08-04T00:33:10.29Z" }, + { url = "https://files.pythonhosted.org/packages/bf/82/1fba935c4d02c33275aca319deabf1f22c0f95f2c0000bf7c5f276d6f7b4/coverage-7.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7f10ca4cde7b466405cce0a0e9971a13eb22e57a5ecc8b5f93a81090cc9c7eb9", size = 243429, upload-time = "2025-08-04T00:33:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a8/c8dc0a57a729fc93be33ab78f187a8f52d455fa8f79bfb379fe23b45868d/coverage-7.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3b990df23dd51dccce26d18fb09fd85a77ebe46368f387b0ffba7a74e470b31b", size = 242104, upload-time = "2025-08-04T00:33:13.467Z" }, + { url = "https://files.pythonhosted.org/packages/b9/6f/0b7da1682e2557caeed299a00897b42afde99a241a01eba0197eb982b90f/coverage-7.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc3902584d25c7eef57fb38f440aa849a26a3a9f761a029a72b69acfca4e31f8", size = 242397, upload-time = "2025-08-04T00:33:14.682Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e4/54dc833dadccd519c04a28852f39a37e522bad35d70cfe038817cdb8f168/coverage-7.10.2-cp310-cp310-win32.whl", hash = "sha256:9dd37e9ac00d5eb72f38ed93e3cdf2280b1dbda3bb9b48c6941805f265ad8d87", size = 217502, upload-time = "2025-08-04T00:33:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e7/2f78159c4c127549172f427dff15b02176329327bf6a6a1fcf1f603b5456/coverage-7.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:99d16f15cb5baf0729354c5bd3080ae53847a4072b9ba1e10957522fb290417f", size = 218388, upload-time = "2025-08-04T00:33:17.4Z" }, + { url = "https://files.pythonhosted.org/packages/6e/53/0125a6fc0af4f2687b4e08b0fb332cd0d5e60f3ca849e7456f995d022656/coverage-7.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c3b210d79925a476dfc8d74c7d53224888421edebf3a611f3adae923e212b27", size = 215119, upload-time = "2025-08-04T00:33:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2e/960d9871de9152dbc9ff950913c6a6e9cf2eb4cc80d5bc8f93029f9f2f9f/coverage-7.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf67d1787cd317c3f8b2e4c6ed1ae93497be7e30605a0d32237ac37a37a8a322", size = 215511, upload-time = "2025-08-04T00:33:20.32Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/68509e44995b9cad806d81b76c22bc5181f3535bca7cd9c15791bfd8951e/coverage-7.10.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:069b779d03d458602bc0e27189876e7d8bdf6b24ac0f12900de22dd2154e6ad7", size = 245513, upload-time = "2025-08-04T00:33:21.896Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d4/9b12f357413248ce40804b0f58030b55a25b28a5c02db95fb0aa50c5d62c/coverage-7.10.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c2de4cb80b9990e71c62c2d3e9f3ec71b804b1f9ca4784ec7e74127e0f42468", size = 247350, upload-time = "2025-08-04T00:33:23.917Z" }, + { url = "https://files.pythonhosted.org/packages/b6/40/257945eda1f72098e4a3c350b1d68fdc5d7d032684a0aeb6c2391153ecf4/coverage-7.10.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75bf7ab2374a7eb107602f1e07310cda164016cd60968abf817b7a0b5703e288", size = 249516, upload-time = "2025-08-04T00:33:25.5Z" }, + { url = "https://files.pythonhosted.org/packages/ff/55/8987f852ece378cecbf39a367f3f7ec53351e39a9151b130af3a3045b83f/coverage-7.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3f37516458ec1550815134937f73d6d15b434059cd10f64678a2068f65c62406", size = 247241, upload-time = "2025-08-04T00:33:26.767Z" }, + { url = "https://files.pythonhosted.org/packages/df/ae/da397de7a42a18cea6062ed9c3b72c50b39e0b9e7b2893d7172d3333a9a1/coverage-7.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:de3c6271c482c250d3303fb5c6bdb8ca025fff20a67245e1425df04dc990ece9", size = 245274, upload-time = "2025-08-04T00:33:28.494Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/7baa895eb55ec0e1ec35b988687ecd5d4475ababb0d7ae5ca3874dd90ee7/coverage-7.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:98a838101321ac3089c9bb1d4bfa967e8afed58021fda72d7880dc1997f20ae1", size = 245882, upload-time = "2025-08-04T00:33:30.048Z" }, + { url = "https://files.pythonhosted.org/packages/24/6c/1fd76a0bd09ae75220ae9775a8290416d726f0e5ba26ea72346747161240/coverage-7.10.2-cp311-cp311-win32.whl", hash = "sha256:f2a79145a531a0e42df32d37be5af069b4a914845b6f686590739b786f2f7bce", size = 217541, upload-time = "2025-08-04T00:33:31.376Z" }, + { url = "https://files.pythonhosted.org/packages/5f/2d/8c18fb7a6e74c79fd4661e82535bc8c68aee12f46c204eabf910b097ccc9/coverage-7.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:e4f5f1320f8ee0d7cfa421ceb257bef9d39fd614dd3ddcfcacd284d4824ed2c2", size = 218426, upload-time = "2025-08-04T00:33:32.976Z" }, + { url = "https://files.pythonhosted.org/packages/da/40/425bb35e4ff7c7af177edf5dffd4154bc2a677b27696afe6526d75c77fec/coverage-7.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:d8f2d83118f25328552c728b8e91babf93217db259ca5c2cd4dd4220b8926293", size = 217116, upload-time = "2025-08-04T00:33:34.302Z" }, + { url = "https://files.pythonhosted.org/packages/4e/1e/2c752bdbbf6f1199c59b1a10557fbb6fb3dc96b3c0077b30bd41a5922c1f/coverage-7.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83", size = 215311, upload-time = "2025-08-04T00:33:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/84277d73a2cafb96e24be81b7169372ba7ff28768ebbf98e55c85a491b0f/coverage-7.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c", size = 215550, upload-time = "2025-08-04T00:33:37.109Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e7/5358b73b46ac76f56cc2de921eeabd44fabd0b7ff82ea4f6b8c159c4d5dc/coverage-7.10.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518", size = 246564, upload-time = "2025-08-04T00:33:38.33Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0e/b0c901dd411cb7fc0cfcb28ef0dc6f3049030f616bfe9fc4143aecd95901/coverage-7.10.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21", size = 248993, upload-time = "2025-08-04T00:33:39.555Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4e/a876db272072a9e0df93f311e187ccdd5f39a190c6d1c1f0b6e255a0d08e/coverage-7.10.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0", size = 250454, upload-time = "2025-08-04T00:33:41.023Z" }, + { url = "https://files.pythonhosted.org/packages/64/d6/1222dc69f8dd1be208d55708a9f4a450ad582bf4fa05320617fea1eaa6d8/coverage-7.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75", size = 248365, upload-time = "2025-08-04T00:33:42.376Z" }, + { url = "https://files.pythonhosted.org/packages/62/e3/40fd71151064fc315c922dd9a35e15b30616f00146db1d6a0b590553a75a/coverage-7.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01", size = 246562, upload-time = "2025-08-04T00:33:43.663Z" }, + { url = "https://files.pythonhosted.org/packages/fc/14/8aa93ddcd6623ddaef5d8966268ac9545b145bce4fe7b1738fd1c3f0d957/coverage-7.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b", size = 247772, upload-time = "2025-08-04T00:33:45.068Z" }, + { url = "https://files.pythonhosted.org/packages/07/4e/dcb1c01490623c61e2f2ea85cb185fa6a524265bb70eeb897d3c193efeb9/coverage-7.10.2-cp312-cp312-win32.whl", hash = "sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340", size = 217710, upload-time = "2025-08-04T00:33:46.378Z" }, + { url = "https://files.pythonhosted.org/packages/79/16/e8aab4162b5f80ad2e5e1f54b1826e2053aa2f4db508b864af647f00c239/coverage-7.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388", size = 218499, upload-time = "2025-08-04T00:33:48.048Z" }, + { url = "https://files.pythonhosted.org/packages/06/7f/c112ec766e8f1131ce8ce26254be028772757b2d1e63e4f6a4b0ad9a526c/coverage-7.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20", size = 217154, upload-time = "2025-08-04T00:33:49.299Z" }, + { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337, upload-time = "2025-08-04T00:33:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596, upload-time = "2025-08-04T00:33:52.33Z" }, + { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145, upload-time = "2025-08-04T00:33:53.641Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492, upload-time = "2025-08-04T00:33:55.366Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927, upload-time = "2025-08-04T00:33:57.042Z" }, + { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138, upload-time = "2025-08-04T00:33:58.329Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111, upload-time = "2025-08-04T00:33:59.899Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493, upload-time = "2025-08-04T00:34:01.619Z" }, + { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756, upload-time = "2025-08-04T00:34:03.277Z" }, + { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526, upload-time = "2025-08-04T00:34:04.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176, upload-time = "2025-08-04T00:34:05.973Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058, upload-time = "2025-08-04T00:34:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273, upload-time = "2025-08-04T00:34:09.073Z" }, + { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513, upload-time = "2025-08-04T00:34:10.403Z" }, + { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377, upload-time = "2025-08-04T00:34:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516, upload-time = "2025-08-04T00:34:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110, upload-time = "2025-08-04T00:34:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248, upload-time = "2025-08-04T00:34:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063, upload-time = "2025-08-04T00:34:18.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433, upload-time = "2025-08-04T00:34:19.71Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523, upload-time = "2025-08-04T00:34:21.171Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739, upload-time = "2025-08-04T00:34:22.514Z" }, + { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328, upload-time = "2025-08-04T00:34:23.991Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608, upload-time = "2025-08-04T00:34:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111, upload-time = "2025-08-04T00:34:26.857Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419, upload-time = "2025-08-04T00:34:28.726Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038, upload-time = "2025-08-04T00:34:30.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066, upload-time = "2025-08-04T00:34:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909, upload-time = "2025-08-04T00:34:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329, upload-time = "2025-08-04T00:34:34.388Z" }, + { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007, upload-time = "2025-08-04T00:34:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802, upload-time = "2025-08-04T00:34:37.35Z" }, + { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397, upload-time = "2025-08-04T00:34:39.15Z" }, + { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068, upload-time = "2025-08-04T00:34:40.648Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285, upload-time = "2025-08-04T00:34:42.441Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603, upload-time = "2025-08-04T00:34:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568, upload-time = "2025-08-04T00:34:45.519Z" }, + { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691, upload-time = "2025-08-04T00:34:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166, upload-time = "2025-08-04T00:34:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241, upload-time = "2025-08-04T00:34:51.038Z" }, + { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139, upload-time = "2025-08-04T00:34:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809, upload-time = "2025-08-04T00:34:54.075Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926, upload-time = "2025-08-04T00:34:55.643Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925, upload-time = "2025-08-04T00:34:57.564Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973, upload-time = "2025-08-04T00:35:15.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -92,6 +186,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] +[[package]] +name = "pytest-cov" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, +] + [[package]] name = "tomli" version = "2.2.1" From 507edbad2790a51cbe0f827afba74f297a7b2cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 14:17:16 +0200 Subject: [PATCH 11/28] ci: fix coverage file path in Codecov upload step --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd911f6..66de89b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,5 +36,5 @@ jobs: - name: Upload coverage report uses: codecov/codecov-action@v4 with: - files: coverage.xml + files: .coverage fail_ci_if_error: true From e99dbc3ed2931f583c80cb6d8f1e5134330878bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 14:22:21 +0200 Subject: [PATCH 12/28] ci: fix missing coverage.xml export for Codecov --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66de89b..39d675b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,10 +31,10 @@ jobs: run: uvx ruff check . - name: Run tests - run: uv run pytest --cov + run: uv run pytest --cov --cov-branch --cov-report=xml - name: Upload coverage report uses: codecov/codecov-action@v4 with: - files: .coverage + files: coverage.xml fail_ci_if_error: true From 99f865a652d5e57a6fa2012e23f16b2e7739477a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 14:25:04 +0200 Subject: [PATCH 13/28] ci: remove feature/ci-initial-pipeline from branch filters --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39d675b..8499072 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main, develop,feature/ci-initial-pipeline] + branches: [main, develop] pull_request: branches: [main, develop] From 1fb12b41fedd0feaf434ba1d80b49e2abc696aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 18:16:59 +0200 Subject: [PATCH 14/28] feat(android-device): add get_prop and get_all_props methods with running state checks --- src/android_device_manager/adb/client.py | 23 +++++++++ src/android_device_manager/android_device.py | 52 ++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/android_device_manager/adb/client.py b/src/android_device_manager/adb/client.py index 382fe9b..662c111 100644 --- a/src/android_device_manager/adb/client.py +++ b/src/android_device_manager/adb/client.py @@ -27,6 +27,29 @@ def __init__(self, emulator_port: int, android_sdk: Optional[AndroidSDK] = None) self._android_sdk = android_sdk or AndroidSDK() self._adb_path = self._android_sdk.adb_path + def get_all_props(self, timeout: int = 10) -> dict[str, str]: + """ + Get all Android system properties as a dictionary. + + Args: + timeout (int): Timeout in seconds. + + Returns: + dict[str, str]: All system properties as {key: value} + + Raises: + ADBError: On failure. + """ + result = self.shell(["getprop"], timeout=timeout) + props = {} + for line in result.stdout.splitlines(): + if line.startswith("[") and "]:" in line: + key, value = line.split("]: [", 1) + key = key[1:] + value = value.rstrip("]") + props[key] = value + return props + def get_prop( self, key: str | AndroidProp, timeout: int = 10, check: bool = True ) -> str: diff --git a/src/android_device_manager/android_device.py b/src/android_device_manager/android_device.py index cf29450..1c74fee 100644 --- a/src/android_device_manager/android_device.py +++ b/src/android_device_manager/android_device.py @@ -7,9 +7,11 @@ from .avd.config import AVDConfiguration from .avd.exceptions import AVDCreationError, AVDDeletionError from .avd.manager import AVDManager +from .constants import AndroidProp from .emulator.config import EmulatorConfiguration from .emulator.exceptions import EmulatorStartError from .emulator.manager import EmulatorManager +from .exceptions import AndroidDeviceError from .utils.android_sdk import AndroidSDK logger = logging.getLogger(__name__) @@ -148,6 +150,56 @@ def stop(self): logger.error(f"Failed to stop emulator for '{self.name}': {e}") raise + def get_prop( + self, key: str | AndroidProp, timeout: int = 10, check: bool = True + ) -> str: + """ + Retrieve a single Android system property from the device. + + Args: + key (str or AndroidProp): The name of the property, or an AndroidProp Enum. + timeout (int): Timeout in seconds for the adb command (default: 10). + check (bool): Whether to raise an exception if the command fails (default: True). + + Returns: + str: The value of the requested property, or an empty string if not found. + + Raises: + AndroidDeviceError: If the device is not started or the ADB client is not initialized. + ADBError: If there is a failure in communicating with the device. + """ + self._ensure_running() + return self._adb_client.get_prop(key, timeout=timeout, check=check) + + def get_all_props(self, timeout: int = 10) -> dict[str, str]: + """ + Retrieve all Android system properties from the device as a dictionary. + + Args: + timeout (int): Timeout in seconds for the adb command (default: 10). + + Returns: + dict[str, str]: A dictionary mapping property names to their values. + + Raises: + AndroidDeviceError: If the device is not started or the ADB client is not initialized. + ADBError: If there is a failure in communicating with the device. + """ + self._ensure_running() + return self._adb_client.get_all_props(timeout=timeout) + + def _ensure_running(self): + """ + Ensure that the Android device is started and the ADB client is initialized. + + Raises: + AndroidDeviceError: If the device is not running or the ADB client is not available. + """ + if self.state != AndroidDeviceState.RUNNING or not self._adb_client: + raise AndroidDeviceError( + "ADB client not initialized. Device must be started." + ) + def __enter__(self): """ Context manager entry: ensure device is created and started. From b06c6af03d261a8e9a11753ca7614b7f16702435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 18:19:14 +0200 Subject: [PATCH 15/28] feat(android-device): add root, unroot and is_root methods --- src/android_device_manager/adb/client.py | 33 +++++++++++++++ src/android_device_manager/android_device.py | 42 ++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/android_device_manager/adb/client.py b/src/android_device_manager/adb/client.py index 662c111..21ae554 100644 --- a/src/android_device_manager/adb/client.py +++ b/src/android_device_manager/adb/client.py @@ -112,6 +112,39 @@ def kill_emulator(self): except ADBError as e: raise ADBError(f"Failed to kill emulator {self._serial}: {str(e)}") + def root(self, timeout: int = 10, check: bool = True) -> bool: + """ + Restart adbd with root permissions, if possible. + + Args: + timeout (int): Timeout for the command (default: 10s) + check (bool): Raise if the command fails. + + Returns: + bool: True if adbd is now running as root, False otherwise. + + Raises: + ADBError: On failure to restart adbd. + """ + self._run_adb_command(["root"], timeout=timeout, check=check) + return self.is_root() + + def unroot(self, timeout: int = 10, check: bool = True) -> bool: + self._run_adb_command(["unroot"], timeout=timeout, check=check) + return not self.is_root() + + def is_root(self, timeout: int = 10) -> bool: + """ + Check if adbd is running as root on the device. + + Returns: + bool: True if running as root, False otherwise. + """ + result = self._run_adb_command( + ["shell", "id", "-u"], timeout=timeout, check=True + ) + return result.stdout.strip() == "0" + def shell( self, cmd: list[str], timeout: int = 30, check: bool = True ) -> subprocess.CompletedProcess: diff --git a/src/android_device_manager/android_device.py b/src/android_device_manager/android_device.py index 1c74fee..4384b9f 100644 --- a/src/android_device_manager/android_device.py +++ b/src/android_device_manager/android_device.py @@ -188,6 +188,48 @@ def get_all_props(self, timeout: int = 10) -> dict[str, str]: self._ensure_running() return self._adb_client.get_all_props(timeout=timeout) + def root(self) -> bool: + """ + Restart the ADB daemon with root privileges. + + Returns: + bool: True if the device is now running in root mode, False otherwise. + + Raises: + AndroidDeviceError: If the device is not started or the ADB client is not initialized. + ADBError: If the operation fails. + """ + self._ensure_running() + return self._adb_client.root() + + def unroot(self) -> bool: + """ + Restart the ADB daemon without root privileges (back to user mode). + + Returns: + bool: True if the device is now running in unrooted mode, False otherwise. + + Raises: + AndroidDeviceError: If the device is not started or the ADB client is not initialized. + ADBError: If the operation fails. + """ + self._ensure_running() + return self._adb_client.unroot() + + def is_root(self) -> bool: + """ + Check if the ADB daemon is currently running with root privileges. + + Returns: + bool: True if the device is running in root mode, False otherwise. + + Raises: + AndroidDeviceError: If the device is not started or the ADB client is not initialized. + ADBError: If the operation fails. + """ + self._ensure_running() + return self._adb_client.is_root() + def _ensure_running(self): """ Ensure that the Android device is started and the ADB client is initialized. From 016b60d67d73623ca5340dc0248f330d20e738cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 18:22:02 +0200 Subject: [PATCH 16/28] feat(android-device): add package management methods (list/install/uninstall/check) --- src/android_device_manager/adb/client.py | 61 ++++++++++++++++++- src/android_device_manager/android_device.py | 64 ++++++++++++++++++++ 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/android_device_manager/adb/client.py b/src/android_device_manager/adb/client.py index 21ae554..5d36909 100644 --- a/src/android_device_manager/adb/client.py +++ b/src/android_device_manager/adb/client.py @@ -140,11 +140,68 @@ def is_root(self, timeout: int = 10) -> bool: Returns: bool: True if running as root, False otherwise. """ - result = self._run_adb_command( - ["shell", "id", "-u"], timeout=timeout, check=True + result = self.shell( + ["id", "-u"], timeout=timeout, check=True ) return result.stdout.strip() == "0" + def list_installed_packages(self) -> list[str]: + """ + List installed package names on the device. + Returns: + list[str]: Package names. + Raises: + ADBError: On failure. + """ + result = self.shell(["pm", "list", "packages"], check=True) + lines = result.stdout.strip().splitlines() + return [ + line[len("package:"):].strip() + for line in lines + if line.startswith("package:") + ] + + def install_apk(self, apk_path: str, timeout: int = 60): + """ + Install an APK file on the device. + + Args: + apk_path (str): Path to the APK file on the host. + timeout (int): Timeout in seconds for the installation. + + Raises: + ADBError: If the installation fails. + """ + try: + args = ["install", apk_path] + self._run_adb_command(args, check=True, timeout=timeout) + logger.info(f"Successfully installed APK {apk_path} on {self._serial}") + except ADBError as e: + raise ADBError(f"Failed to install APK {apk_path}: {str(e)}") + + def uninstall_package( + self, package_name: str, keep_data: bool = False, timeout: int = 60 + ) -> None: + """ + Uninstall a package from the device. + + Args: + package_name (str): The full package name to uninstall. + keep_data (bool): If True, keep app data and cache (default: False). + timeout (int): Timeout in seconds. + + Raises: + ADBError: If the uninstallation fails. + """ + cmd = ["uninstall"] + if keep_data: + cmd.append("-k") + cmd.append(package_name) + result = self._run_adb_command(cmd, timeout=timeout, check=True) + output = result.stdout.strip().lower() + if not ("success" in output): + raise ADBError(f"Failed to uninstall package '{package_name}': {output}") + def shell( self, cmd: list[str], timeout: int = 30, check: bool = True ) -> subprocess.CompletedProcess: diff --git a/src/android_device_manager/android_device.py b/src/android_device_manager/android_device.py index 4384b9f..c668f99 100644 --- a/src/android_device_manager/android_device.py +++ b/src/android_device_manager/android_device.py @@ -230,6 +230,70 @@ def is_root(self) -> bool: self._ensure_running() return self._adb_client.is_root() + def list_installed_packages(self) -> list[str]: + """ + List all installed package names on the device. + + Returns: + list[str]: A list of installed package names. + + Raises: + AndroidDeviceError: If the device is not running or the ADB client is not initialized. + ADBError: If the command fails. + """ + self._ensure_running() + return self._adb_client.list_installed_packages() + + def is_package_installed(self, package_name: str) -> bool: + """ + Check if a given package is installed on the device. + + Args: + package_name (str): The package name to check. + + Returns: + bool: True if the package is installed, False otherwise. + + Raises: + AndroidDeviceError: If the device is not running or the ADB client is not initialized. + ADBError: If the command fails. + """ + self._ensure_running() + return package_name in self._adb_client.list_installed_packages() + + def install_apk(self, apk_path: str, timeout: int = 30) -> None: + """ + Install an APK on the device. + + Args: + apk_path (str): The file path to the APK. + timeout (int): Timeout in seconds for the installation process (default: 30). + + Raises: + AndroidDeviceError: If the device is not running or the ADB client is not initialized. + ADBError: If the command fails. + """ + self._ensure_running() + self._adb_client.install_apk(apk_path, timeout=timeout) + + def uninstall_package(self, package_name: str, keep_data: bool = False) -> None: + """ + Uninstall a package from the device. + + Args: + package_name (str): The name of the package to uninstall. + keep_data (bool): If True, application data and cache are retained (default: False). + + Raises: + AndroidDeviceError: If the device is not running, the ADB client is not initialized, + or the package is not installed. + ADBError: If the command fails. + """ + self._ensure_running() + if not self.is_package_installed(package_name): + raise AndroidDeviceError(f"Package '{package_name}' is not installed.") + self._adb_client.uninstall_package(package_name, keep_data=keep_data) + def _ensure_running(self): """ Ensure that the Android device is started and the ADB client is initialized. From 27e90992a81ca40696e5b1d5104983d5c3d7c322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 18:24:30 +0200 Subject: [PATCH 17/28] feat(android-device): add file transfer and data partition dump methods --- src/android_device_manager/adb/client.py | 41 ++++++++++++++ src/android_device_manager/android_device.py | 57 ++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/android_device_manager/adb/client.py b/src/android_device_manager/adb/client.py index 5d36909..270d111 100644 --- a/src/android_device_manager/adb/client.py +++ b/src/android_device_manager/adb/client.py @@ -1,5 +1,6 @@ import logging import subprocess +from pathlib import Path from typing import Optional from ..adb.exceptions import ADBError, ADBTimeoutError @@ -202,6 +203,46 @@ def uninstall_package( if not ("success" in output): raise ADBError(f"Failed to uninstall package '{package_name}': {output}") + def push_file( + self, local: str | Path, remote: str, timeout: int = 60, check: bool = True + ) -> None: + """ + Push a file from the local host to the device. + + Args: + local (str | Path): Path to the local file. + remote (str): Destination path on the device (e.g., /sdcard/file.txt). + timeout (int): Timeout in seconds. + check (bool): Raise exception on failure. + + Raises: + ADBError: If the command fails. + ADBTimeoutError: On timeout. + """ + local_path = str(local) + args = ["push", local_path, remote] + self._run_adb_command(args, timeout=timeout, check=check) + + def pull_file( + self, remote: str, local: str | Path, timeout: int = 60, check: bool = True + ) -> None: + """ + Pull a file from the device to the local host. + + Args: + remote (str): Path to the file on the device. + local (str | Path): Destination path on the host. + timeout (int): Timeout in seconds. + check (bool): Raise exception on failure. + + Raises: + ADBError: If the command fails. + ADBTimeoutError: On timeout. + """ + local_path = str(local) + args = ["pull", remote, local_path] + self._run_adb_command(args, timeout=timeout, check=check) + def shell( self, cmd: list[str], timeout: int = 30, check: bool = True ) -> subprocess.CompletedProcess: diff --git a/src/android_device_manager/android_device.py b/src/android_device_manager/android_device.py index c668f99..5bdd145 100644 --- a/src/android_device_manager/android_device.py +++ b/src/android_device_manager/android_device.py @@ -1,5 +1,6 @@ import logging from enum import Enum +from pathlib import Path from typing import Optional from .adb.client import AdbClient @@ -294,6 +295,62 @@ def uninstall_package(self, package_name: str, keep_data: bool = False) -> None: raise AndroidDeviceError(f"Package '{package_name}' is not installed.") self._adb_client.uninstall_package(package_name, keep_data=keep_data) + def push_file(self, local: str | Path, remote: str) -> None: + """ + Push a file from the local machine to the device. + + Args: + local (str or Path): Path to the local file to push. + remote (str): Destination path on the device. + + Raises: + AndroidDeviceError: If the device is not running or the ADB client is not initialized. + ADBError: If the command fails. + """ + self._ensure_running() + self._adb_client.push_file(local, remote) + + def pull_file(self, remote: str, local: str | Path) -> None: + """ + Pull a file from the device to the local machine. + + Args: + remote (str): Path to the file on the device. + local (str or Path): Destination path on the local machine. + + Raises: + AndroidDeviceError: If the device is not running or the ADB client is not initialized. + ADBError: If the command fails. + """ + self._ensure_running() + self._adb_client.pull_file(remote, local) + + def pull_data_partition(self, dest_path: str | Path = "./data.tar"): + """ + Archive and pull the entire /data partition from the device. + + The method switches to root mode, stops the Android runtime, + archives the /data directory to a tar, pulls it to the local machine, + removes the archive on the device, restarts Android, and returns to unrooted mode. + + Args: + dest_path (str or Path): Local destination path for the pulled tarball (default: "./data.tar"). + + Raises: + AndroidDeviceError: If the device is not running or the ADB client is not initialized. + ADBError: If any command fails during the process. + """ + self._ensure_running() + self._adb_client.root() + self._adb_client._run_adb_command(["shell", "stop"]) + self._adb_client.shell( + ["tar", "cf", "/tmp/data.tar", "/data"], check=False, timeout=120 + ) + self._adb_client.pull_file("/tmp/data.tar", dest_path) + self._adb_client.shell(["rm", "-r", "/tmp/data.tar"]) + self._adb_client._run_adb_command(["shell", "start"]) + self._adb_client.unroot() + def _ensure_running(self): """ Ensure that the Android device is started and the ADB client is initialized. From 5cf3096eddc793d67aeb2869064c3e28a0a4a6f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 18:26:22 +0200 Subject: [PATCH 18/28] feat(android-device): add logcat retrieval and clear methods --- src/android_device_manager/adb/client.py | 42 ++++++++++++++++++++ src/android_device_manager/android_device.py | 28 +++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/android_device_manager/adb/client.py b/src/android_device_manager/adb/client.py index 270d111..e0190f3 100644 --- a/src/android_device_manager/adb/client.py +++ b/src/android_device_manager/adb/client.py @@ -243,6 +243,48 @@ def pull_file( args = ["pull", remote, local_path] self._run_adb_command(args, timeout=timeout, check=check) + def get_logcat( + self, + filter_spec: Optional[list[str]] = None, + timeout: int = 30, + check: bool = True, + ) -> str: + """ + Retrieve logcat output from the device. + + Args: + filter_spec (Optional[List[str]]): List of filter spec strings, e.g., ['*:E', 'ActivityManager:I'] + timeout (int): Timeout for the command. + check (bool): Raise on non-zero exit code. + + Returns: + str: Logcat output (stdout). + + Raises: + ADBError: If adb command fails. + ADBTimeoutError: On timeout. + """ + args = ["logcat", "-d"] + if filter_spec: + args.extend(filter_spec) + result = self._run_adb_command(args, timeout=timeout, check=check) + return result.stdout + + def clear_logcat(self, timeout: int = 10, check: bool = True) -> None: + """ + Clear the device logcat buffer. + + Args: + timeout (int): Timeout for the command (default: 10 seconds). + check (bool): If True, raise if the command fails. + + Raises: + ADBError: If the command fails. + AVDTimeoutError: On timeout. + """ + args = ["logcat", "-c"] + self._run_adb_command(args, timeout=timeout, check=check) + def shell( self, cmd: list[str], timeout: int = 30, check: bool = True ) -> subprocess.CompletedProcess: diff --git a/src/android_device_manager/android_device.py b/src/android_device_manager/android_device.py index 5bdd145..9d816a6 100644 --- a/src/android_device_manager/android_device.py +++ b/src/android_device_manager/android_device.py @@ -351,6 +351,34 @@ def pull_data_partition(self, dest_path: str | Path = "./data.tar"): self._adb_client._run_adb_command(["shell", "start"]) self._adb_client.unroot() + def get_logcat(self, filter_spec: Optional[list[str]] = None) -> str: + """ + Retrieve the current logcat output from the device. + + Args: + filter_spec (Optional[List[str]]): Optional list of logcat filters (e.g. ['*:E', 'ActivityManager:I']). + + Returns: + str: The logcat output as a string. + + Raises: + AndroidDeviceError: If the device is not running or the ADB client is not initialized. + ADBError: If the command fails. + """ + self._ensure_running() + return self._adb_client.get_logcat(filter_spec=filter_spec) + + def clear_logcat(self) -> None: + """ + Clear the device's logcat logs. + + Raises: + AndroidDeviceError: If the device is not running or the ADB client is not initialized. + ADBError: If the command fails. + """ + self._ensure_running() + self._adb_client.clear_logcat() + def _ensure_running(self): """ Ensure that the Android device is started and the ADB client is initialized. From bd42ef58557688e310c3a79ad8bc2b1f6c1a7f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 18:27:36 +0200 Subject: [PATCH 19/28] feat(android-device): add shell command execution with forbidden command checks --- src/android_device_manager/android_device.py | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/android_device_manager/android_device.py b/src/android_device_manager/android_device.py index 9d816a6..fc7ad21 100644 --- a/src/android_device_manager/android_device.py +++ b/src/android_device_manager/android_device.py @@ -1,4 +1,5 @@ import logging +import subprocess from enum import Enum from pathlib import Path from typing import Optional @@ -379,6 +380,34 @@ def clear_logcat(self) -> None: self._ensure_running() self._adb_client.clear_logcat() + def shell( + self, args: list[str], timeout: int = 30, check: bool = True + ) -> subprocess.CompletedProcess: + """ + Execute a shell command on the device/emulator via ADB, with safety checks for forbidden commands. + + Args: + args (List[str]): The shell command as a list of arguments (e.g., ["ls", "/sdcard"]). + timeout (int): Timeout in seconds for the command (default: 30). + check (bool): If True, raise an exception for non-zero exit code. + + Returns: + subprocess.CompletedProcess: The result object containing stdout, stderr, and exit code. + + Raises: + AndroidDeviceError: If the command is forbidden (e.g., stop, reboot, poweroff). + ADBError: If the shell command fails (when check is True). + ADBTimeoutError: On timeout. + """ + forbidden = {"stop", "reboot", "poweroff"} + if args and args[0] in forbidden: + raise AndroidDeviceError( + f"The shell command '{args[0]}' is not allowed via shell(). " + "Such commands can cause the device state to become incoherent with the library state. " + "Direct use of stop/reboot/poweroff is not supported yet—please use explicit API methods." + ) + return self._adb_client.shell(args, timeout=timeout, check=check) + def _ensure_running(self): """ Ensure that the Android device is started and the ADB client is initialized. From 876624f2bbb8f4a0b9d3ed1f40c8fb68985a5171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 18:28:35 +0200 Subject: [PATCH 20/28] feat(android-device): add shell command execution with forbidden command checks --- src/android_device_manager/android_device.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/android_device_manager/android_device.py b/src/android_device_manager/android_device.py index fc7ad21..558e788 100644 --- a/src/android_device_manager/android_device.py +++ b/src/android_device_manager/android_device.py @@ -395,7 +395,7 @@ def shell( subprocess.CompletedProcess: The result object containing stdout, stderr, and exit code. Raises: - AndroidDeviceError: If the command is forbidden (e.g., stop, reboot, poweroff). + AndroidDeviceError: If the command is forbidden (e.g., stop, reboot, poweroff). If the device is not running or the ADB client is not initialized. ADBError: If the shell command fails (when check is True). ADBTimeoutError: On timeout. """ @@ -406,6 +406,7 @@ def shell( "Such commands can cause the device state to become incoherent with the library state. " "Direct use of stop/reboot/poweroff is not supported yet—please use explicit API methods." ) + self._ensure_running() return self._adb_client.shell(args, timeout=timeout, check=check) def _ensure_running(self): From 3200c6ffe5cc464d56e25a3d4cdcc8372a85a404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 18:29:48 +0200 Subject: [PATCH 21/28] style: format codebase using uvx ruff format --- src/android_device_manager/adb/client.py | 22 +++++++++----------- src/android_device_manager/android_device.py | 4 ++-- tests/adb/test_client.py | 6 +++--- tests/conftest.py | 2 +- tests/emulator/test_config.py | 19 +++-------------- tests/utils/test_sdk_manager.py | 1 - 6 files changed, 19 insertions(+), 35 deletions(-) diff --git a/src/android_device_manager/adb/client.py b/src/android_device_manager/adb/client.py index e0190f3..5035ba7 100644 --- a/src/android_device_manager/adb/client.py +++ b/src/android_device_manager/adb/client.py @@ -141,9 +141,7 @@ def is_root(self, timeout: int = 10) -> bool: Returns: bool: True if running as root, False otherwise. """ - result = self.shell( - ["id", "-u"], timeout=timeout, check=True - ) + result = self.shell(["id", "-u"], timeout=timeout, check=True) return result.stdout.strip() == "0" def list_installed_packages(self) -> list[str]: @@ -157,7 +155,7 @@ def list_installed_packages(self) -> list[str]: result = self.shell(["pm", "list", "packages"], check=True) lines = result.stdout.strip().splitlines() return [ - line[len("package:"):].strip() + line[len("package:") :].strip() for line in lines if line.startswith("package:") ] @@ -181,7 +179,7 @@ def install_apk(self, apk_path: str, timeout: int = 60): raise ADBError(f"Failed to install APK {apk_path}: {str(e)}") def uninstall_package( - self, package_name: str, keep_data: bool = False, timeout: int = 60 + self, package_name: str, keep_data: bool = False, timeout: int = 60 ) -> None: """ Uninstall a package from the device. @@ -200,11 +198,11 @@ def uninstall_package( cmd.append(package_name) result = self._run_adb_command(cmd, timeout=timeout, check=True) output = result.stdout.strip().lower() - if not ("success" in output): + if "success" not in output: raise ADBError(f"Failed to uninstall package '{package_name}': {output}") def push_file( - self, local: str | Path, remote: str, timeout: int = 60, check: bool = True + self, local: str | Path, remote: str, timeout: int = 60, check: bool = True ) -> None: """ Push a file from the local host to the device. @@ -224,7 +222,7 @@ def push_file( self._run_adb_command(args, timeout=timeout, check=check) def pull_file( - self, remote: str, local: str | Path, timeout: int = 60, check: bool = True + self, remote: str, local: str | Path, timeout: int = 60, check: bool = True ) -> None: """ Pull a file from the device to the local host. @@ -244,10 +242,10 @@ def pull_file( self._run_adb_command(args, timeout=timeout, check=check) def get_logcat( - self, - filter_spec: Optional[list[str]] = None, - timeout: int = 30, - check: bool = True, + self, + filter_spec: Optional[list[str]] = None, + timeout: int = 30, + check: bool = True, ) -> str: """ Retrieve logcat output from the device. diff --git a/src/android_device_manager/android_device.py b/src/android_device_manager/android_device.py index 558e788..67ade56 100644 --- a/src/android_device_manager/android_device.py +++ b/src/android_device_manager/android_device.py @@ -153,7 +153,7 @@ def stop(self): raise def get_prop( - self, key: str | AndroidProp, timeout: int = 10, check: bool = True + self, key: str | AndroidProp, timeout: int = 10, check: bool = True ) -> str: """ Retrieve a single Android system property from the device. @@ -381,7 +381,7 @@ def clear_logcat(self) -> None: self._adb_client.clear_logcat() def shell( - self, args: list[str], timeout: int = 30, check: bool = True + self, args: list[str], timeout: int = 30, check: bool = True ) -> subprocess.CompletedProcess: """ Execute a shell command on the device/emulator via ADB, with safety checks for forbidden commands. diff --git a/tests/adb/test_client.py b/tests/adb/test_client.py index 85f803d..c2cca69 100644 --- a/tests/adb/test_client.py +++ b/tests/adb/test_client.py @@ -61,7 +61,7 @@ def test_wait_for_boot_success(fake_sdk): client = AdbClient(5554, fake_sdk) result = mock.Mock() result.stdout = "0" - + with mock.patch.object( client, "_run_adb_command", @@ -72,7 +72,7 @@ def test_wait_for_boot_success(fake_sdk): def test_wait_for_boot_timeout(fake_sdk): client = AdbClient(5554, fake_sdk) - + with mock.patch.object( client, "_run_adb_command", return_value=mock.Mock(stdout="0") ): @@ -83,7 +83,7 @@ def test_wait_for_boot_timeout(fake_sdk): def test_kill_emulator_success(fake_sdk): client = AdbClient(5554, fake_sdk) with mock.patch.object(client, "_run_adb_command", return_value=mock.Mock()): - client.kill_emulator() + client.kill_emulator() def test_kill_emulator_adberror(fake_sdk): diff --git a/tests/conftest.py b/tests/conftest.py index c584ed7..52361ba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,6 +35,7 @@ def fake_sdk_path(monkeypatch, tmp_path, request): yield sdk_root + @pytest.fixture def fake_sdk(fake_sdk_path): return AndroidSDK(fake_sdk_path) @@ -43,4 +44,3 @@ def fake_sdk(fake_sdk_path): @pytest.fixture def manager(fake_sdk): return SDKManager(fake_sdk) - diff --git a/tests/emulator/test_config.py b/tests/emulator/test_config.py index 3f70e7e..aac71bd 100644 --- a/tests/emulator/test_config.py +++ b/tests/emulator/test_config.py @@ -54,12 +54,10 @@ def test_partial_initialization(self): """Test EmulatorConfiguration with some custom values.""" config = EmulatorConfiguration(no_window=True, memory=1024, netdelay="umts") - assert config.no_window is True assert config.memory == 1024 assert config.netdelay == "umts" - assert config.no_audio is False assert config.gpu == "auto" assert config.cores is None @@ -70,7 +68,6 @@ def test_to_args_default_config(self): config = EmulatorConfiguration() args = config.to_args() - assert args == [] def test_to_args_all_boolean_flags(self): @@ -94,11 +91,9 @@ def test_to_args_all_boolean_flags(self): "-verbose", ] - for arg in expected_args: assert arg in args - assert len(args) == len(expected_args) def test_to_args_with_values(self): @@ -116,7 +111,6 @@ def test_to_args_with_values(self): ("-netspeed", "edge"), ] - arg_pairs = [(args[i], args[i + 1]) for i in range(0, len(args), 2)] assert len(arg_pairs) == len(expected_pairs) @@ -135,7 +129,6 @@ def test_to_args_mixed_configuration(self): ) args = config.to_args() - assert "-no-window" in args assert "-wipe-data" in args assert "-verbose" in args @@ -194,7 +187,6 @@ def test_immutability_after_creation(self): """Test that configuration can be modified after creation (dataclass is mutable).""" config = EmulatorConfiguration() - config.no_window = True config.memory = 1024 @@ -251,7 +243,7 @@ class TestEmulatorConfigurationValidation: def test_memory_negative_value(self): """Test configuration with negative memory value.""" - + config = EmulatorConfiguration(memory=-100) args = config.to_args() @@ -279,7 +271,6 @@ def test_empty_string_values(self): config = EmulatorConfiguration(gpu="", netdelay="", netspeed="") args = config.to_args() - assert "-gpu" in args assert "" in args assert "-netdelay" in args @@ -348,7 +339,6 @@ def test_value_parameters_parametrized( assert expected_flag in args assert expected_value in args - flag_index = args.index(expected_flag) assert args[flag_index + 1] == expected_value @@ -367,7 +357,6 @@ def test_default_values_not_in_args(field_name, default_value): config = EmulatorConfiguration(**kwargs) args = config.to_args() - assert args == [] @@ -439,12 +428,11 @@ def test_testing_configuration(self): wipe_data=True, no_snapshot=True, cold_boot=True, - netspeed="full", - netdelay="none", + netspeed="full", + netdelay="none", ) args = config.to_args() - expected_flags = [ "-no-window", "-no-audio", @@ -467,7 +455,6 @@ def test_performance_configuration(self): ) args = config.to_args() - assert "-gpu" in args and "host" in args assert "-memory" in args and "8192" in args assert "-cores" in args and "8" in args diff --git a/tests/utils/test_sdk_manager.py b/tests/utils/test_sdk_manager.py index 798d600..39b158e 100644 --- a/tests/utils/test_sdk_manager.py +++ b/tests/utils/test_sdk_manager.py @@ -47,7 +47,6 @@ def test_is_system_image_installed_false(manager): def test_is_system_image_installed_section_skips(manager): - output = make_sdkmanager_list_output( installed=[], available=["system-images;android-34;google_apis;x86_64"] ) From 36ae24b112a019766ee0d20b8eabbd0a26055b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 18:37:53 +0200 Subject: [PATCH 22/28] docs: add initial MkDocs documentation setup --- docs/index.md | 0 mkdocs.yml | 21 ++ pyproject.toml | 3 + uv.lock | 525 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 549 insertions(+) create mode 100644 docs/index.md create mode 100644 mkdocs.yml diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..e69de29 diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..122d260 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,21 @@ +site_name: Android Device Manager +site_author: Jérémy Woirhaye + +repo_name: jwoirhaye/android-device-manager-python +repo_url: https://github.com/jwoirhaye/android-device-manager-python + +theme: + name: material + features: + - navigation.tabs + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + docstring_style: google + +nav: + - Home: index.md \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8ff79ba..7dcd78d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,9 @@ markers = [ [dependency-groups] dev = [ + "mkdocs>=1.6.1", + "mkdocs-material>=9.6.16", + "mkdocstrings[python]>=0.30.0", "pytest>=8.4.1", "pytest-cov>=6.2.1", ] diff --git a/uv.lock b/uv.lock index 006e0d3..daddf6c 100644 --- a/uv.lock +++ b/uv.lock @@ -9,6 +9,9 @@ source = { editable = "." } [package.dev-dependencies] dev = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, { name = "pytest" }, { name = "pytest-cov" }, ] @@ -17,10 +20,118 @@ dev = [ [package.metadata.requires-dev] dev = [ + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-material", specifier = ">=9.6.16" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.0" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-cov", specifier = ">=6.2.1" }, ] +[[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 = "backrefs" +version = "5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -132,6 +243,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffe" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/d1/3f3a1771fab90bddcb7437ceb407179f216cd9e72da3b0c165397445a784/griffe-1.10.0.tar.gz", hash = "sha256:7fe89ebfb5140e0589748888b99680968e5b9ef7e2dcb2b01caf87ec552b66be", size = 409727, upload-time = "2025-08-06T09:19:22.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/dd/00256082bf552b88373fd871526737ed456e2b7a0a5c97588d169d049c16/griffe-1.10.0-py3-none-any.whl", hash = "sha256:a5eec6d5431cc49eb636b8a078d2409844453c1b0e556e4ba26f8c923047cd11", size = 137120, upload-time = "2025-08-06T09:19:21.009Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -141,6 +285,214 @@ 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 = "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 = "markdown" +version = "3.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969, upload-time = "2025-05-20T13:09:08.237Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.6.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/84/aec27a468c5e8c27689c71b516fb5a0d10b8fca45b9ad2dd9d6e43bc4296/mkdocs_material-9.6.16.tar.gz", hash = "sha256:d07011df4a5c02ee0877496d9f1bfc986cfb93d964799b032dd99fe34c0e9d19", size = 4028828, upload-time = "2025-07-26T15:53:47.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f4/90ad67125b4dd66e7884e4dbdfab82e3679eb92b751116f8bb25ccfe2f0c/mkdocs_material-9.6.16-py3-none-any.whl", hash = "sha256:8d1a1282b892fe1fdf77bfeb08c485ba3909dd743c9ba69a19a40f637c6ec18c", size = 9223743, upload-time = "2025-07-26T15:53:44.236Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.16.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -150,6 +502,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -168,6 +547,19 @@ 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 = "pymdown-extensions" +version = "10.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, +] + [[package]] name = "pytest" version = "8.4.1" @@ -200,6 +592,98 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, ] +[[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 = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[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 = "tomli" version = "2.2.1" @@ -247,3 +731,44 @@ sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09 wheels = [ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] From 5400e6ddd05fa99d720b44a6fde95546e2395542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 19:19:06 +0200 Subject: [PATCH 23/28] docs: add initial documentation with MkDocs --- docs/api/adb.md | 7 + docs/api/android_device.md | 6 + docs/api/avd.md | 9 + docs/api/constants.md | 3 + docs/api/emulator.md | 9 + docs/api/index.md | 13 ++ docs/examples/advanced/adbclient.md | 164 +++++++++++++++++ docs/examples/advanced/avdmanager.md | 123 +++++++++++++ docs/examples/advanced/emulatormanager.md | 149 ++++++++++++++++ docs/examples/advanced/index.md | 10 ++ docs/examples/apk-management.md | 33 ++++ docs/examples/automation.md | 47 +++++ docs/examples/basic-usage.md | 32 ++++ docs/examples/device-info.md | 34 ++++ docs/examples/file-operations.md | 27 +++ docs/getting-started/installation.md | 87 +++++++++ docs/getting-started/quickstart.md | 71 ++++++++ docs/index.md | 68 +++++++ docs/troubleshooting.md | 3 + mkdocs.yml | 48 ++++- pyproject.toml | 2 +- src/android_device_manager/__init__.py | 6 +- uv.lock | 206 +++++++++++++++++++++- 23 files changed, 1152 insertions(+), 5 deletions(-) create mode 100644 docs/api/adb.md create mode 100644 docs/api/android_device.md create mode 100644 docs/api/avd.md create mode 100644 docs/api/constants.md create mode 100644 docs/api/emulator.md create mode 100644 docs/api/index.md create mode 100644 docs/examples/advanced/adbclient.md create mode 100644 docs/examples/advanced/avdmanager.md create mode 100644 docs/examples/advanced/emulatormanager.md create mode 100644 docs/examples/advanced/index.md create mode 100644 docs/examples/apk-management.md create mode 100644 docs/examples/automation.md create mode 100644 docs/examples/basic-usage.md create mode 100644 docs/examples/device-info.md create mode 100644 docs/examples/file-operations.md create mode 100644 docs/getting-started/installation.md create mode 100644 docs/getting-started/quickstart.md create mode 100644 docs/troubleshooting.md diff --git a/docs/api/adb.md b/docs/api/adb.md new file mode 100644 index 0000000..07c9233 --- /dev/null +++ b/docs/api/adb.md @@ -0,0 +1,7 @@ +# ADB Module + +Interact with Android Debug Bridge (ADB). + +::: android_device_manager.adb.client + +::: android_device_manager.adb.exceptions \ No newline at end of file diff --git a/docs/api/android_device.md b/docs/api/android_device.md new file mode 100644 index 0000000..8cda396 --- /dev/null +++ b/docs/api/android_device.md @@ -0,0 +1,6 @@ +# AndroidDevice + +High-level class for managing Android devices. + +::: android_device_manager.android_device.AndroidDevice + diff --git a/docs/api/avd.md b/docs/api/avd.md new file mode 100644 index 0000000..f4211d2 --- /dev/null +++ b/docs/api/avd.md @@ -0,0 +1,9 @@ +# AVD Module + +Manage Android Virtual Devices (AVDs). + +::: android_device_manager.avd.AVDManager + +::: android_device_manager.avd.AVDConfiguration + +::: android_device_manager.avd.exceptions \ No newline at end of file diff --git a/docs/api/constants.md b/docs/api/constants.md new file mode 100644 index 0000000..b57cc78 --- /dev/null +++ b/docs/api/constants.md @@ -0,0 +1,3 @@ +# Constants + +::: android_device_manager.AndroidProp \ No newline at end of file diff --git a/docs/api/emulator.md b/docs/api/emulator.md new file mode 100644 index 0000000..75cb9d6 --- /dev/null +++ b/docs/api/emulator.md @@ -0,0 +1,9 @@ +# Emulator Module + +Emulate Android Virtual Devices (AVDs). + +::: android_device_manager.emulator.EmulatorManager + +::: android_device_manager.emulator.EmulatorConfiguration + +::: android_device_manager.emulator.exceptions \ No newline at end of file diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..06d979b --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,13 @@ +# API Reference + +Detailed API documentation for **Android Device Manager**. + +--- + +## 🔗 **Contents** + +- [AndroidDevice](android_device.md) +- [ADB](adb.md) +- [AVD](avd.md) +- [Emulator](emulator.md) +- [Constants](constants.md) \ No newline at end of file diff --git a/docs/examples/advanced/adbclient.md b/docs/examples/advanced/adbclient.md new file mode 100644 index 0000000..e8f1609 --- /dev/null +++ b/docs/examples/advanced/adbclient.md @@ -0,0 +1,164 @@ +# Advanced: Using `AdbClient` + +The `AdbClient` class provides **low-level access** to the Android Debug Bridge (ADB) for a running emulator/device. + +!!! info + + This interface is intended for **advanced users** who want to directly run commands, manage files, install/uninstall APKs, or query system information without going through the high-level `AndroidDevice` abstraction. + +--- + +## When to use `AdbClient` +You might prefer `AdbClient` over `AndroidDevice` when: +- You want **fine-grained control** over ADB commands. +- You need to **run custom shell commands**. +- You want to **manipulate files** or **query system properties** without creating/deleting AVDs. + +--- + +## Initializing `AdbClient` + +```python +from android_device_manager.adb.client import AdbClient +from android_device_manager.utils.android_sdk import AndroidSDK + +sdk = AndroidSDK() +port = 5554 # Replace with the actual emulator port + +adb_client = AdbClient(port, sdk) +adb_client.wait_for_boot() # Wait until the emulator is fully booted +print("Emulator is ready!") +``` + +--- + +## Key Features and Examples + +### 1️⃣ Query System Properties +Retrieve device properties (API level, Android version, manufacturer, etc.). + +```python +from android_device_manager.constants import AndroidProp + +# Get a specific property +api_level = adb_client.get_prop(AndroidProp.API_LEVEL) +print("API Level:", api_level) + +# Or by string +android_version = adb_client.get_prop("ro.build.version.release") +print("Android Version:", android_version) + +# Get all properties +all_props = adb_client.get_all_props() +print("All properties:", all_props) +``` + +--- + +### 2️⃣ Install and Uninstall APKs +Install or remove applications from the emulator. + +```python +apk_path = "/path/to/app.apk" + +# Install APK +adb_client.install_apk(apk_path) +print("APK installed successfully.") + +# List installed packages +packages = adb_client.list_installed_packages() +print("Installed packages:", packages) + +# Uninstall APK +adb_client.uninstall_package("com.example.app") +print("Package uninstalled.") +``` + +--- + +### 3️⃣ File Operations +Copy files between the host and the emulator. + +```python +# Push file to /sdcard +adb_client.push_file("local_file.txt", "/sdcard/remote_file.txt") +print("File pushed to emulator.") + +# Pull file from /sdcard +adb_client.pull_file("/sdcard/remote_file.txt", "downloaded_file.txt") +print("File pulled from emulator.") +``` + +--- + +### 4️⃣ Log Management +Access or clear the device logs. + +```python +# Retrieve logcat output +logs = adb_client.get_logcat() +print("Logs:", logs) + +# Clear logcat buffer +adb_client.clear_logcat() +print("Logcat cleared.") +``` + +--- + +### 5️⃣ Execute Shell Commands +Run shell commands directly inside the emulator. + +```python +# List files in /sdcard +result = adb_client.shell(["ls", "/sdcard"]) +print("Files in /sdcard:", result.stdout) + +# Create a directory +adb_client.shell(["mkdir", "/sdcard/test_dir"]) +``` + +!!! warning + Avoid using destructive commands (`stop`, `reboot`, `poweroff`) unless you handle emulator state manually. + +--- + +### 6️⃣ Root Access +Gain root privileges (if supported by the emulator image). + +```python +# Enable root +if adb_client.root(): + print("ADB is now running as root.") +else: + print("Root access not available.") + +# Check root status +print("Is root?", adb_client.is_root()) + +# Disable root +adb_client.unroot() +print("Root disabled.") +``` + +--- + +### 7️⃣ Stop Emulator +Shut down the emulator instance cleanly. + +```python +adb_client.kill_emulator() +print("Emulator killed.") +``` + +--- + +## 🔍 Notes for Advanced Users +- `AdbClient` does **not manage emulator lifecycle** — you must ensure an emulator is running before using it. +- Some commands require `root()` to be called before execution. +- Avoid long-running or blocking commands in automation workflows without adjusting `timeout` parameters. +- For bulk file transfers (like `/data`), ensure proper permissions and storage space on the host. + +--- + +✅ `AdbClient` is ideal when you want **direct control of an emulator without the overhead** of creating/managing it via `AndroidDevice`. \ No newline at end of file diff --git a/docs/examples/advanced/avdmanager.md b/docs/examples/advanced/avdmanager.md new file mode 100644 index 0000000..af20fd4 --- /dev/null +++ b/docs/examples/advanced/avdmanager.md @@ -0,0 +1,123 @@ +# AVDManager — Advanced Usage + +The `AVDManager` class provides programmatic access to the Android Virtual Device (AVD) management functionalities, wrapping the `avdmanager` CLI commands with Python. + +It can be used directly to create, list, and delete AVDs without relying on the higher-level `AndroidDevice` abstraction. + +--- + +## 📦 Import and Initialization + +To use `AVDManager`, you need an initialized `AndroidSDK` (which automatically locates your SDK tools): + +```python +from android_device_manager.avd.manager import AVDManager +from android_device_manager.avd.config import AVDConfiguration +from android_device_manager.utils.android_sdk import AndroidSDK + +# Initialize SDK and AVDManager +sdk = AndroidSDK() +avd_manager = AVDManager(sdk) +``` + +--- + +## 🆕 Creating an AVD + +The `create()` method creates a new virtual device. +You need an `AVDConfiguration` specifying at least a name and a system image package. + +```python +avd_config = AVDConfiguration( + name="advanced_avd", + package="system-images;android-34;google_apis;x86_64" +) + +try: + avd_manager.create(avd_config, force=False) + print("AVD created successfully.") +except Exception as e: + print(f"Failed to create AVD: {e}") +``` + +!!! note + + - The AVD name must follow Android naming rules (letters, digits, `_`, `-`, starting with a letter). + - If `force=True` is set, an existing AVD with the same name will be overwritten. + +--- + +## 📜 Listing AVDs + +You can retrieve the list of existing AVDs using the `list()` method. + +```python +avd_list = avd_manager.list() +print("Available AVDs:") +for avd in avd_list: + print(f" - {avd}") +``` + +Example output: + +``` +Available AVDs: + - Pixel_5_API_34 + - advanced_avd +``` + +--- + +## 🔍 Checking if an AVD Exists + +You can check for the existence of a specific AVD: + +```python +if avd_manager.exist("advanced_avd"): + print("The AVD exists.") +else: + print("The AVD does not exist.") +``` + +--- + +## 🗑️ Deleting an AVD + +The `delete()` method removes a specific AVD by name: + +```python +try: + avd_manager.delete("advanced_avd") + print("AVD deleted successfully.") +except Exception as e: + print(f"Failed to delete AVD: {e}") +``` + +If the AVD does not exist, `delete()` will log a warning but will **not raise an error**. + +--- + +## 🛠️ Under the Hood + +Internally, `AVDManager`: +- Uses `avdmanager create avd` and `avdmanager delete avd` CLI commands. +- Validates AVD names with `is_valid_avd_name()`. +- Checks if the system image package is installed through `SDKManager`. + +Advanced users can directly call `_run_avd_command()` to execute raw `avdmanager` commands: + +```python +result = avd_manager._run_avd_command(["list", "avd"], timeout=30) +print(result.stdout) +``` + +--- + +## ⚠️ Common Pitfalls +- **System image not installed**: Ensure your package (`system-images;android-XX;google_apis;x86_64`) is installed via `sdkmanager`. +- **Invalid names**: Follow Android AVD naming rules (no spaces, must start with a letter). +- **Permissions**: Ensure you have write access to the `.android/avd` directory. + +--- + +👉 This advanced control over AVDs is useful when you want to script emulator environments, clean up AVDs, or prepare devices dynamically in CI pipelines. \ No newline at end of file diff --git a/docs/examples/advanced/emulatormanager.md b/docs/examples/advanced/emulatormanager.md new file mode 100644 index 0000000..af2cc0c --- /dev/null +++ b/docs/examples/advanced/emulatormanager.md @@ -0,0 +1,149 @@ +# Advanced Usage: `EmulatorManager` + +The `EmulatorManager` is responsible for managing Android Emulator instances from Python, without relying on Android Studio's graphical interface. +It provides low-level control to **start, stop, and allocate emulator ports programmatically**. + +--- + +## Overview + +The `EmulatorManager` acts as a thin wrapper around the `emulator` binary found in your Android SDK. + +**Main features:** +- Find and allocate free emulator ports automatically. +- Start emulators with a specific AVD and optional configuration. +- Stop emulators gracefully or forcibly. +- Fully compatible with headless and automated environments. + +--- + +## Initialization + +To use `EmulatorManager`, you must provide an instance of `AndroidSDK` so the class can locate the `emulator` executable. + +```python +from android_device_manager.emulator import EmulatorManager +from android_device_manager.utils.android_sdk import AndroidSDK + +sdk = AndroidSDK() # Automatically detects the SDK path +emulator_manager = EmulatorManager(sdk) +``` + +--- + +## Starting an Emulator + +You can start an emulator by specifying the **AVD name**. +Optionally, you can provide an [`EmulatorConfiguration`](../../api/emulator.md#) to control runtime settings like GPU mode, memory, and cold boot. + +```python +from android_device_manager.emulator import EmulatorConfiguration + +# Configure emulator to run headless +emu_config = EmulatorConfiguration( + no_window=True, + gpu="swiftshader_indirect", + cold_boot=True +) + +# Start emulator for AVD named "test_avd" +port = emulator_manager.start_emulator("test_avd", emulator_config=emu_config) + +print(f"Emulator started on port {port}") +``` + +**Key details:** + +- A free port is automatically selected between `DEFAULT_EMULATOR_PORT_START` and `DEFAULT_EMULATOR_PORT_END`. +- If no free port is available, an `EmulatorPortAllocationError` is raised. +- If the emulator fails to start, an `EmulatorStartError` is raised with logs. + +--- + +## Stopping an Emulator + +Stopping the emulator is straightforward. +The manager first attempts to terminate the process gracefully, and if it does not stop within **10 seconds**, it is killed. + +```python +emulator_manager.stop_emulator() +print("Emulator stopped.") +``` + +--- + +## Port Allocation + +By default, emulator ports are allocated in **even-numbered ranges** starting at `5554`. +This is the same behavior as the official Android Emulator. + +You can find a free port using the private static method `_find_free_emulator_port`: + +```python +free_port = emulator_manager._find_free_emulator_port() +print(f"Free emulator port: {free_port}") +``` + +!!! warning + + This is an internal method. Normally, you don't need to call it manually because `start_emulator()` handles port allocation automatically. + +--- + +## Full Example + +Here is a complete advanced example that: +1. Starts an emulator in headless mode. +2. Runs it for 15 seconds. +3. Stops it gracefully. + +```python +import time +from android_device_manager.utils.android_sdk import AndroidSDK +from android_device_manager.emulator.manager import EmulatorManager +from android_device_manager.emulator.config import EmulatorConfiguration + +# Initialize SDK and Emulator Manager +sdk = AndroidSDK() +emulator_manager = EmulatorManager(sdk) + +# Emulator configuration (headless) +emu_config = EmulatorConfiguration(no_window=True, cold_boot=True) + +# Start emulator +try: + port = emulator_manager.start_emulator("test_avd", emulator_config=emu_config) + print(f"Emulator started on port {port}") + + # Simulate work + time.sleep(15) + +finally: + # Stop emulator + emulator_manager.stop_emulator() + print("Emulator stopped.") +``` + +--- + +## Best Practices + +- Always stop emulators after usage to free system resources. +- Use `no_window=True` in automated pipelines to avoid UI pop-ups. +- Combine `EmulatorManager` with [`AdbClient`](./adbclient.md) for deeper automation (installing APKs, running tests, etc.). +- If running multiple emulators, ensure there are enough free ports. + +--- + +## Exceptions + +The `EmulatorManager` raises specific exceptions for better error handling: + +- **`EmulatorPortAllocationError`** + Raised when no free emulator port can be found. + +- **`EmulatorStartError`** + Raised when the emulator process fails to start. + +These exceptions can be caught individually or through the base exception class. + diff --git a/docs/examples/advanced/index.md b/docs/examples/advanced/index.md new file mode 100644 index 0000000..8c903c9 --- /dev/null +++ b/docs/examples/advanced/index.md @@ -0,0 +1,10 @@ +# Advanced Examples + +This section provides **low-level usage examples** for advanced users who want to interact +directly with individual components instead of the high-level `AndroidDevice` abstraction. + +Available advanced examples: + +- [Using AdbClient](adbclient.md) +- [Using AVDManager](avdmanager.md) +- [Using EmulatorManager](emulatormanager.md) \ No newline at end of file diff --git a/docs/examples/apk-management.md b/docs/examples/apk-management.md new file mode 100644 index 0000000..d84ea0d --- /dev/null +++ b/docs/examples/apk-management.md @@ -0,0 +1,33 @@ +# APK Management + +## Install, list, and uninstall APKs + +```python +from android_device_manager import AndroidDevice +from android_device_manager.avd import AVDConfiguration +from android_device_manager.emulator import EmulatorConfiguration + +# Configure your AVD +avd_config = AVDConfiguration( + name="example_avd", + package="system-images;android-34;google_apis;x86_64" +) + +# Configure your Emulator +emulator_config = EmulatorConfiguration( + no_window=True +) + +with AndroidDevice(avd_config) as device: + # Install APK + device.install_apk("/path/to/app.apk") + + # List installed packages + packages = device.list_installed_packages() + print("Installed packages:") + for p in packages: + print(f"\t- {p}") + + # Uninstall package + device.uninstall_package("com.example.app") +``` \ No newline at end of file diff --git a/docs/examples/automation.md b/docs/examples/automation.md new file mode 100644 index 0000000..a1f6264 --- /dev/null +++ b/docs/examples/automation.md @@ -0,0 +1,47 @@ +# Automation Workflow + +## Full Automated Test Setup + +This example demonstrates a full automation scenario: +- Create and start an emulator +- Install an application +- Retrieve logs +- Capture a screenshot + +```python +from android_device_manager import AndroidDevice +from android_device_manager.avd.config import AVDConfiguration +from android_device_manager.emulator.config import EmulatorConfiguration + +# Configure the AVD +avd_config = AVDConfiguration( + name="automation_avd", + package="system-images;android-34;google_apis;x86_64" +) + +# Configure the Emulator +emulator_config = EmulatorConfiguration( + no_window=True +) + +# APK path +apk_path = "/path/to/app.apk" + +with AndroidDevice(avd_config, emulator_config) as device: + print(f"Device {device.name} started.") + + # Install application + device.install_apk(apk_path) + print("APK installed.") + + # Retrieve logs + logs = device.get_logcat() + print("Captured logs:") + print(logs) + + # Capture screenshot + screenshot_path = "screenshot.png" + device.shell(["screencap", "-p", f"/tmp/{screenshot_path}"]) + device.pull_file(f"/tmp/{screenshot_path}", screenshot_path) + print(f"Screenshot saved to {screenshot_path}") +``` \ No newline at end of file diff --git a/docs/examples/basic-usage.md b/docs/examples/basic-usage.md new file mode 100644 index 0000000..7e3b693 --- /dev/null +++ b/docs/examples/basic-usage.md @@ -0,0 +1,32 @@ +# Basic Usage + +## Create, start, and stop an emulator with custom configuration + +```python +from android_device_manager import AndroidDevice +from android_device_manager.avd import AVDConfiguration +from android_device_manager.emulator import EmulatorConfiguration + +# Configure your AVD +avd_config = AVDConfiguration( + name="example_avd", + package="system-images;android-34;google_apis;x86_64" +) + +# Configure your Emulator +emulator_config = EmulatorConfiguration( + no_window=True +) + +# Manage the device lifecycle automatically +with AndroidDevice(avd_config, emulator_config) as device: + print(f"Device {device.name} is running.") +``` + +This example: + +1. Creates the AVD if it does not exist. + +2. Starts the emulator in headless mode (no window). + +3. Stops and deletes the AVD automatically when exiting the context. \ No newline at end of file diff --git a/docs/examples/device-info.md b/docs/examples/device-info.md new file mode 100644 index 0000000..82e5833 --- /dev/null +++ b/docs/examples/device-info.md @@ -0,0 +1,34 @@ +# Device Info & Properties + +## Retrieve system properties + +```python +from android_device_manager import AndroidDevice, AndroidProp +from android_device_manager.avd import AVDConfiguration +from android_device_manager.emulator import EmulatorConfiguration + +# Configure the AVD +avd_config = AVDConfiguration( + name="test_avd_from_lib", + package="system-images;android-36;google_apis;x86_64" +) + +# Configure the Emulator (headless mode) +emulator_config = EmulatorConfiguration( + no_window=True, +) + +# Start and query the device +with AndroidDevice(avd_config, emulator_config=emulator_config) as device: + api_level = device.get_prop(AndroidProp.API_LEVEL) + android_version = device.get_prop("ro.build.version.release") + print(f"API Level: {api_level}, Android: {android_version}") +``` + +This example: + +1. Starts an emulator for the specified AVD in headless mode. + +2. Retrieves the device API level using an AndroidProp enum. + +3. Retrieves the Android version directly using a property key. \ No newline at end of file diff --git a/docs/examples/file-operations.md b/docs/examples/file-operations.md new file mode 100644 index 0000000..652b776 --- /dev/null +++ b/docs/examples/file-operations.md @@ -0,0 +1,27 @@ +# File Operations + +## Push and Pull Files + +```python +from android_device_manager import AndroidDevice +from android_device_manager.avd import AVDConfiguration +from android_device_manager.emulator import EmulatorConfiguration + +# Configure the AVD +avd_config = AVDConfiguration( + name="test_avd_from_lib", + package="system-images;android-36;google_apis;x86_64" +) + +# Configure the Emulator (headless mode) +emulator_config = EmulatorConfiguration( + no_window=True, +) + +with AndroidDevice(avd_config, emulator_config=emulator_config) as device: + # Push a file + device.push_file("local.txt", "/tmp/local.txt") + + # Pull a file + device.pull_file("/tmp/local.txt", "downloaded.txt") +``` \ No newline at end of file diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 0000000..e94d1c9 --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,87 @@ +# Installation + +## 🐍 Requirements + +Before installing android-device-manager, ensure you have: + +- Python 3.10 or higher +- Android SDK installed and configured + +--- + +## Android SDK Setup + +### Option 1: Android Studio +1. Download and install [Android Studio](https://developer.android.com/studio) +2. Open Android Studio and follow the setup wizard +3. Install additional SDK packages through SDK Manager + +### Option 2: Command Line Tools + +#### Installing Command Line Tools + +```bash +# Download command line tools +# August 2025 version - check for the latest version at: +# https://developer.android.com/studio#command-line-tools-only +wget https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip + +# Extract and organize files +unzip commandlinetools-linux-13114758_latest.zip +mkdir -p ~/Android/Sdk/cmdline-tools/latest +mv cmdline-tools/* ~/Android/Sdk/cmdline-tools/latest/ + +# Cleanup +rm -rf cmdline-tools commandlinetools-linux-13114758_latest.zip +``` + +#### Setting Up Environment Variables + +```bash +# Add these lines to your ~/.bashrc or ~/.zshrc +export ANDROID_HOME=$HOME/Android/Sdk +export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin +export PATH=$PATH:$ANDROID_HOME/platform-tools +export PATH=$PATH:$ANDROID_HOME/emulator + +# Reload your configuration +source ~/.bashrc # or source ~/.zshrc +``` + +#### Installing Essential Components + +```bash +# Accept licenses +yes | sdkmanager --licenses + +# Install basic tools +sdkmanager "platform-tools" +sdkmanager "emulator" +``` + +#### Verifying Installation + +To verify everything works correctly: + +```bash +# Check that tools are accessible +adb version +emulator -version + +# List installed packages +sdkmanager --list_installed +``` + +## Install python-android-avd-manager + +### 📦 From PyPI (Recommended) +```bash +pip install python-android-avd-manager +``` + +### 🚧 From Source +```bash +git clone https://github.com/jwoirhaye/python-android-avd-manager-python.git +cd python-android-avd-manager +pip install -e . +``` \ No newline at end of file diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md new file mode 100644 index 0000000..9fe53dc --- /dev/null +++ b/docs/getting-started/quickstart.md @@ -0,0 +1,71 @@ +# Quickstart + +Welcome to **Android Device Manager**! +This quickstart guide will walk you through the essential steps to create, run, and interact with an Android Device using the library. + +## 1️⃣ Verify Installation + +After following the [Installation Guide](installation.md), verify your setup: + +```bash +adb version +emulator -version +avdmanager list available +``` + +You should see the versions of each tool and the list of available system images. + +--- + +## 2️⃣ Minimal Example — Create & Run an Emulator + +With everything set up, here’s the simplest way to create and run an emulator: + +```python +from android_device_manager import AndroidDevice +from android_device_manager.avd import AVDConfiguration +from android_device_manager.emulator import EmulatorConfiguration + +# Define AVD configuration +avd_config = AVDConfiguration( + name="quickstart_avd", + package="system-images;android-34;google_apis;x86_64" +) + +# Define Emulator configuration +emulator_config = EmulatorConfiguration( + no_window=True # Run emulator in headless mode +) + +# Create and run the device using context manager +with AndroidDevice(avd_config, emulator_config) as device: + print(f"Device {device.name} is running.") + +``` + +✅ What happens here: + +- The AVD is created if it doesn’t already exist. +- The emulator is started. +- When the context exits, the emulator is stopped and the AVD is cleaned up. + +--- + +## 3️⃣ Interact with the Device + +While the emulator is running, you can: +[constants](../api/constants.md) +- Install APKs +- List installed packages +- Read system properties + +Example: +```python +from android_device_manager import AndroidProp + +packages = device.list_installed_packages() +print("Installed packages:", packages[:5]) + +android_version = device.get_prop(AndroidProp.ANDROID_VERSION) +print(f"Android version: {android_version}") +``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index e69de29..12f29be 100644 --- a/docs/index.md +++ b/docs/index.md @@ -0,0 +1,68 @@ +# Android Device Manager + +**Android Device Manager** is a modern Python library to **automate and control Android** programmatically. + +## 🚀 Features + +### 📦 AVD Management +- **Create AVDs programmatically** from system images +- **List existing AVDs** and check availability +- **Delete AVDs** cleanly +- **Validate AVD names** according to Android rules +- **Force recreation** of AVDs with `force=True` + +### ▶️ Emulator Control +- **Start emulators** in headless or windowed mode +- **Automatic port allocation** for multiple running instances +- **Stop emulators** gracefully or force-kill when needed +- **Custom emulator options** via `EmulatorConfiguration` + +### 📡 ADB Integration +- **Execute `adb` commands** directly from Python +- **Install APKs** and manage applications (install/uninstall) +- **List installed packages** and check if a package is installed +- **Push and pull files** between host and device +- **Access `logcat` output** and clear logs + +--- + +## 🐍 Requirements + +- **Python**: 3.10 or higher +- **Android SDK**: Latest version recommended +- **System Resources**: Sufficient RAM and storage for emulators + +--- + +## 📦 Installation + +Follow the [Installation Guide](getting-started/installation.md) + +```bash +pip install android-device-manager +``` + +--- + +## ⚡ Quick Example + +```python +from android_device_manager import AndroidDevice +from android_device_manager.avd import AVDConfiguration +from android_device_manager.emulator import EmulatorConfiguration + +avd_config = AVDConfiguration( + name="test_avd_from_lib", + package="system-images;android-36;google_apis;x86_64" +) + +emulator_config = EmulatorConfiguration( + no_window=True, +) + +with AndroidDevice(avd_config,emulator_config) as device: + print(device.name) +``` + + + diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..550ce5d --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,3 @@ +# Troubleshooting + +This section describes common issues you may encounter when using `android-device-manager`, along with recommended solutions. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 122d260..f5e38a2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,7 @@ site_name: Android Device Manager site_author: Jérémy Woirhaye +site_description: "A Python library for managing Android emulators and devices" +site_url: https://jwoirhaye.com/android-device-manager-python repo_name: jwoirhaye/android-device-manager-python repo_url: https://github.com/jwoirhaye/android-device-manager-python @@ -8,14 +10,58 @@ theme: name: material features: - navigation.tabs + - navigation.top + - content.code.copy + icon: + repo: fontawesome/brands/github + plugins: - search + - social - mkdocstrings: handlers: python: options: docstring_style: google + show_source: false + show_root_heading: true + members_order: source + show_category_heading: true + +markdown_extensions: + - admonition + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + + nav: - - Home: index.md \ No newline at end of file + - Home: index.md + - Getting Started: + - Quickstart: getting-started/quickstart.md + - Installation: getting-started/installation.md + - API Reference: + - Overview: api/index.md + - AndroidDevice: api/android_device.md + - ADB: api/adb.md + - AVD: api/avd.md + - Emulator: api/emulator.md + - Constants: api/constants.md + - Examples: + - Basic usage: examples/basic-usage.md + - APK management: examples/apk-management.md + - Device info & properties: examples/device-info.md + - File operations: examples/file-operations.md + - Automation: examples/automation.md + - Advanced: + - Overview: examples/advanced/index.md + - Using AdbClient: examples/advanced/adbclient.md + - Using AVDManager: examples/advanced/avdmanager.md + - Using EmulatorManager: examples/advanced/emulatormanager.md + - Troubleshooting: troubleshooting.md \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7dcd78d..6cdca7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ markers = [ [dependency-groups] dev = [ "mkdocs>=1.6.1", - "mkdocs-material>=9.6.16", + "mkdocs-material[imaging]>=9.6.16", "mkdocstrings[python]>=0.30.0", "pytest>=8.4.1", "pytest-cov>=6.2.1", diff --git a/src/android_device_manager/__init__.py b/src/android_device_manager/__init__.py index 9d981cd..f0907c9 100644 --- a/src/android_device_manager/__init__.py +++ b/src/android_device_manager/__init__.py @@ -1,3 +1,7 @@ from .android_device import AndroidDevice +from .constants import AndroidProp -__all__ = ["AndroidDevice"] +__all__ = [ + "AndroidDevice", + "AndroidProp", +] diff --git a/uv.lock b/uv.lock index daddf6c..07aa62f 100644 --- a/uv.lock +++ b/uv.lock @@ -10,7 +10,7 @@ source = { editable = "." } [package.dev-dependencies] dev = [ { name = "mkdocs" }, - { name = "mkdocs-material" }, + { name = "mkdocs-material", extra = ["imaging"] }, { name = "mkdocstrings", extra = ["python"] }, { name = "pytest" }, { name = "pytest-cov" }, @@ -21,7 +21,7 @@ dev = [ [package.metadata.requires-dev] dev = [ { name = "mkdocs", specifier = ">=1.6.1" }, - { name = "mkdocs-material", specifier = ">=9.6.16" }, + { name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.6.16" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.0" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-cov", specifier = ">=6.2.1" }, @@ -50,6 +50,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, ] +[[package]] +name = "cairocffi" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" }, +] + +[[package]] +name = "cairosvg" +version = "2.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cairocffi" }, + { name = "cssselect2" }, + { name = "defusedxml" }, + { name = "pillow" }, + { name = "tinycss2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/b9/5106168bd43d7cd8b7cc2a2ee465b385f14b63f4c092bb89eee2d48c8e67/cairosvg-2.8.2.tar.gz", hash = "sha256:07cbf4e86317b27a92318a4cac2a4bb37a5e9c1b8a27355d06874b22f85bef9f", size = 8398590, upload-time = "2025-05-15T06:56:32.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/48/816bd4aaae93dbf9e408c58598bc32f4a8c65f4b86ab560864cb3ee60adb/cairosvg-2.8.2-py3-none-any.whl", hash = "sha256:eab46dad4674f33267a671dce39b64be245911c901c70d65d2b7b0821e852bf5", size = 45773, upload-time = "2025-05-15T06:56:28.552Z" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -59,6 +87,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.2" @@ -231,6 +316,28 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cssselect2" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tinycss2" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -447,6 +554,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/65/f4/90ad67125b4dd66e7884e4dbdfab82e3679eb92b751116f8bb25ccfe2f0c/mkdocs_material-9.6.16-py3-none-any.whl", hash = "sha256:8d1a1282b892fe1fdf77bfeb08c485ba3909dd743c9ba69a19a40f637c6ec18c", size = 9223743, upload-time = "2025-07-26T15:53:44.236Z" }, ] +[package.optional-dependencies] +imaging = [ + { name = "cairosvg" }, + { name = "pillow" }, +] + [[package]] name = "mkdocs-material-extensions" version = "1.3.1" @@ -520,6 +633,65 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] +[[package]] +name = "pillow" +version = "10.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059, upload-time = "2024-07-01T09:48:43.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271, upload-time = "2024-07-01T09:45:22.07Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", size = 3375658, upload-time = "2024-07-01T09:45:25.292Z" }, + { url = "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", size = 4332075, upload-time = "2024-07-01T09:45:27.94Z" }, + { url = "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", size = 4444808, upload-time = "2024-07-01T09:45:30.305Z" }, + { url = "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", size = 4356290, upload-time = "2024-07-01T09:45:32.868Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", size = 4525163, upload-time = "2024-07-01T09:45:35.279Z" }, + { url = "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", size = 4463100, upload-time = "2024-07-01T09:45:37.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", size = 4592880, upload-time = "2024-07-01T09:45:39.89Z" }, + { url = "https://files.pythonhosted.org/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", size = 2235218, upload-time = "2024-07-01T09:45:42.771Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", size = 2554487, upload-time = "2024-07-01T09:45:45.176Z" }, + { url = "https://files.pythonhosted.org/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1", size = 2243219, upload-time = "2024-07-01T09:45:47.274Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265, upload-time = "2024-07-01T09:45:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655, upload-time = "2024-07-01T09:45:52.462Z" }, + { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304, upload-time = "2024-07-01T09:45:55.006Z" }, + { url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804, upload-time = "2024-07-01T09:45:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126, upload-time = "2024-07-01T09:46:00.713Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541, upload-time = "2024-07-01T09:46:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616, upload-time = "2024-07-01T09:46:05.356Z" }, + { url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802, upload-time = "2024-07-01T09:46:08.145Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213, upload-time = "2024-07-01T09:46:10.211Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498, upload-time = "2024-07-01T09:46:12.685Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219, upload-time = "2024-07-01T09:46:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350, upload-time = "2024-07-01T09:46:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980, upload-time = "2024-07-01T09:46:19.169Z" }, + { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799, upload-time = "2024-07-01T09:46:21.883Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973, upload-time = "2024-07-01T09:46:24.321Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054, upload-time = "2024-07-01T09:46:26.825Z" }, + { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484, upload-time = "2024-07-01T09:46:29.355Z" }, + { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375, upload-time = "2024-07-01T09:46:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773, upload-time = "2024-07-01T09:46:33.73Z" }, + { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690, upload-time = "2024-07-01T09:46:36.587Z" }, + { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951, upload-time = "2024-07-01T09:46:38.777Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427, upload-time = "2024-07-01T09:46:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685, upload-time = "2024-07-01T09:46:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883, upload-time = "2024-07-01T09:46:47.331Z" }, + { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837, upload-time = "2024-07-01T09:46:49.647Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562, upload-time = "2024-07-01T09:46:51.811Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761, upload-time = "2024-07-01T09:46:53.961Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767, upload-time = "2024-07-01T09:46:56.664Z" }, + { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989, upload-time = "2024-07-01T09:46:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255, upload-time = "2024-07-01T09:47:01.189Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603, upload-time = "2024-07-01T09:47:03.918Z" }, + { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972, upload-time = "2024-07-01T09:47:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375, upload-time = "2024-07-01T09:47:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", size = 3493889, upload-time = "2024-07-01T09:48:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", size = 3346160, upload-time = "2024-07-01T09:48:07.206Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", size = 3435020, upload-time = "2024-07-01T09:48:09.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", size = 3490539, upload-time = "2024-07-01T09:48:12.529Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", size = 3476125, upload-time = "2024-07-01T09:48:14.891Z" }, + { url = "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", size = 3579373, upload-time = "2024-07-01T09:48:17.601Z" }, + { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661, upload-time = "2024-07-01T09:48:20.293Z" }, +] + [[package]] name = "platformdirs" version = "4.3.8" @@ -538,6 +710,15 @@ 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 = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -684,6 +865,18 @@ 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 = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + [[package]] name = "tomli" version = "2.2.1" @@ -772,3 +965,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] From d5b003d83555b6694e44f20c9bb596c90b298bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 19:47:36 +0200 Subject: [PATCH 24/28] ci: add initial CD pipeline setup --- .github/workflows/cd.yml | 37 ++++ pyproject.toml | 12 ++ uv.lock | 371 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 417 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/cd.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..94c2904 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,37 @@ +name: CD + +on: + push: + branches: [main,feature/cd-pipeline] + +permissions: + contents: write + id-token: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: "pyproject.toml" + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Install the project + run: uv sync --extra build + + - name: Release | Create Release + id: release + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + uv run semantic-release -v --strict version --skip-build + uv run semantic-release publish \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6cdca7e..689a024 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,4 +33,16 @@ dev = [ "mkdocstrings[python]>=0.30.0", "pytest>=8.4.1", "pytest-cov>=6.2.1", + "python-semantic-release>=10.3.1", ] + +[tool.semantic_release] +build_command = """ + uv lock --upgrade-package "$PACKAGE_NAME" + git add uv.lock + uv build +""" + +[tool.semantic_release.branches.test] +match = "feature/cd-pipeline" +prerelease = false diff --git a/uv.lock b/uv.lock index 07aa62f..79ada96 100644 --- a/uv.lock +++ b/uv.lock @@ -14,6 +14,7 @@ dev = [ { name = "mkdocstrings", extra = ["python"] }, { name = "pytest" }, { name = "pytest-cov" }, + { name = "python-semantic-release" }, ] [package.metadata] @@ -25,6 +26,16 @@ dev = [ { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.0" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-cov", specifier = ">=6.2.1" }, + { name = "python-semantic-release", specifier = ">=10.3.1" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] @@ -207,14 +218,26 @@ wheels = [ [[package]] name = "click" -version = "8.2.1" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +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/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { 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.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/9f/1f917934da4e07ae7715a982347e3c2179556d8a58d1108c5da3e8f09c76/click_option_group-0.5.7.tar.gz", hash = "sha256:8dc780be038712fc12c9fecb3db4fe49e0d0723f9c171d7cda85c20369be693c", size = 22110, upload-time = "2025-03-24T13:24:55.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/27/bf74dc1494625c3b14dbcdb93740defd7b8c58dae3736be8d264f2a643fb/click_option_group-0.5.7-py3-none-any.whl", hash = "sha256:96b9f52f397ef4d916f81929bd6c1f85e89046c7a401a64e72a61ae74ad35c24", size = 11483, upload-time = "2025-03-24T13:24:54.611Z" }, ] [[package]] @@ -338,6 +361,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] +[[package]] +name = "deprecated" +version = "1.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, +] + +[[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.0" @@ -362,6 +406,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] +[[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.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, +] + [[package]] name = "griffe" version = "1.10.0" @@ -383,6 +451,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +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" @@ -413,6 +490,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, ] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +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 = "markupsafe" version = "3.0.2" @@ -471,6 +560,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] +[[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 = "mergedeep" version = "1.3.4" @@ -719,6 +817,108 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] +[[package]] +name = "pydantic" +version = "2.11.7" +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/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -785,6 +985,43 @@ 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.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "requests-toolbelt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/32/8b1f293d106ec69033eb29dc998ff8a86fdbce5eebc1f6af8835b2faef61/python_gitlab-6.2.0.tar.gz", hash = "sha256:b88c79cea65dd2425922c829730ea95827ed7132d869b8532b90a8c7199cc1a6", size = 397611, upload-time = "2025-07-28T01:25:56.294Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/4b/aa99112a09c2898e17b88245a1d2e5bfcc71016f3cbdc24b6d870de38cf9/python_gitlab-6.2.0-py3-none-any.whl", hash = "sha256:8adf2bbf1ac8a5224ee04a456d318da0d15128606711e8c8e1a2ff050968432b", size = 144242, upload-time = "2025-07-28T01:25:54.408Z" }, +] + +[[package]] +name = "python-semantic-release" +version = "10.3.1" +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/af/75/fb578d1805dc482f619c82431b398ab3c0489817a1cdbe2fa1be48d3d7d3/python_semantic_release-10.3.1.tar.gz", hash = "sha256:16003292315ee29b6ad424fad9745946422cf965e19649269c4e2fb8271cb267", size = 323536, upload-time = "2025-08-06T04:41:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/49/0a74aafd0da45a7ee2c38b82112d576ee48a6795d813c8025cdf9cbaf159/python_semantic_release-10.3.1-py3-none-any.whl", hash = "sha256:b2677018e761dfbfdf5758eeee6b09963e136d67c43059f12f9bc6c441fcbf13", size = 142065, upload-time = "2025-08-06T04:41:08.056Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -856,6 +1093,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] +[[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.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[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" @@ -865,6 +1136,15 @@ 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 = "tinycss2" version = "1.4.0" @@ -916,6 +1196,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] +[[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 = "typing-extensions" version = "4.14.1" @@ -925,6 +1214,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + [[package]] name = "urllib3" version = "2.5.0" @@ -974,3 +1275,67 @@ sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda5308 wheels = [ { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, ] + +[[package]] +name = "wrapt" +version = "1.17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307, upload-time = "2025-01-14T10:33:13.616Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486, upload-time = "2025-01-14T10:33:15.947Z" }, + { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777, upload-time = "2025-01-14T10:33:17.462Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314, upload-time = "2025-01-14T10:33:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947, upload-time = "2025-01-14T10:33:24.414Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778, upload-time = "2025-01-14T10:33:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716, upload-time = "2025-01-14T10:33:27.372Z" }, + { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548, upload-time = "2025-01-14T10:33:28.52Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334, upload-time = "2025-01-14T10:33:29.643Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427, upload-time = "2025-01-14T10:33:30.832Z" }, + { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774, upload-time = "2025-01-14T10:33:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" }, + { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" }, + { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" }, + { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" }, + { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" }, + { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" }, + { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" }, + { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" }, + { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload-time = "2025-01-14T10:34:29.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload-time = "2025-01-14T10:34:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" }, + { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload-time = "2025-01-14T10:34:52.297Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload-time = "2025-01-14T10:34:53.489Z" }, + { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" }, + { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" }, + { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" }, +] From 6eee883f35e3fde9af3ed3cc2855126b04cbf383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 19:48:58 +0200 Subject: [PATCH 25/28] ci: Add mkdoc deploy to CD pipeline --- .github/workflows/cd.yml | 32 ++++++++++++++++++++++++++++---- pyproject.toml | 3 ++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 94c2904..c8f9f7b 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -2,7 +2,7 @@ name: CD on: push: - branches: [main,feature/cd-pipeline] + branches: [main] permissions: contents: write @@ -25,7 +25,7 @@ jobs: uses: astral-sh/setup-uv@v6 - name: Install the project - run: uv sync --extra build + run: uv sync --all-groups - name: Release | Create Release id: release @@ -33,5 +33,29 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - uv run semantic-release -v --strict version --skip-build - uv run semantic-release publish \ No newline at end of file + uv run semantic-release -v --strict version + uv run semantic-release publish + + - name: Deploy to Pypi + run: | + uv publish + + deploy-docs: + runs-on: ubuntu-latest + needs: release + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: "pyproject.toml" + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Install the project + run: uv sync --all-groups + + - name: Publish documentation + run: uv run mkdocs gh-deploy --force diff --git a/pyproject.toml b/pyproject.toml index 689a024..ffca66b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,9 @@ dev = [ ] [tool.semantic_release] +version_toml = ["pyproject.toml:project.version"] build_command = """ - uv lock --upgrade-package "$PACKAGE_NAME" + uv lock --upgrade-package "android-device-manager" git add uv.lock uv build """ From 80be7045b3580c7bd0fab8a741828c784ec69d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 20:57:17 +0200 Subject: [PATCH 26/28] docs: Update Readme and add doc in pyproject --- README.md | 109 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b81fb03..e9675e7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,107 @@ -# android-device-manager-python -Android Device Manager is a Python library for creating, launching, and managing Android device programmatically. +# Android Device Manager Python +Android Device Manager is a Python library for creating, launching, and managing Android emulators (AVDs) programmatically. + +[![PyPI](https://img.shields.io/pypi/v/python-android-avd-manager?label=PyPI)](https://pypi.org/project/android-device-manager/) +[![CI](https://github.com/jwoirhaye/android-device-manager-python/actions/workflows/ci.yml/badge.svg)](https://github.com/jwoirhaye/android-device-manager-python/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/jwoirhaye/android-device-manager-python/branch/main/graph/badge.svg)](https://codecov.io/gh/jwoirhaye/android-device-manager-python) +[![Docs](https://img.shields.io/badge/docs-mkdocs-blue?logo=readthedocs)](https://jwoirhaye.github.io/android-device-manager-python/) +[![License](https://img.shields.io/github/license/jwoirhaye/android-device-manager-python)](https://github.com/jwoirhaye/android-device-manager-python/blob/main/LICENSE) +--- + +## 📖 Table of Contents +- [Features](#-features) +- [Requirements](#-requirements) +- [Installation](#-installation) +- [Quickstart](#-quickstart) +- [License](#-license) +- [Support](#-support) + +--- + +## 🚀 Features + +### 📦 AVD Management +- **Create AVDs programmatically** from system images +- **List existing AVDs** and check availability +- **Delete AVDs** cleanly +- **Validate AVD names** according to Android rules +- **Force recreation** of AVDs with `force=True` + +### ▶️ Emulator Control +- **Start emulators** in headless or windowed mode +- **Automatic port allocation** for multiple running instances +- **Stop emulators** gracefully or force-kill when needed +- **Custom emulator options** via `EmulatorConfiguration` + +### 📡 ADB Integration +- **Execute `adb` commands** directly from Python +- **Install APKs** and manage applications (install/uninstall) +- **List installed packages** and check if a package is installed +- **Push and pull files** between host and device +- **Access `logcat` output** and clear logs + +--- + +## 🐍 Requirements + +- **Python**: 3.10 or higher +- **Android SDK**: Latest version recommended +- **System Resources**: Sufficient RAM and storage for emulators + +--- + +## 📦 Installation + +### 📦 From PyPI (Recommended) +```bash +pip install python-android-avd-manager +``` + +### 🚧 From Source +```bash +git clone https://github.com/jwoirhaye/python-android-avd-manager-python.git +cd python-android-avd-manager +pip install -e . +``` + +--- + +## ⚡ Quickstart + +With everything set up, here’s the simplest way to create and run an emulator: + +```python +from android_device_manager import AndroidDevice , AndroidProp +from android_device_manager.avd import AVDConfiguration +from android_device_manager.emulator import EmulatorConfiguration + +# Define AVD configuration +avd_config = AVDConfiguration( + name="quickstart_avd", + package="system-images;android-34;google_apis;x86_64" +) + +# Define Emulator configuration +emulator_config = EmulatorConfiguration( + no_window=True # Run emulator in headless mode +) + +# Create and run the device using context manager +with AndroidDevice(avd_config, emulator_config) as device: + print(f"Device {device.name} is running!") + print("Android Version:", device.get_prop(AndroidProp.ANDROID_VERSION)) + # Or + #print("Android Version:", device.get_prop("ro.build.version.release")) + +``` + +--- + +## 📜 License +This project is licensed under the [MIT License](LICENSE). + +--- + +## 📧 Support +- 🐛 Issues: [GitHub Issues](https://github.com/jwoirhaye/android-device-manager-python/issues) +- 📬 Contact: [jerem.woirhaye@gmail.com] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ffca66b..21e64ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [] Homepage = "https://github.com/jwoirhaye/android-device-manager-python" Source = "https://github.com/jwoirhaye/android-device-manager-python" Issues = "https://github.com/jwoirhaye/android-device-manager-python/issues" +Documentation = "https://jwoirhaye.com/android-device-manager-python/" [build-system] requires = ["uv_build>=0.8.5,<0.9.0"] From 1acb11d2ef3aedee90d3dc4ef9a6b3ad6cd9a9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 21:04:51 +0200 Subject: [PATCH 27/28] chore(pyproject): update pyproject.toml for release configuration --- pyproject.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 21e64ff..50b51ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ build_command = """ uv build """ -[tool.semantic_release.branches.test] -match = "feature/cd-pipeline" -prerelease = false +[tool.semantic_release.branches.main] +match = "main" +prerelease_token = "rc" +prerelease = false \ No newline at end of file From 1b30c870c2b36238e17486b712bbe8458589bbb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Woirhaye?= Date: Thu, 7 Aug 2025 21:11:19 +0200 Subject: [PATCH 28/28] cd: fix pypi deploy --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 50b51ef..8b76307 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "android-device-manager" -version = "0.0.0" +version = "1.0.1" description = "Android Device Manager is a Python library for creating, launching, and managing Android device programmatically. " authors = [ { name = "Jérémy Woirhaye", email = "jerem.woirhaye@gmail.com" }