Python
Python SDK의 모든 것을 다룹니다. 설치, 연결, 명령 호출, 파일, 대기, 에러, 여러 폰과 스레드, 알림, 이벤트, pytest로 하는 테스트.
번역된 페이지입니다. 영어 문서와 내용이 다르면 영어 문서가 기준입니다. English
Python 패키지는 droidline serve를 위한 얇은 클라이언트입니다. 대기, 재시도, 대체 동작은 서버와 폰이 맡고, 패키지는 명령을 보내고, 응답을 Python 값으로 바꾸고, 실패를 예외로 바꿉니다. Python 3.9 이상이 필요하고 의존성은 없습니다.
설치
pip install droidline
가상 환경에 설치하면 다른 프로젝트와 섞이지 않습니다. 시스템별 방법은 설치에 있습니다. 서버를 실행한 상태에서 잘 동작하는지 확인합니다.
python -c "from droidline import connect; print(connect().info())"
연결
from droidline import connect
d = connect() # 온라인인 유일한 폰
d = connect("shelf-01") # 이름이나 ID로 지정한 폰
connect()는 Device를 돌려줍니다. 메서드를 부르면 폰 한 대에 명령을 보내는 객체입니다. droidline serve가 실행 중이 아니면 바로 ServerNotRunningError로 실패합니다.
다른 컴퓨터의 서버에 접속하려면 주소와 클라이언트 토큰을 넘기거나, DROIDLINE_HOST, DROIDLINE_PORT, DROIDLINE_TOKEN, DROIDLINE_DEVICE를 설정합니다.
d = connect("shelf-01", host="192.168.0.12", port=8780, token="dlc_...")
여러 폰을 다루거나 devices, pair 같은 서버 명령을 쓰려면 Droidline 클라이언트를 만들고 거기서 기기를 받아 옵니다.
from droidline import Droidline
dl = Droidline() # connect()와 같은 host, port, token 인자
print(dl.devices())
a = dl.device("shelf-01")
b = dl.device("shelf-02")
두 객체 모두 with와 함께 쓸 수 있고, 블록이 끝나면 연결을 닫습니다.
with Droidline() as dl:
dl.device("shelf-01").home()
명령 호출
명령어 레퍼런스의 모든 명령은 같은 이름의 메서드입니다. 필수 파라미터가 먼저 오고 위치 인자로 넘길 수 있습니다. 선택 파라미터는 위치 인자로도, 키워드 인자로도 넘길 수 있습니다.
d.touch("text", "Log in")
d.touch("text", "Log in", 0, 30) # 위치 인자로 nth=0, timeout=30
d.touch("text", "Log in", timeout=30) # 더 명확함: 키워드 인자
d.swipe("up") # swipe는 방향을 받거나...
d.swipe(540, 1600, 540, 400, 300) # ...좌표와 시간을 받음
d.chrome.go("droidline.dev") # 점이 들어간 명령은 속성으로 접근
가장 많이 쓰는 선택자 속성에는 축약형이 있습니다. d.touchById("login"), d.touchByText("OK"), d.touchByDesc("Search")입니다.
돌려받는 값
| 명령이 돌려주는 것 | 받는 값 | 예 |
|---|---|---|
| 특별한 것 없음 | via, ms 같은 세부 정보가 담긴 dict | d.touch(...)는 {"via": "node", "ms": 208}을 돌려줌 |
| 값 하나 | 그 값 자체 | d.exists(...)는 True를, d.get_text(...)는 str을 돌려줌 |
| 여러 항목 | dict | d.battery()는 {"level": 84, "charging": False, "temperature": 31.5}를 돌려줌 |
메서드에는 타입 힌트가 있어서 편집기가 파라미터 이름을 자동 완성하고 결과의 형태를 보여 줍니다.
파일: 화면 덤프와 스크린샷
d.dump("screen.json") # 화면 트리를 PC의 파일로 저장
tree = d.dump() # 또는 dict로 돌려받음
d.screenshot("shot.png") # 확장자에 따라 PNG 또는 JPEG
d.screenshot("small.jpg", scale=0.5, quality=70)
png = d.screenshot(format="png") # 경로가 없으면 이미지를 bytes로 돌려받음
경로는 PC 기준이며, 스크립트를 실행한 폴더에 대한 상대 경로입니다. 이미지를 메모리에서 다루려면 Pillow로 bytes를 엽니다(Image.open(io.BytesIO(png))).
대기와 타임아웃
요소를 다루는 명령은 그 요소를 기다립니다. 기본값은 10초이고, 호출마다 바꿀 수 있습니다.
d.touch("text", "Next", timeout=30)
d.wait("text", "Upload complete", 120) # 기다리기만 함
d.wait_gone("id", "progress") # 무언가 사라질 때까지 기다림
명령 앞에 time.sleep()을 넣지 마세요. 스크립트만 느려집니다. 화면에 드러나지 않는 이유로 잠시 멈춰야 할 때, 예를 들어 서버가 메시지를 보낼 시간을 줘야 할 때는 sleep을 씁니다.
조건 확인
조건 확인은 바로 응답하고, 요소가 없어도 예외를 일으키지 않습니다.
if d.exists("text", "Allow"):
d.touch("text", "Allow")
if not d.in_app("com.android.chrome"):
d.launch("com.android.chrome")
state = d.which([("text", "Log in"), ("id", "main_tab")], timeout=15) # 0, 1, -1 중 하나
에러
실패한 명령은 DroidlineError의 하위 클래스 예외를 일으킵니다. 모든 예외에는 code, retryable, 그리고 서버가 보낸 추가 항목을 담은 data가 있습니다. 이 추가 항목은 속성으로도 읽을 수 있습니다.
from droidline import connect, DroidlineError, NotFoundError, DeviceOfflineError
d = connect()
try:
d.touch("text", "Log in", timeout=5)
except NotFoundError as e:
print("not on screen:", e.screen) # 그 대신 보이던 화면
except DeviceOfflineError:
print("the phone is offline")
except DroidlineError as e:
print(e.code, e.retryable, e)
| 예외 | 코드 | 보통 뜻하는 것 |
|---|---|---|
NotFoundError | NOT_FOUND | 타임아웃 안에 요소가 나타나지 않음 |
AmbiguousError | AMBIGUOUS | 여러 요소가 일치했는데 명령에는 하나가 필요함 |
NotClickableError | NOT_CLICKABLE | 요소를 탭하는 모든 방법이 실패함 |
DroidlineTimeoutError | TIMEOUT | 폰에서 명령이 너무 오래 걸림 |
NoAccessibilityError, NoImeError, NoPermissionError | NO_ACCESSIBILITY, NO_IME, NO_PERMISSION | 폰에서 권한이 꺼져 있음 |
AppNotFoundError | APP_NOT_FOUND | 그 패키지명의 앱이 없음 |
MacroFailedError | MACRO_FAILED | clear_data 같은 설정 매크로를 끝내지 못함 |
DeviceOfflineError, DeviceNotFoundError, DeviceAmbiguousError | DEVICE_* | 어느 폰인지, 또는 폰에 닿을 수 있는지에 관한 문제 |
AgentRestartedError | AGENT_RESTARTED | 앱이 재시작됨. 명령이 실행됐을 수도, 안 됐을 수도 있음 |
BadArgsError | BAD_ARGS | 파라미터가 빠졌거나 타입이 틀림 |
ServerNotRunningError | SERVER_NOT_RUNNING | droidline serve가 실행 중이 아님 |
ConnectionLostError | CONNECTION_LOST | 호출 도중 서버와의 연결이 끊김 |
모두 droidline에서 import할 수 있습니다. 각 코드의 설명은 에러 코드에 있습니다.
재시도할 만한 에러로 표시된 경우에 쓰는 간단한 재시도 함수입니다.
import time
from droidline import DroidlineError
def retry(fn, attempts=3):
for i in range(attempts):
try:
return fn()
except DroidlineError as e:
if not e.retryable or i == attempts - 1:
raise
time.sleep(2)
retry(lambda: d.touch("text", "Refresh"))
여러 폰과 스레드
Droidline 클라이언트 하나로 여러 스레드에서 여러 폰을 다룰 수 있습니다. 한 폰에 보낸 명령은 순서대로 실행되고, 서로 다른 폰에 보낸 명령은 동시에 실행됩니다.
from concurrent.futures import ThreadPoolExecutor
from droidline import Droidline
dl = Droidline()
def morning(name):
d = dl.device(name)
d.wake()
d.launch("com.android.chrome")
d.chrome.go("droidline.dev")
return name, d.current()["package"]
names = [p["name"] for p in dl.devices() if p["online"]]
with ThreadPoolExecutor(max_workers=len(names) or 1) as pool:
for name, pkg in pool.map(morning, names):
print(name, pkg)
선반 가득한 폰을 위한 패턴은 여러 폰에 더 있습니다.
알림과 이벤트
알림 하나를 기다리거나, 조건에 맞는 알림마다 콜백을 받을 수 있습니다. 먼저 폰이 어떤 앱의 알림을 넘길지 notify_filter나 앱의 상태 탭에서 고르세요.
d.notify_filter(["com.google.android.gm"])
n = d.wait_notification("textContains", "verification code", 60)
print(n["title"], n["text"])
stop = d.on_notification(package="com.google.android.gm",
callback=lambda n: print(n["title"], n["text"]))
# ... 나중에
stop()
콜백은 SDK의 이벤트 스레드에서 실행되므로 짧게 유지하거나 작업을 큐로 넘기세요. 폰이 온라인이나 오프라인이 되는 것 같은 다른 이벤트는 클라이언트를 통해 들어옵니다.
dl = Droidline()
dl.subscribe(["device"])
dl.on("device", lambda e: print(e["name"], e["state"]))
답장, 클릭, 웹훅은 알림에서 다룹니다.
SDK가 아직 모르는 명령
서버가 패키지보다 새 버전이라면 명령을 이름으로 호출합니다.
d.call("some_new_command", value=1)
pytest로 테스트하기
fixture로 각 테스트에 홈 화면에서 시작하는 폰을 줍니다.
# conftest.py
import pytest
from droidline import connect
@pytest.fixture
def phone():
d = connect()
d.home()
yield d
d.screenshot("last-test.png")
# test_login.py
def test_login(phone):
phone.launch("dev.droidline.demo")
phone.input("id", "email", "knife")
phone.touchById("login")
assert phone.which([("text", "Log in"), ("id", "main_tab")]) == 1
assert phone.get_text("id", "greeting") == "Welcome, knife"
이 테스트는 고치지 않고도 CI에서는 droidline-fakephone을 상대로, 책상 위에서는 실제 폰을 상대로 실행됩니다.
완성된 스크립트
첫 스크립트의 스크립트에 재시도와 깔끔한 종료 코드를 더한 것입니다.
import sys
from droidline import connect, DroidlineError
def main() -> int:
d = connect()
d.launch("dev.droidline.demo")
d.wait("text", "Sign in")
if d.exists("text", "Close ad"):
d.touch("text", "Close ad")
d.input("id", "email", "knife")
if not d.checked("id", "auto_login"):
d.touch("id", "auto_login")
d.touch("text", "Log in")
if d.which([("text", "Log in"), ("id", "main_tab")], timeout=15) != 1:
d.screenshot("login-failed.png")
return 1
print(d.get_text("id", "greeting"))
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except DroidlineError as e:
print(f"{e.code}: {e}", file=sys.stderr)
sys.exit(2)