androidctl 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {androidctl-0.1.0 → androidctl-0.1.1}/PKG-INFO +9 -1
- {androidctl-0.1.0 → androidctl-0.1.1}/README.md +8 -0
- androidctl-0.1.1/VERSION +1 -0
- androidctl-0.1.1/androidctl/src/androidctl/_version.py +5 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/daemon/owner.py +111 -19
- androidctl-0.1.1/androidctl/src/androidctl/resources/androidctl-agent-0.1.1-release.apk +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl.egg-info/PKG-INFO +9 -1
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl.egg-info/SOURCES.txt +3 -1
- androidctl-0.1.1/androidctld/src/androidctld/_version.py +5 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/handlers/observe.py +1 -2
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/daemon/server.py +140 -8
- androidctl-0.1.1/contracts/src/androidctl_contracts/_version.py +5 -0
- androidctl-0.1.1/contracts/src/androidctl_contracts/_version_source.py +57 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/pyproject.toml +4 -1
- androidctl-0.1.0/androidctl/src/androidctl/_version.py +0 -1
- androidctl-0.1.0/androidctl/src/androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
- androidctl-0.1.0/androidctld/src/androidctld/_version.py +0 -1
- androidctl-0.1.0/contracts/src/androidctl_contracts/_version.py +0 -1
- {androidctl-0.1.0 → androidctl-0.1.1}/LICENSE +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/__main__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/app.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/cli_options.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/command_payloads.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/command_views.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/actions.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/adb_wireless.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/close.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/connect.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/execute.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/list_apps.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/observe.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/open.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/plumbing.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/run_pipeline.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/screenshot.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/setup.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/commands/wait.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/daemon/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/daemon/client.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/daemon/discovery.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/daemon/launcher.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/errors/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/errors/mapping.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/errors/models.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/exit_codes.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/output.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/parsing/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/parsing/duration.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/parsing/open_target.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/parsing/refs.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/parsing/screen_id.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/parsing/wait.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/renderers/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/renderers/_paths.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/renderers/xml.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/renderers/xml_projection.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/resources/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/setup/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/setup/accessibility.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/setup/adb.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/setup/apk_resource.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/setup/pairing.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/setup/verify.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/workspace/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl/src/androidctl/workspace/resolve.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl.egg-info/dependency_links.txt +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl.egg-info/entry_points.txt +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl.egg-info/requires.txt +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctl.egg-info/top_level.txt +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/__main__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/action_target.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/capabilities.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/executor.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/focus_confirmation.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/focused_input_admissibility.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/fresh_current.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/postconditions.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/repair.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/request_builder.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/settle.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/submit_confirmation.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/submit_routing.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/actions/type_confirmation.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/app_targets.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/artifacts/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/artifacts/models.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/artifacts/screen_lookup.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/artifacts/screen_payloads.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/artifacts/writer.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/auth/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/auth/active_registry.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/auth/secret_files.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/auth/token_store.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/assembly.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/command_models.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/dispatch.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/executor.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/from_boundary.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/handlers/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/handlers/action.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/handlers/connect.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/handlers/list_apps.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/handlers/screenshot.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/handlers/wait.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/models.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/open_targets.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/orchestration.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/registry.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/result_builders.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/result_models.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/results.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/semantic_command_names.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/semantic_error_mapping.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/semantic_truth.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/service.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/config.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/daemon/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/daemon/active_slot.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/daemon/envelope.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/daemon/http_host.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/daemon/ingress.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/daemon/ownership_probe.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/daemon/service.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/device/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/device/action_models.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/device/action_serialization.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/device/adapters.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/device/bootstrap.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/device/connectors.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/device/errors.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/device/interfaces.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/device/parsing.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/device/rpc.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/device/schema.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/device/types.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/errors/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/logging/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/observation.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/protocol.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/refs/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/refs/models.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/refs/repair.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/refs/service.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/rendering/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/rendering/screen_xml.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/runtime/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/runtime/kernel.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/runtime/lifecycle.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/runtime/models.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/runtime/screen_state.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/runtime/state_repo.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/runtime/store.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/runtime_policy.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/schema/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/schema/base.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/schema/core.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/schema/daemon_api.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/schema/persistence.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/schema/persistence_io.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/schema/validation_errors.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/semantics/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/semantics/compiler.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/semantics/continuity.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/semantics/labels.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/semantics/models.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/semantics/policy.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/semantics/public_models.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/semantics/registries.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/semantics/submit_refs.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/semantics/surface.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/semantics/targets.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/snapshots/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/snapshots/models.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/snapshots/refresh.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/snapshots/schema.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/snapshots/service.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/text_equivalence.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/waits/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/waits/evaluators.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/waits/loop.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/waits/matcher.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/contracts/src/androidctl_contracts/__init__.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/contracts/src/androidctl_contracts/_wire_helpers.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/contracts/src/androidctl_contracts/base.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/contracts/src/androidctl_contracts/command_catalog.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/contracts/src/androidctl_contracts/command_results.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/contracts/src/androidctl_contracts/daemon_api.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/contracts/src/androidctl_contracts/errors.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/contracts/src/androidctl_contracts/paths.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/contracts/src/androidctl_contracts/public_screen.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/contracts/src/androidctl_contracts/user_state.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/contracts/src/androidctl_contracts/vocabulary.py +0 -0
- {androidctl-0.1.0 → androidctl-0.1.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: androidctl
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: A Playwright-like CLI for Android automation, built for AI agents.
|
|
5
5
|
Author: Azure99
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -64,6 +64,14 @@ androidctl observe
|
|
|
64
64
|
- Android SDK platform tools, especially `adb`.
|
|
65
65
|
- An Android 11+ device or emulator with USB debugging enabled.
|
|
66
66
|
|
|
67
|
+
## Installing skills
|
|
68
|
+
|
|
69
|
+
For agents that support skills, ask your agent to install the AndroidCtl skill:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
Install this skill: https://github.com/Azure99/androidctl/tree/main/skills/androidctl
|
|
73
|
+
```
|
|
74
|
+
|
|
67
75
|
## Install From PyPI
|
|
68
76
|
|
|
69
77
|
Install the released host tools with the single public distribution:
|
|
@@ -28,6 +28,14 @@ androidctl observe
|
|
|
28
28
|
- Android SDK platform tools, especially `adb`.
|
|
29
29
|
- An Android 11+ device or emulator with USB debugging enabled.
|
|
30
30
|
|
|
31
|
+
## Installing skills
|
|
32
|
+
|
|
33
|
+
For agents that support skills, ask your agent to install the AndroidCtl skill:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
Install this skill: https://github.com/Azure99/androidctl/tree/main/skills/androidctl
|
|
37
|
+
```
|
|
38
|
+
|
|
31
39
|
## Install From PyPI
|
|
32
40
|
|
|
33
41
|
Install the released host tools with the single public distribution:
|
androidctl-0.1.1/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.1
|
|
@@ -10,6 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
12
|
OWNER_ENV = "ANDROIDCTL_OWNER_ID"
|
|
13
|
+
WINDOWS_OWNER_ANCHOR_ENV = "ANDROIDCTL_OWNER_ANCHOR_PROCESSES"
|
|
13
14
|
DEFAULT_OWNER_HINT = "Set ANDROIDCTL_OWNER_ID explicitly."
|
|
14
15
|
_MAX_OWNER_PROCESS_HOPS = 64
|
|
15
16
|
_SHELL_PROCESS_NAMES = frozenset(
|
|
@@ -18,6 +19,7 @@ _SHELL_PROCESS_NAMES = frozenset(
|
|
|
18
19
|
_WINDOWS_SHELL_PROCESS_NAMES = frozenset(
|
|
19
20
|
{"bash.exe", "cmd.exe", "powershell.exe", "pwsh.exe", "sh.exe"}
|
|
20
21
|
)
|
|
22
|
+
_DEFAULT_WINDOWS_OWNER_ANCHOR_PROCESS_NAMES = frozenset({"claude.exe", "codex.exe"})
|
|
21
23
|
_TH32CS_SNAPPROCESS = 0x00000002
|
|
22
24
|
_PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
23
25
|
_ERROR_NO_MORE_FILES = 18
|
|
@@ -30,6 +32,19 @@ class _WindowsProcessInfo:
|
|
|
30
32
|
process_name: str
|
|
31
33
|
|
|
32
34
|
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class _WindowsAncestorProcess:
|
|
37
|
+
pid: int
|
|
38
|
+
process_name: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class _WindowsOwnerTarget:
|
|
43
|
+
kind: str
|
|
44
|
+
pid: int
|
|
45
|
+
process_name: str
|
|
46
|
+
|
|
47
|
+
|
|
33
48
|
class _WindowsFileTime(ctypes.Structure):
|
|
34
49
|
_fields_ = [
|
|
35
50
|
("dwLowDateTime", wintypes.DWORD),
|
|
@@ -58,8 +73,8 @@ def derive_owner_id(*, env: Mapping[str, str]) -> str:
|
|
|
58
73
|
candidate = configured.strip()
|
|
59
74
|
if candidate:
|
|
60
75
|
return candidate
|
|
61
|
-
if
|
|
62
|
-
owner_id = _derive_windows_owner_id()
|
|
76
|
+
if _is_windows_platform():
|
|
77
|
+
owner_id = _derive_windows_owner_id(env=env)
|
|
63
78
|
if owner_id is None:
|
|
64
79
|
raise ValueError(
|
|
65
80
|
"Unable to derive a safe owner identity automatically. "
|
|
@@ -81,6 +96,10 @@ def derive_owner_id(*, env: Mapping[str, str]) -> str:
|
|
|
81
96
|
return f"shell:{shell_pid}:{lifetime}"
|
|
82
97
|
|
|
83
98
|
|
|
99
|
+
def _is_windows_platform() -> bool:
|
|
100
|
+
return sys.platform == "win32"
|
|
101
|
+
|
|
102
|
+
|
|
84
103
|
def _find_interactive_shell_ancestor_pid(env: Mapping[str, str]) -> int | None:
|
|
85
104
|
del env
|
|
86
105
|
current_pid = os.getpid()
|
|
@@ -108,39 +127,112 @@ def _find_interactive_shell_ancestor_pid(env: Mapping[str, str]) -> int | None:
|
|
|
108
127
|
return None
|
|
109
128
|
|
|
110
129
|
|
|
111
|
-
def _derive_windows_owner_id() -> str | None:
|
|
112
|
-
shell_pid = _find_windows_shell_ancestor_pid()
|
|
113
|
-
if shell_pid is None:
|
|
114
|
-
return None
|
|
115
|
-
lifetime = _read_windows_process_creation_filetime(shell_pid)
|
|
116
|
-
if lifetime is None:
|
|
117
|
-
return None
|
|
118
|
-
return f"shell:win32:{shell_pid}:{lifetime}"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def _find_windows_shell_ancestor_pid() -> int | None:
|
|
130
|
+
def _derive_windows_owner_id(*, env: Mapping[str, str]) -> str | None:
|
|
122
131
|
process_table = _read_windows_process_table()
|
|
123
132
|
if process_table is None:
|
|
124
133
|
return None
|
|
134
|
+
for target in _find_windows_owner_targets(process_table, env=env):
|
|
135
|
+
lifetime = _read_windows_process_creation_filetime(target.pid)
|
|
136
|
+
if lifetime is None:
|
|
137
|
+
continue
|
|
138
|
+
if target.kind == "agent":
|
|
139
|
+
return f"agent:win32:{target.process_name}:{target.pid}:{lifetime}"
|
|
140
|
+
return f"shell:win32:{target.pid}:{lifetime}"
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _find_windows_owner_targets(
|
|
145
|
+
process_table: dict[int, _WindowsProcessInfo],
|
|
146
|
+
*,
|
|
147
|
+
env: Mapping[str, str],
|
|
148
|
+
) -> list[_WindowsOwnerTarget]:
|
|
149
|
+
anchor_names = _windows_owner_anchor_process_names(env)
|
|
150
|
+
agent_targets: list[_WindowsOwnerTarget] = []
|
|
151
|
+
shell_target: _WindowsOwnerTarget | None = None
|
|
152
|
+
for ancestor in _windows_ancestor_chain(process_table):
|
|
153
|
+
if ancestor.process_name in anchor_names:
|
|
154
|
+
agent_targets.append(
|
|
155
|
+
_WindowsOwnerTarget(
|
|
156
|
+
kind="agent",
|
|
157
|
+
pid=ancestor.pid,
|
|
158
|
+
process_name=ancestor.process_name,
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
if (
|
|
162
|
+
shell_target is None
|
|
163
|
+
and ancestor.process_name in _WINDOWS_SHELL_PROCESS_NAMES
|
|
164
|
+
):
|
|
165
|
+
shell_target = _WindowsOwnerTarget(
|
|
166
|
+
kind="shell",
|
|
167
|
+
pid=ancestor.pid,
|
|
168
|
+
process_name=ancestor.process_name,
|
|
169
|
+
)
|
|
170
|
+
if shell_target is not None:
|
|
171
|
+
return [*agent_targets, shell_target]
|
|
172
|
+
return agent_targets
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _windows_ancestor_chain(
|
|
176
|
+
process_table: dict[int, _WindowsProcessInfo],
|
|
177
|
+
) -> list[_WindowsAncestorProcess]:
|
|
125
178
|
current_pid = os.getpid()
|
|
126
179
|
current = process_table.get(current_pid)
|
|
127
180
|
if current is None:
|
|
128
|
-
return
|
|
181
|
+
return []
|
|
129
182
|
ancestor_pid = current.parent_pid
|
|
130
183
|
seen: set[int] = {current_pid}
|
|
184
|
+
ancestors: list[_WindowsAncestorProcess] = []
|
|
131
185
|
hops = 0
|
|
132
186
|
while ancestor_pid > 0 and hops < _MAX_OWNER_PROCESS_HOPS:
|
|
133
187
|
if ancestor_pid in seen:
|
|
134
|
-
return
|
|
188
|
+
return []
|
|
135
189
|
seen.add(ancestor_pid)
|
|
136
190
|
ancestor = process_table.get(ancestor_pid)
|
|
137
191
|
if ancestor is None:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
192
|
+
break
|
|
193
|
+
ancestors.append(
|
|
194
|
+
_WindowsAncestorProcess(
|
|
195
|
+
pid=ancestor_pid,
|
|
196
|
+
process_name=_normalize_windows_process_name(ancestor.process_name),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
142
199
|
ancestor_pid = ancestor.parent_pid
|
|
143
200
|
hops += 1
|
|
201
|
+
return ancestors
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _windows_owner_anchor_process_names(env: Mapping[str, str]) -> set[str]:
|
|
205
|
+
process_names = set(_DEFAULT_WINDOWS_OWNER_ANCHOR_PROCESS_NAMES)
|
|
206
|
+
configured = env.get(WINDOWS_OWNER_ANCHOR_ENV)
|
|
207
|
+
if configured is None:
|
|
208
|
+
return process_names
|
|
209
|
+
for raw_name in configured.replace(";", ",").split(","):
|
|
210
|
+
normalized = _normalize_windows_process_name(raw_name)
|
|
211
|
+
if normalized:
|
|
212
|
+
process_names.add(normalized)
|
|
213
|
+
return process_names
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _find_windows_shell_ancestor_pid() -> int | None:
|
|
217
|
+
process_table = _read_windows_process_table()
|
|
218
|
+
if process_table is None:
|
|
219
|
+
return None
|
|
220
|
+
target = _find_windows_shell_ancestor(process_table)
|
|
221
|
+
if target is None:
|
|
222
|
+
return None
|
|
223
|
+
return target.pid
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _find_windows_shell_ancestor(
|
|
227
|
+
process_table: dict[int, _WindowsProcessInfo],
|
|
228
|
+
) -> _WindowsOwnerTarget | None:
|
|
229
|
+
for ancestor in _windows_ancestor_chain(process_table):
|
|
230
|
+
if ancestor.process_name in _WINDOWS_SHELL_PROCESS_NAMES:
|
|
231
|
+
return _WindowsOwnerTarget(
|
|
232
|
+
kind="shell",
|
|
233
|
+
pid=ancestor.pid,
|
|
234
|
+
process_name=ancestor.process_name,
|
|
235
|
+
)
|
|
144
236
|
return None
|
|
145
237
|
|
|
146
238
|
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: androidctl
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: A Playwright-like CLI for Android automation, built for AI agents.
|
|
5
5
|
Author: Azure99
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -64,6 +64,14 @@ androidctl observe
|
|
|
64
64
|
- Android SDK platform tools, especially `adb`.
|
|
65
65
|
- An Android 11+ device or emulator with USB debugging enabled.
|
|
66
66
|
|
|
67
|
+
## Installing skills
|
|
68
|
+
|
|
69
|
+
For agents that support skills, ask your agent to install the AndroidCtl skill:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
Install this skill: https://github.com/Azure99/androidctl/tree/main/skills/androidctl
|
|
73
|
+
```
|
|
74
|
+
|
|
67
75
|
## Install From PyPI
|
|
68
76
|
|
|
69
77
|
Install the released host tools with the single public distribution:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
|
+
VERSION
|
|
3
4
|
pyproject.toml
|
|
4
5
|
androidctl.egg-info/PKG-INFO
|
|
5
6
|
androidctl.egg-info/SOURCES.txt
|
|
@@ -49,7 +50,7 @@ androidctl/src/androidctl/renderers/_paths.py
|
|
|
49
50
|
androidctl/src/androidctl/renderers/xml.py
|
|
50
51
|
androidctl/src/androidctl/renderers/xml_projection.py
|
|
51
52
|
androidctl/src/androidctl/resources/__init__.py
|
|
52
|
-
androidctl/src/androidctl/resources/androidctl-agent-0.1.
|
|
53
|
+
androidctl/src/androidctl/resources/androidctl-agent-0.1.1-release.apk
|
|
53
54
|
androidctl/src/androidctl/setup/__init__.py
|
|
54
55
|
androidctl/src/androidctl/setup/accessibility.py
|
|
55
56
|
androidctl/src/androidctl/setup/adb.py
|
|
@@ -178,6 +179,7 @@ androidctld/src/androidctld/waits/loop.py
|
|
|
178
179
|
androidctld/src/androidctld/waits/matcher.py
|
|
179
180
|
contracts/src/androidctl_contracts/__init__.py
|
|
180
181
|
contracts/src/androidctl_contracts/_version.py
|
|
182
|
+
contracts/src/androidctl_contracts/_version_source.py
|
|
181
183
|
contracts/src/androidctl_contracts/_wire_helpers.py
|
|
182
184
|
contracts/src/androidctl_contracts/base.py
|
|
183
185
|
contracts/src/androidctl_contracts/command_catalog.py
|
{androidctl-0.1.0 → androidctl-0.1.1}/androidctld/src/androidctld/commands/handlers/observe.py
RENAMED
|
@@ -54,7 +54,6 @@ class ObserveCommandHandler:
|
|
|
54
54
|
artifacts=None if source_basis is None else source_basis.artifacts,
|
|
55
55
|
).model_dump(by_alias=True, mode="json")
|
|
56
56
|
lifecycle_lease = self._runtime_kernel.capture_lifecycle_lease(runtime)
|
|
57
|
-
force_refresh = source_basis is None
|
|
58
57
|
self._runtime_kernel.acquire_progress_lane(
|
|
59
58
|
runtime,
|
|
60
59
|
occupant_kind=command.kind.value,
|
|
@@ -62,7 +61,7 @@ class ObserveCommandHandler:
|
|
|
62
61
|
try:
|
|
63
62
|
snapshot = self._snapshot_service.fetch(
|
|
64
63
|
runtime,
|
|
65
|
-
force_refresh=
|
|
64
|
+
force_refresh=True,
|
|
66
65
|
lifecycle_lease=lifecycle_lease,
|
|
67
66
|
)
|
|
68
67
|
previous_screen = (
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import errno
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
7
8
|
import threading
|
|
@@ -27,6 +28,18 @@ from androidctld.runtime_policy import (
|
|
|
27
28
|
DAEMON_HTTP_SOCKET_TIMEOUT_SECONDS,
|
|
28
29
|
)
|
|
29
30
|
|
|
31
|
+
_CLIENT_DISCONNECT_ERRNOS = {
|
|
32
|
+
errno.EPIPE,
|
|
33
|
+
errno.ECONNRESET,
|
|
34
|
+
errno.ECONNABORTED,
|
|
35
|
+
errno.ETIMEDOUT,
|
|
36
|
+
}
|
|
37
|
+
_CLIENT_DISCONNECT_WINERRORS = {
|
|
38
|
+
10053, # WSAECONNABORTED
|
|
39
|
+
10054, # WSAECONNRESET
|
|
40
|
+
10058, # WSAESHUTDOWN
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
|
|
31
44
|
class AndroidctldHttpServer:
|
|
32
45
|
def __init__(
|
|
@@ -84,6 +97,7 @@ class AndroidctldHttpServer:
|
|
|
84
97
|
and method == "POST"
|
|
85
98
|
and path
|
|
86
99
|
in {
|
|
100
|
+
"/health",
|
|
87
101
|
"/runtime/get",
|
|
88
102
|
"/runtime/close",
|
|
89
103
|
"/commands/run",
|
|
@@ -176,24 +190,31 @@ class AndroidctldHttpServer:
|
|
|
176
190
|
)
|
|
177
191
|
if result.shutdown_after_write:
|
|
178
192
|
self._enter_closing_gate()
|
|
193
|
+
self._retire_active_slot_for_close()
|
|
179
194
|
try:
|
|
180
|
-
self.
|
|
195
|
+
self._try_write_json_response(
|
|
181
196
|
handler,
|
|
182
197
|
result.status_code,
|
|
183
198
|
success_envelope(result.payload),
|
|
199
|
+
response_kind="close_success",
|
|
184
200
|
)
|
|
185
|
-
except OSError:
|
|
186
|
-
self._logger.info("close response write failed", exc_info=True)
|
|
187
201
|
finally:
|
|
188
202
|
self._request_shutdown_after_close()
|
|
189
203
|
return
|
|
190
|
-
self.
|
|
204
|
+
if not self._try_write_json_response(
|
|
191
205
|
handler,
|
|
192
206
|
result.status_code,
|
|
193
207
|
success_envelope(result.payload),
|
|
194
|
-
|
|
208
|
+
response_kind="success",
|
|
209
|
+
):
|
|
210
|
+
return
|
|
195
211
|
except DaemonError as error:
|
|
196
|
-
self.
|
|
212
|
+
self._try_write_json_response(
|
|
213
|
+
handler,
|
|
214
|
+
error.http_status,
|
|
215
|
+
error_envelope(error),
|
|
216
|
+
response_kind="daemon_error",
|
|
217
|
+
)
|
|
197
218
|
except Exception: # pragma: no cover - defensive fallback
|
|
198
219
|
self._logger.exception("unexpected daemon failure")
|
|
199
220
|
daemon_error = DaemonError(
|
|
@@ -203,8 +224,11 @@ class AndroidctldHttpServer:
|
|
|
203
224
|
details={},
|
|
204
225
|
http_status=500,
|
|
205
226
|
)
|
|
206
|
-
self.
|
|
207
|
-
handler,
|
|
227
|
+
self._try_write_json_response(
|
|
228
|
+
handler,
|
|
229
|
+
daemon_error.http_status,
|
|
230
|
+
error_envelope(daemon_error),
|
|
231
|
+
response_kind="internal_error",
|
|
208
232
|
)
|
|
209
233
|
|
|
210
234
|
def _enter_closing_gate(self) -> None:
|
|
@@ -212,6 +236,22 @@ class AndroidctldHttpServer:
|
|
|
212
236
|
return
|
|
213
237
|
self._closing = True
|
|
214
238
|
|
|
239
|
+
def _retire_active_slot_for_close(self) -> None:
|
|
240
|
+
try:
|
|
241
|
+
self._active_slot.clear_record()
|
|
242
|
+
except Exception:
|
|
243
|
+
self._logger.warning(
|
|
244
|
+
"failed to clear active daemon record while closing",
|
|
245
|
+
exc_info=True,
|
|
246
|
+
)
|
|
247
|
+
try:
|
|
248
|
+
self._active_slot.release_owner()
|
|
249
|
+
except Exception:
|
|
250
|
+
self._logger.warning(
|
|
251
|
+
"failed to release active daemon owner lock while closing",
|
|
252
|
+
exc_info=True,
|
|
253
|
+
)
|
|
254
|
+
|
|
215
255
|
def _request_shutdown_after_close(self) -> None:
|
|
216
256
|
if self._shutdown_after_close_requested:
|
|
217
257
|
return
|
|
@@ -284,3 +324,95 @@ class AndroidctldHttpServer:
|
|
|
284
324
|
handler.send_header("Content-Length", str(len(body)))
|
|
285
325
|
handler.end_headers()
|
|
286
326
|
handler.wfile.write(body)
|
|
327
|
+
|
|
328
|
+
def _try_write_json_response(
|
|
329
|
+
self,
|
|
330
|
+
handler: BaseHTTPRequestHandler,
|
|
331
|
+
status_code: int,
|
|
332
|
+
payload: dict[str, Any],
|
|
333
|
+
*,
|
|
334
|
+
response_kind: str,
|
|
335
|
+
) -> bool:
|
|
336
|
+
try:
|
|
337
|
+
self._write_json(handler, status_code, payload)
|
|
338
|
+
return True
|
|
339
|
+
except OSError as error:
|
|
340
|
+
if _is_response_write_client_disconnect(error):
|
|
341
|
+
self._log_client_disconnect(
|
|
342
|
+
handler,
|
|
343
|
+
status_code=status_code,
|
|
344
|
+
response_kind=response_kind,
|
|
345
|
+
error=error,
|
|
346
|
+
)
|
|
347
|
+
handler.close_connection = True
|
|
348
|
+
return False
|
|
349
|
+
self._log_response_write_failure(
|
|
350
|
+
handler,
|
|
351
|
+
status_code=status_code,
|
|
352
|
+
response_kind=response_kind,
|
|
353
|
+
error=error,
|
|
354
|
+
)
|
|
355
|
+
handler.close_connection = True
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
def _log_client_disconnect(
|
|
359
|
+
self,
|
|
360
|
+
handler: BaseHTTPRequestHandler,
|
|
361
|
+
*,
|
|
362
|
+
status_code: int,
|
|
363
|
+
response_kind: str,
|
|
364
|
+
error: OSError,
|
|
365
|
+
) -> None:
|
|
366
|
+
self._logger.info(
|
|
367
|
+
"client disconnected before response write completed "
|
|
368
|
+
"method=%s path=%s status=%s response=%s exception=%s",
|
|
369
|
+
_handler_method(handler),
|
|
370
|
+
_handler_path(handler),
|
|
371
|
+
status_code,
|
|
372
|
+
response_kind,
|
|
373
|
+
type(error).__name__,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def _log_response_write_failure(
|
|
377
|
+
self,
|
|
378
|
+
handler: BaseHTTPRequestHandler,
|
|
379
|
+
*,
|
|
380
|
+
status_code: int,
|
|
381
|
+
response_kind: str,
|
|
382
|
+
error: OSError,
|
|
383
|
+
) -> None:
|
|
384
|
+
self._logger.warning(
|
|
385
|
+
"response write failed method=%s path=%s status=%s response=%s "
|
|
386
|
+
"exception=%s",
|
|
387
|
+
_handler_method(handler),
|
|
388
|
+
_handler_path(handler),
|
|
389
|
+
status_code,
|
|
390
|
+
response_kind,
|
|
391
|
+
type(error).__name__,
|
|
392
|
+
exc_info=True,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _is_response_write_client_disconnect(error: OSError) -> bool:
|
|
397
|
+
if isinstance(
|
|
398
|
+
error,
|
|
399
|
+
(
|
|
400
|
+
BrokenPipeError,
|
|
401
|
+
ConnectionAbortedError,
|
|
402
|
+
ConnectionResetError,
|
|
403
|
+
TimeoutError,
|
|
404
|
+
),
|
|
405
|
+
):
|
|
406
|
+
return True
|
|
407
|
+
if error.errno in _CLIENT_DISCONNECT_ERRNOS:
|
|
408
|
+
return True
|
|
409
|
+
winerror = getattr(error, "winerror", None)
|
|
410
|
+
return winerror in _CLIENT_DISCONNECT_WINERRORS
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _handler_method(handler: BaseHTTPRequestHandler) -> str:
|
|
414
|
+
return str(getattr(handler, "command", "<unknown>"))
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _handler_path(handler: BaseHTTPRequestHandler) -> str:
|
|
418
|
+
return str(getattr(handler, "path", "<unknown>"))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from importlib.metadata import PackageNotFoundError
|
|
5
|
+
from importlib.metadata import version as distribution_version
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_DISTRIBUTION_NAME = "androidctl"
|
|
9
|
+
_VERSION_FILE_NAME = "VERSION"
|
|
10
|
+
_VERSION_PATTERN = re.compile(r"\d+\.\d+\.\d+\Z")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resolve_release_version() -> str:
|
|
14
|
+
source_version = _read_source_version()
|
|
15
|
+
if source_version is not None:
|
|
16
|
+
return source_version
|
|
17
|
+
try:
|
|
18
|
+
return distribution_version(_DISTRIBUTION_NAME)
|
|
19
|
+
except PackageNotFoundError as error:
|
|
20
|
+
raise RuntimeError("unable to resolve androidctl release version") from error
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _read_source_version() -> str | None:
|
|
24
|
+
for candidate_root in Path(__file__).resolve().parents:
|
|
25
|
+
version_path = candidate_root / _VERSION_FILE_NAME
|
|
26
|
+
if not version_path.is_file() or not _looks_like_repo_root(candidate_root):
|
|
27
|
+
continue
|
|
28
|
+
return _parse_version_text(version_path.read_text(encoding="utf-8"))
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _looks_like_repo_root(path: Path) -> bool:
|
|
33
|
+
return (
|
|
34
|
+
(path / "pyproject.toml").is_file()
|
|
35
|
+
and (path / "androidctl").is_dir()
|
|
36
|
+
and (path / "androidctld").is_dir()
|
|
37
|
+
and (path / "contracts").is_dir()
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_version_text(raw_text: str) -> str:
|
|
42
|
+
if raw_text.endswith("\r\n"):
|
|
43
|
+
candidate = raw_text.removesuffix("\r\n")
|
|
44
|
+
elif raw_text.endswith("\n"):
|
|
45
|
+
candidate = raw_text.removesuffix("\n")
|
|
46
|
+
else:
|
|
47
|
+
candidate = raw_text
|
|
48
|
+
if (
|
|
49
|
+
"\n" in candidate
|
|
50
|
+
or "\r" in candidate
|
|
51
|
+
or _VERSION_PATTERN.fullmatch(candidate) is None
|
|
52
|
+
):
|
|
53
|
+
raise RuntimeError(
|
|
54
|
+
"canonical VERSION must contain exactly MAJOR.MINOR.PATCH with "
|
|
55
|
+
"at most one terminating newline"
|
|
56
|
+
)
|
|
57
|
+
return candidate
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "androidctl"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"]
|
|
8
8
|
description = "A Playwright-like CLI for Android automation, built for AI agents."
|
|
9
9
|
authors = [{ name = "Azure99" }]
|
|
10
10
|
keywords = ["agent", "android", "adb", "automation", "cli", "accessibility"]
|
|
@@ -58,6 +58,9 @@ namespaces = false
|
|
|
58
58
|
[tool.setuptools.package-data]
|
|
59
59
|
"androidctl.resources" = ["*.apk"]
|
|
60
60
|
|
|
61
|
+
[tool.setuptools.dynamic]
|
|
62
|
+
version = { file = ["VERSION"] }
|
|
63
|
+
|
|
61
64
|
[tool.pytest.ini_options]
|
|
62
65
|
pythonpath = [
|
|
63
66
|
"contracts/src",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|