dataapp-resource-proxy 0.1.0__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.
@@ -0,0 +1,16 @@
1
+ """Public client errors."""
2
+
3
+
4
+ class ResourceProxyError(RuntimeError):
5
+ """A managed resource request failed."""
6
+
7
+ def __init__(
8
+ self,
9
+ message: str,
10
+ *,
11
+ code: str = "resource-proxy-error",
12
+ status_code: int | None = None,
13
+ ) -> None:
14
+ super().__init__(message)
15
+ self.code = code
16
+ self.status_code = status_code
@@ -0,0 +1,199 @@
1
+ """Owned resource handles returned by :mod:`dataapp_resource_proxy`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ if TYPE_CHECKING:
11
+ from .client import ResourceProxy
12
+
13
+
14
+ @dataclass
15
+ class ProxyResource:
16
+ """One DataOS proxy lease owned by this client."""
17
+
18
+ client: ResourceProxy = field(repr=False)
19
+ lease_id: str
20
+ proxy_url: str = field(repr=False)
21
+ expires_at: datetime | None = None
22
+ credential_generation: int | None = None
23
+ network: str | None = None
24
+ node_id: str | None = None
25
+ line_tier: str | None = None
26
+ proxy_group: str | None = None
27
+ supplier_code: str | None = None
28
+ supplier_route_id: str | None = None
29
+ supplier_account_ref: str | None = None
30
+ line_policy_version: str | None = None
31
+ _closed: bool = field(default=False, init=False, repr=False)
32
+ _lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False, repr=False)
33
+
34
+ def __repr__(self) -> str:
35
+ return f"ProxyResource(lease_id={self.lease_id!r}, proxy_url=<redacted>)"
36
+
37
+ async def renew(self) -> None:
38
+ async with self._lock:
39
+ self._require_open()
40
+ value = await self.client._renew_proxy(self.lease_id)
41
+ self._update(value)
42
+
43
+ async def refresh(self) -> None:
44
+ """Refresh safe lease metadata without changing credentials."""
45
+ async with self._lock:
46
+ self._require_open()
47
+ value = await self.client._get_proxy(self.lease_id)
48
+ self._update(value)
49
+
50
+ async def rotate_credentials(self) -> None:
51
+ """Rotate only the DataOS gateway credential, not the upstream exit IP."""
52
+ async with self._lock:
53
+ self._require_open()
54
+ value = await self.client._rotate_proxy(
55
+ self.lease_id, credential_generation=self.credential_generation
56
+ )
57
+ self._update(value)
58
+
59
+ async def close(self) -> None:
60
+ async with self._lock:
61
+ if self._closed:
62
+ return
63
+ await self.client._release_proxy(self.lease_id)
64
+ self._closed = True
65
+ self.client._forget(self)
66
+
67
+ def _update(self, value: dict[str, Any]) -> None:
68
+ if "proxy_url" in value:
69
+ self.proxy_url = _required_text(value, "proxy_url")
70
+ if "expires_at" in value:
71
+ self.expires_at = _datetime(value.get("expires_at"))
72
+ if "credential_generation" in value:
73
+ generation = value.get("credential_generation")
74
+ self.credential_generation = int(generation) if generation is not None else None
75
+ if "network" in value:
76
+ self.network = _optional_text(value.get("network"))
77
+ if "node_id" in value:
78
+ self.node_id = _optional_text(value.get("node_id"))
79
+ if "line_tier" in value:
80
+ self.line_tier = _optional_text(value.get("line_tier"))
81
+ if "proxy_group" in value:
82
+ self.proxy_group = _optional_text(value.get("proxy_group"))
83
+ if "supplier_code" in value:
84
+ self.supplier_code = _optional_text(value.get("supplier_code"))
85
+ if "supplier_route_id" in value:
86
+ self.supplier_route_id = _optional_text(value.get("supplier_route_id"))
87
+ if "supplier_account_ref" in value:
88
+ self.supplier_account_ref = _optional_text(value.get("supplier_account_ref"))
89
+ if "line_policy_version" in value:
90
+ self.line_policy_version = _optional_text(value.get("line_policy_version"))
91
+
92
+ def _require_open(self) -> None:
93
+ if self._closed:
94
+ raise RuntimeError("proxy resource is closed")
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class CaptchaTask:
99
+ """Node-owned task snapshot; solver output still needs site acceptance."""
100
+
101
+ task_id: str
102
+ status: str
103
+ token: str | None = field(default=None, repr=False)
104
+ text: str | None = field(default=None, repr=False)
105
+ user_agent: str | None = field(default=None, repr=False)
106
+ error_code: str | None = None
107
+ node_id: str | None = None
108
+ network: str | None = None
109
+ data: dict[str, Any] | None = field(default=None, repr=False)
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class CaptchaPluginGrant:
114
+ """Short-lived permission for the managed extension adapter only."""
115
+
116
+ api_url: str
117
+ client_key: str = field(repr=False)
118
+ expires_at: datetime
119
+ node_id: str
120
+ network: str
121
+ protocol_version: int
122
+
123
+
124
+ @dataclass
125
+ class BrowserResource:
126
+ """One managed browser session owned by this client."""
127
+
128
+ client: ResourceProxy = field(repr=False)
129
+ resource_id: str
130
+ status: str
131
+ session_deadline: datetime | None = None
132
+ network: str | None = None
133
+ node_id: str | None = None
134
+ _closed: bool = field(default=False, init=False, repr=False)
135
+ _lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False, repr=False)
136
+
137
+ async def connection(self) -> tuple[str, dict[str, str]]:
138
+ async with self._lock:
139
+ self._require_open()
140
+ value = await self.client._browser_connection(self.resource_id)
141
+ ws_url = _required_text(value, "ws_url")
142
+ raw_headers = value.get("headers")
143
+ if not isinstance(raw_headers, dict) or not all(
144
+ isinstance(key, str) and isinstance(item, str)
145
+ for key, item in raw_headers.items()
146
+ ):
147
+ raise ValueError("resource service returned invalid browser headers")
148
+ return ws_url, dict(raw_headers)
149
+
150
+ async def refresh(self) -> None:
151
+ """Refresh the browser status and deadline."""
152
+ async with self._lock:
153
+ self._require_open()
154
+ value = await self.client._get_browser(self.resource_id)
155
+ if "status" in value:
156
+ self.status = str(value["status"])
157
+ if "session_deadline" in value or "expires_at" in value:
158
+ self.session_deadline = _datetime(
159
+ value.get("session_deadline") or value.get("expires_at")
160
+ )
161
+ if "network" in value:
162
+ self.network = _optional_text(value.get("network"))
163
+ if "node_id" in value:
164
+ self.node_id = _optional_text(value.get("node_id"))
165
+
166
+ async def close(self) -> None:
167
+ async with self._lock:
168
+ if self._closed:
169
+ return
170
+ await self.client._release_browser(self.resource_id)
171
+ self._closed = True
172
+ self.client._forget(self)
173
+
174
+ def _require_open(self) -> None:
175
+ if self._closed:
176
+ raise RuntimeError("browser resource is closed")
177
+
178
+
179
+ def _required_text(value: dict[str, Any], name: str) -> str:
180
+ result = value.get(name)
181
+ if not isinstance(result, str) or not result:
182
+ raise ValueError(f"resource service returned invalid {name}")
183
+ return result
184
+
185
+
186
+ def _datetime(value: Any) -> datetime | None:
187
+ if value is None:
188
+ return None
189
+ if not isinstance(value, str):
190
+ raise ValueError("resource service returned an invalid timestamp")
191
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
192
+
193
+
194
+ def _optional_text(value: Any) -> str | None:
195
+ if value is None:
196
+ return None
197
+ if not isinstance(value, str) or not value:
198
+ raise ValueError("resource service returned invalid resource metadata")
199
+ return value
File without changes
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.5
2
+ Name: dataapp-resource-proxy
3
+ Version: 0.1.0
4
+ Summary: Run-scoped client for managed browser, proxy and CAPTCHA resources
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: httpx<1,>=0.28
@@ -0,0 +1,10 @@
1
+ dataapp_resource_proxy/__init__.py,sha256=RHgtBZrNRZpzD5xTD8UaUCtOeT4M0rEYIAJqZg56UM0,405
2
+ dataapp_resource_proxy/__main__.py,sha256=nLRIRYJdKbIbfIa9aZUiHij28aS8cT-waGVWA0CUlzc,2417
3
+ dataapp_resource_proxy/_local_config.py,sha256=VeevtAmHCJZC1Gy4Xc6T0wXHweMDY5RopOBMwwcBNzo,11134
4
+ dataapp_resource_proxy/client.py,sha256=_ACYOpltkUsP8-pM_e3ZINCjnktORuQajXASYjmzSC8,76512
5
+ dataapp_resource_proxy/errors.py,sha256=ASFQ56uxUHlPx7wFySpk5sLO5-k4FcydCgW0dxY4zwo,378
6
+ dataapp_resource_proxy/handles.py,sha256=Y-bSgl7uCA-I01HhCTjMcNLCa4nCl081Bv6KLT0TgdI,7354
7
+ dataapp_resource_proxy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ dataapp_resource_proxy-0.1.0.dist-info/METADATA,sha256=FYg6XLXJMoLya6hpyEBqdTcz5_EqDqnS-GiY6PrazAE,196
9
+ dataapp_resource_proxy-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
10
+ dataapp_resource_proxy-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any