tzafon 0.1.1__py3-none-any.whl

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.

Potentially problematic release.


This version of tzafon might be problematic. Click here for more details.

tzafon/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .client import Waypoint
2
+
3
+ __all__ = ["Waypoint"]
tzafon/_connection.py ADDED
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+ import traceback
4
+ import websockets
5
+ from websockets.connection import State
6
+
7
+ from .models import Command, Result
8
+
9
+
10
+ class _WsConnection:
11
+ def __init__(self, url: str, *, ping_interval: int = 20):
12
+ if not url.startswith(("ws://", "wss://")):
13
+ raise ValueError(f"Invalid websocket URL '{url}'")
14
+ self._url = url
15
+ self._conn: websockets.WebSocketClientProtocol | None = None
16
+ self._ping = ping_interval
17
+
18
+ async def connect(self) -> None:
19
+ if self.is_open:
20
+ return
21
+ self._conn = await websockets.connect(
22
+ self._url,
23
+ max_size=2**24, # 16 MiB
24
+ ping_interval=self._ping,
25
+ ping_timeout=self._ping,
26
+ )
27
+
28
+ async def close(self) -> None:
29
+ if self._conn and self._conn.state is State.OPEN:
30
+ try:
31
+ await self._conn.close(code=1000, reason="Client shutting down")
32
+ await asyncio.wait_for(self._conn.wait_closed(), timeout=5)
33
+ except Exception:
34
+ traceback.print_exc()
35
+ self._conn = None
36
+
37
+ async def send(self, cmd: Command) -> Result:
38
+ if not self.is_open:
39
+ raise RuntimeError("Websocket not connected")
40
+ await self._conn.send(cmd.dump())
41
+ raw = await self._conn.recv()
42
+ if isinstance(raw, str):
43
+ raw = raw.encode()
44
+ return Result.load(raw)
45
+
46
+ @property
47
+ def is_open(self) -> bool:
48
+ return (
49
+ self._conn is not None and getattr(self._conn, "state", None) is State.OPEN
50
+ )
tzafon/client.py ADDED
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+ import logging
4
+ import os
5
+ from typing import Optional
6
+
7
+ from .models import Command, Result, ActionType
8
+ from ._connection import _WsConnection
9
+ from .exceptions import ScreenshotFailed
10
+
11
+
12
+ log = logging.getLogger("waypoint")
13
+ _DEFAULT_VIEWPORT = {"width": 1280, "height": 720}
14
+
15
+
16
+ class Waypoint:
17
+ def __init__(
18
+ self,
19
+ token: str,
20
+ *,
21
+ url_template: str = "wss://api.tzafon.ai/ephemeral-tzafonwright?token={token}",
22
+ connect_timeout: float = 10.0,
23
+ ):
24
+ if not token or not token.startswith("wpk_"):
25
+ raise ValueError("token must look like 'wpk_…'")
26
+ self._ws_url = url_template.format(token=token)
27
+ self._timeout = connect_timeout
28
+ self._ws: Optional[_WsConnection] = None
29
+
30
+ async def __aenter__(self) -> "Waypoint":
31
+ self._ws = _WsConnection(self._ws_url)
32
+ await asyncio.wait_for(self._ws.connect(), timeout=self._timeout)
33
+ # one-time only
34
+ await self.set_viewport(**_DEFAULT_VIEWPORT)
35
+ return self
36
+
37
+ async def __aexit__(self, exc_t, exc, tb) -> None:
38
+ if self._ws:
39
+ await self._ws.close()
40
+ self._ws = None
41
+
42
+ async def goto(self, url: str, *, timeout: int = 5_000) -> Result:
43
+ return await self._send(Command(ActionType.GOTO, url=url, timeout=timeout))
44
+
45
+ async def click(self, x: float, y: float) -> Result:
46
+ return await self._send(Command(ActionType.CLICK, x=x, y=y))
47
+
48
+ async def type(self, text: str) -> Result:
49
+ return await self._send(Command(ActionType.TYPE, text=text))
50
+
51
+ async def scroll(self, dx: int = 0, dy: int = 100) -> Result:
52
+ return await self._send(Command(ActionType.SCROLL, delta_x=dx, delta_y=dy))
53
+
54
+ async def screenshot(
55
+ self,
56
+ path: str | os.PathLike | None = None,
57
+ *,
58
+ mkdir: bool = True,
59
+ ) -> bytes:
60
+ """
61
+ Grab a **JPEG** screenshot.
62
+
63
+ Parameters
64
+ path:
65
+ Optional file destination. If provided the image is written to disk
66
+ *and* the raw bytes are still returned.
67
+ mkdir:
68
+ Automatically create parent folders if they do not exist.
69
+ """
70
+ res = await self._send(Command(ActionType.SCREENSHOT))
71
+ if not (res.success and res.image):
72
+ raise ScreenshotFailed(res.error_message or "unknown error")
73
+
74
+ if path is not None:
75
+ from pathlib import Path
76
+
77
+ p = Path(path)
78
+ if mkdir:
79
+ p.parent.mkdir(parents=True, exist_ok=True)
80
+ p.write_bytes(res.image)
81
+ return res.image
82
+
83
+ async def set_viewport(self, *, width: int, height: int) -> Result:
84
+ return await self._send(
85
+ Command(ActionType.SET_VIEWPORT_SIZE, width=width, height=height)
86
+ )
87
+
88
+ async def _send(self, cmd: Command) -> Result:
89
+ if self._ws is None:
90
+ raise RuntimeError("Waypoint must be used inside 'async with'")
91
+ res = await self._ws.send(cmd)
92
+ if not res.success:
93
+ log.warning("Command %s failed: %s", cmd.action_type, res.error_message)
94
+ return res
tzafon/exceptions.py ADDED
@@ -0,0 +1,6 @@
1
+ class WaypointError(Exception):
2
+ """Base for all SDK exceptions."""
3
+
4
+
5
+ class ScreenshotFailed(WaypointError):
6
+ """Raised when screenshot returns success=False."""
tzafon/models.py ADDED
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+ import json
3
+ import base64
4
+ from dataclasses import dataclass, asdict
5
+ from enum import Enum
6
+ from typing import Optional, Dict, Any, Self
7
+
8
+
9
+ class ActionType(str, Enum):
10
+ CLICK = "click"
11
+ TYPE = "type"
12
+ SCROLL = "scroll"
13
+ GOTO = "goto"
14
+ SCREENSHOT = "screenshot"
15
+ SET_VIEWPORT_SIZE = "set_viewport_size"
16
+
17
+
18
+ @dataclass(slots=True)
19
+ class Command:
20
+ action_type: ActionType
21
+ # optional payload
22
+ x: Optional[float] = None
23
+ y: Optional[float] = None
24
+ text: Optional[str] = None
25
+ delta_x: Optional[int] = None
26
+ delta_y: Optional[int] = None
27
+ url: Optional[str] = None
28
+ width: Optional[int] = None
29
+ height: Optional[int] = None
30
+ timeout: int = 5000 # ms
31
+
32
+ @classmethod
33
+ def load(cls, body: bytes) -> Self:
34
+ data = json.loads(body.decode("utf-8"))
35
+ action_str = data.pop("action_type", None)
36
+ if action_str is None:
37
+ raise ValueError("Command missing 'action_type'")
38
+ try:
39
+ action_enum = ActionType(action_str)
40
+ except ValueError as e:
41
+ raise ValueError(f"Unknown action_type '{action_str}'") from e
42
+ return cls(action_type=action_enum, **data)
43
+
44
+ def dump(self) -> bytes:
45
+ d = asdict(self)
46
+ d["action_type"] = self.action_type.value
47
+ d = {k: v for k, v in d.items() if v is not None}
48
+ return json.dumps(d).encode("utf-8")
49
+
50
+
51
+ @dataclass(slots=True)
52
+ class Result:
53
+ success: bool
54
+ image: Optional[bytes] = None # jpeg bytes
55
+ error_message: Optional[str] = None
56
+
57
+ @classmethod
58
+ def load(cls, body: bytes) -> Self:
59
+ data = json.loads(body.decode("utf-8"))
60
+ img_b64 = data.get("image")
61
+ img = base64.b64decode(img_b64) if img_b64 else None
62
+ return cls(
63
+ success=data.get("success", False),
64
+ image=img,
65
+ error_message=data.get("error_message"),
66
+ )
67
+
68
+ def dump(self) -> bytes:
69
+ d: Dict[str, Any] = {
70
+ "success": self.success,
71
+ "error_message": self.error_message,
72
+ "image": base64.b64encode(self.image).decode() if self.image else None,
73
+ }
74
+ return json.dumps({k: v for k, v in d.items() if v is not None}).encode()
75
+
76
+ def __str__(self) -> str:
77
+ ok = "✅" if self.success else "❌"
78
+ img = "🖼️" if self.image else "—"
79
+ return f"<Result {ok} image:{img} msg:{self.error_message!r}>"
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: tzafon
3
+ Version: 0.1.1
4
+ Summary: Tzafon Waypoint – browser automation
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: twine>=6.1.0
8
+ Requires-Dist: websockets>=15.0.1
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Tzafon Waypoint
12
+
13
+ A Python client for interacting with the tzafon.ai API for web automation.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install tzafon
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```python
24
+ import asyncio
25
+ from tzafon import Waypoint
26
+
27
+ async def main():
28
+ async with Waypoint(token="wpk_your_token_here") as wp:
29
+ # Navigate to a website
30
+ await wp.goto("https://example.com")
31
+
32
+ # Take a screenshot
33
+ image_bytes = await wp.screenshot("screenshot.jpg")
34
+
35
+ # Click at a specific position
36
+ await wp.click(100, 200)
37
+
38
+ # Type some text
39
+ await wp.type("Hello world")
40
+
41
+ # Scroll down
42
+ await wp.scroll(dy=200)
43
+
44
+ if __name__ == "__main__":
45
+ asyncio.run(main())
46
+ ```
47
+
48
+ ## Features
49
+
50
+ - Web navigation
51
+ - Screenshots
52
+ - Clicking and typing
53
+ - Scrolling
54
+ - Viewport control
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,8 @@
1
+ tzafon/__init__.py,sha256=QpZABzfciDC1uUdoRHp5tW0694ZJGtHNF9ieGilnt94,53
2
+ tzafon/_connection.py,sha256=HmmnHjO0N9pN3esCsVucws7lm1ZfyB6F0G8GMZoOCXw,1602
3
+ tzafon/client.py,sha256=NUwXgrGPhoFbmHq122ZtSLBJvs2hiz1d8EbaRsGDW8o,3149
4
+ tzafon/exceptions.py,sha256=SeeZRBAsyeogXXcfF3b3IaKUsehWNeNZAgxbmrzjI48,168
5
+ tzafon/models.py,sha256=woHESkPE3R2TbiQoZjlknz7VR938WU5ZYlPFCh0Td9U,2428
6
+ tzafon-0.1.1.dist-info/METADATA,sha256=nJmiwa8WRJFuQvhPtxTqWDl4ugGMlZsfmzXtBZo2ooE,1052
7
+ tzafon-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ tzafon-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any