scalebrowser 0.7.0__tar.gz → 0.8.0__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.
- scalebrowser-0.8.0/CHANGELOG.md +54 -0
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/PKG-INFO +4 -2
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/pyproject.toml +11 -2
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/__init__.py +2 -2
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/_version.py +1 -1
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/client.py +22 -2
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/errors.py +2 -2
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/events.py +1 -2
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/models.py +25 -17
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/models_control.py +0 -3
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/models_identity.py +2 -3
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/models_videos.py +43 -8
- scalebrowser-0.7.0/examples/quickstart.py +0 -45
- scalebrowser-0.7.0/examples/quickstart_sync.py +0 -35
- scalebrowser-0.7.0/tests/__init__.py +0 -0
- scalebrowser-0.7.0/tests/conftest.py +0 -225
- scalebrowser-0.7.0/tests/test_cdp.py +0 -123
- scalebrowser-0.7.0/tests/test_client.py +0 -445
- scalebrowser-0.7.0/tests/test_coverage.py +0 -524
- scalebrowser-0.7.0/tests/test_e2e.py +0 -47
- scalebrowser-0.7.0/tests/test_errors.py +0 -49
- scalebrowser-0.7.0/tests/test_events.py +0 -141
- scalebrowser-0.7.0/tests/test_prose_guard.py +0 -106
- scalebrowser-0.7.0/tests/test_sync.py +0 -83
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/.gitignore +0 -0
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/LICENSE +0 -0
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/README.md +0 -0
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/_http.py +0 -0
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/_sync.py +0 -0
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/cdp.py +0 -0
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/models_runs.py +0 -0
- {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/models_tasks.py +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
The Python SDK for the Scalebrowser daemon.
|
|
4
|
+
|
|
5
|
+
This file starts at 0.5.0, the oldest release still on PyPI when it was written
|
|
6
|
+
(2026-09-09). What came before is in the repository's history. It was never
|
|
7
|
+
published as release notes, and reconstructing it here would look like a record
|
|
8
|
+
that had been kept all along.
|
|
9
|
+
|
|
10
|
+
The daemon and the SDK version independently. An SDK release names the daemon
|
|
11
|
+
version it was tested against wherever that matters.
|
|
12
|
+
|
|
13
|
+
## 0.8.0 (2026-09-11)
|
|
14
|
+
|
|
15
|
+
Breaking: `host_mode` is gone, and with it the `HostMode` alias and the
|
|
16
|
+
`HOST_MODES` tuple. It named one of two tiers, and there is only one left: a
|
|
17
|
+
daemon runs the engine built for its own operating system, so the tier is the
|
|
18
|
+
host. The field disappears from `Profile`, from `CreateProfileBody` and from a
|
|
19
|
+
preset's `config`. A client that keeps sending it is not refused; the daemon
|
|
20
|
+
ignores the key.
|
|
21
|
+
|
|
22
|
+
Reading a profile from a daemon of this generation needs this release: 0.7.0
|
|
23
|
+
refuses the answer, because `host_mode` and the persona's `headless_guards` are
|
|
24
|
+
no longer in it.
|
|
25
|
+
|
|
26
|
+
- The source distribution carries the package, the README, this file and the
|
|
27
|
+
licence, and no longer the repository's own tests and examples.
|
|
28
|
+
|
|
29
|
+
## 0.7.0 (2026-09-09)
|
|
30
|
+
|
|
31
|
+
- `check_proxy` and the exit-address views report the address KIND (mobile, fixed
|
|
32
|
+
line, datacenter, unknown) from an ASN dataset instead of the provider's own
|
|
33
|
+
description. The ownership term for an exit address follows that kind, so a
|
|
34
|
+
wrong classification used to be off by a factor of 30.
|
|
35
|
+
- A proxy row carries a name and lists the profiles bound to it.
|
|
36
|
+
- The exit-exclusivity rule has a switch, globally and per profile.
|
|
37
|
+
|
|
38
|
+
## 0.6.0 (2026-09-01)
|
|
39
|
+
|
|
40
|
+
- Remote access (the MCP tunnel) has a switch. It ships off, and it can only be
|
|
41
|
+
turned on at the machine itself.
|
|
42
|
+
|
|
43
|
+
## 0.5.0 (2026-08-31)
|
|
44
|
+
|
|
45
|
+
- Every profile carries a memory (`PROFILE.md`) and a task list. Both are
|
|
46
|
+
reachable through the client.
|
|
47
|
+
- `proxy_short_side` in both SDKs.
|
|
48
|
+
- The agent layer says "lease" throughout, matching the names of its tools.
|
|
49
|
+
|
|
50
|
+
## What this file does not promise
|
|
51
|
+
|
|
52
|
+
Nothing here promises a stable wire format. The daemon's REST surface is
|
|
53
|
+
versioned under `/v1` and this client tracks it. A breaking change there arrives
|
|
54
|
+
as a major version of the SDK, with the reason written in this file.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: scalebrowser
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Official Python SDK for the Scalebrowser daemon
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: Official Python SDK for the Scalebrowser daemon: typed REST client plus a direct-CDP driver (nodriver-style).
|
|
5
5
|
Project-URL: Homepage, https://scalebrowser.net
|
|
6
6
|
Project-URL: Documentation, https://scalebrowser.net
|
|
7
|
+
Project-URL: Examples, https://github.com/blackdiamond-labs/scalebrowser-mcp/tree/main/examples/python
|
|
7
8
|
Author: Scalebrowser
|
|
8
9
|
License-Expression: MIT
|
|
9
10
|
License-File: LICENSE
|
|
@@ -25,6 +26,7 @@ Provides-Extra: dev
|
|
|
25
26
|
Requires-Dist: build>=1.2; extra == 'dev'
|
|
26
27
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
27
28
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: tomli>=2.4.1; (python_version < '3.11') and extra == 'dev'
|
|
28
30
|
Description-Content-Type: text/markdown
|
|
29
31
|
|
|
30
32
|
# scalebrowser: the Python SDK
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scalebrowser"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Official Python SDK for the Scalebrowser daemon
|
|
7
|
+
version = "0.8.0"
|
|
8
|
+
description = "Official Python SDK for the Scalebrowser daemon: typed REST client plus a direct-CDP driver (nodriver-style)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = "MIT"
|
|
@@ -32,17 +32,26 @@ dependencies = [
|
|
|
32
32
|
[project.urls]
|
|
33
33
|
Homepage = "https://scalebrowser.net"
|
|
34
34
|
Documentation = "https://scalebrowser.net"
|
|
35
|
+
Examples = "https://github.com/blackdiamond-labs/scalebrowser-mcp/tree/main/examples/python"
|
|
35
36
|
|
|
36
37
|
[project.optional-dependencies]
|
|
37
38
|
dev = [
|
|
38
39
|
"pytest>=8",
|
|
39
40
|
"pytest-asyncio>=0.23",
|
|
40
41
|
"build>=1.2",
|
|
42
|
+
# The tests read this file; tomllib only exists from 3.11 on, and the floor is 3.10.
|
|
43
|
+
"tomli>=2.4.1; python_version < '3.11'",
|
|
41
44
|
]
|
|
42
45
|
|
|
43
46
|
[tool.hatch.build.targets.wheel]
|
|
44
47
|
packages = ["src/scalebrowser"]
|
|
45
48
|
|
|
49
|
+
# Das Quellpaket trug ohne diese Liste auch tests/ und examples/ nach pypi.org,
|
|
50
|
+
# und damit Verweise ins Repo und Gedankenstriche, die kein Gate sah. Es liefert
|
|
51
|
+
# jetzt dasselbe aus wie das Rad, plus die drei Texte, die ein Kunde liest.
|
|
52
|
+
[tool.hatch.build.targets.sdist]
|
|
53
|
+
only-include = ["src/scalebrowser", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
54
|
+
|
|
46
55
|
[tool.pytest.ini_options]
|
|
47
56
|
asyncio_mode = "auto"
|
|
48
57
|
testpaths = ["tests"]
|
|
@@ -74,6 +74,7 @@ from .models import (
|
|
|
74
74
|
AuditReport,
|
|
75
75
|
AuditStatus,
|
|
76
76
|
ExitStatus,
|
|
77
|
+
ProfileCounts,
|
|
77
78
|
Extension,
|
|
78
79
|
ExtensionPolicy,
|
|
79
80
|
CredentialBundle,
|
|
@@ -85,7 +86,6 @@ from .models import (
|
|
|
85
86
|
GeoMode,
|
|
86
87
|
GpuPersona,
|
|
87
88
|
Group,
|
|
88
|
-
HostMode,
|
|
89
89
|
Locale,
|
|
90
90
|
MediaDevice,
|
|
91
91
|
Metrics,
|
|
@@ -192,7 +192,6 @@ __all__ = [
|
|
|
192
192
|
"parse_sse_frame",
|
|
193
193
|
# enums
|
|
194
194
|
"RuntimeState",
|
|
195
|
-
"HostMode",
|
|
196
195
|
"GeoMode",
|
|
197
196
|
"ProxyKind",
|
|
198
197
|
"Rotation",
|
|
@@ -247,6 +246,7 @@ __all__ = [
|
|
|
247
246
|
"AuditReport",
|
|
248
247
|
"AuditStatus",
|
|
249
248
|
"ExitStatus",
|
|
249
|
+
"ProfileCounts",
|
|
250
250
|
"Extension",
|
|
251
251
|
"ExtensionPolicy",
|
|
252
252
|
"CredentialBundle",
|
|
@@ -25,6 +25,7 @@ from .models import (
|
|
|
25
25
|
RunStep,
|
|
26
26
|
AuditStatus,
|
|
27
27
|
ExitStatus,
|
|
28
|
+
ProfileCounts,
|
|
28
29
|
BulkAssignProxyBody,
|
|
29
30
|
BulkCreateBody,
|
|
30
31
|
BulkIdsBody,
|
|
@@ -99,6 +100,7 @@ from .models_videos import (
|
|
|
99
100
|
Recording,
|
|
100
101
|
RenderRequestBody,
|
|
101
102
|
ShareLinkLocal,
|
|
103
|
+
ShareStatus,
|
|
102
104
|
TtsProviderView,
|
|
103
105
|
TtsVerifyResult,
|
|
104
106
|
Video,
|
|
@@ -277,6 +279,24 @@ class AsyncScalebrowserClient:
|
|
|
277
279
|
params={"group": group, "state": state, "q": q, "sort": sort, "order": order},
|
|
278
280
|
)
|
|
279
281
|
|
|
282
|
+
async def count_profiles(
|
|
283
|
+
self,
|
|
284
|
+
*,
|
|
285
|
+
group: Optional[str] = None,
|
|
286
|
+
q: Optional[str] = None,
|
|
287
|
+
) -> ProfileCounts:
|
|
288
|
+
"""How many profiles match, split by runtime state, in one small response.
|
|
289
|
+
|
|
290
|
+
``list_profiles`` is paged and answers a bare array, so counting what came
|
|
291
|
+
back counts the page. ``list_profile_ids`` carries the same total but ships
|
|
292
|
+
an identifier per profile to do it, and cannot split by state at all.
|
|
293
|
+
|
|
294
|
+
There is deliberately no ``state`` parameter: the route ignores it, because
|
|
295
|
+
a caller labelling every state is sitting in one of them.
|
|
296
|
+
"""
|
|
297
|
+
data = await self._t.get("/v1/profiles/count", params={"group": group, "q": q})
|
|
298
|
+
return ProfileCounts.model_validate(data)
|
|
299
|
+
|
|
280
300
|
async def bulk_start(self, ids: Sequence[str], *, headless: Optional[bool] = None) -> Any:
|
|
281
301
|
"""Start a batch.
|
|
282
302
|
|
|
@@ -987,7 +1007,7 @@ class AsyncScalebrowserClient:
|
|
|
987
1007
|
data = await self._t.post(f"/v1/videos/{_q(video_id)}/share", body)
|
|
988
1008
|
return ShareLinkLocal.model_validate(data)
|
|
989
1009
|
|
|
990
|
-
async def share_status(self, video_id: str) ->
|
|
1010
|
+
async def share_status(self, video_id: str) -> ShareStatus:
|
|
991
1011
|
"""The share ORDER as this machine knows it.
|
|
992
1012
|
|
|
993
1013
|
How many people watched and whether the link still lives is read from the
|
|
@@ -995,7 +1015,7 @@ class AsyncScalebrowserClient:
|
|
|
995
1015
|
moment another machine revoked.
|
|
996
1016
|
"""
|
|
997
1017
|
data = await self._t.get(f"/v1/videos/{_q(video_id)}/share")
|
|
998
|
-
return
|
|
1018
|
+
return ShareStatus.model_validate(data)
|
|
999
1019
|
|
|
1000
1020
|
async def revoke_share(self, video_id: str) -> None:
|
|
1001
1021
|
"""Revoke the link. The files fall at once."""
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Typed SDK errors and the daemon's stable numeric error codes.
|
|
2
2
|
|
|
3
3
|
The daemon returns error bodies shaped ``{ "code": <4001-4013>, "message": "…" }``
|
|
4
|
-
|
|
4
|
+
and the matching HTTP status.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -10,7 +10,7 @@ from typing import Any, Final, Optional
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class ErrorCode:
|
|
13
|
-
"""
|
|
13
|
+
"""The daemon's stable numeric API error codes."""
|
|
14
14
|
|
|
15
15
|
NOT_FOUND: Final = 4001
|
|
16
16
|
PROXY_GEO: Final = 4002
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""Live lifecycle event stream (``GET /v1/events``, AC-API-003).
|
|
2
2
|
|
|
3
3
|
We read the ``text/event-stream`` over the authenticated transport and frame it
|
|
4
|
-
into the typed :data:`~scalebrowser.models.Event` union
|
|
5
|
-
of ``web-ui/src/api/events.ts``.
|
|
4
|
+
into the typed :data:`~scalebrowser.models.Event` union.
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
7
|
from __future__ import annotations
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
"""Wire models: a 1:1 mirror of the daemon's serialized domain model and
|
|
2
|
-
endpoint contract
|
|
3
|
-
``web-ui/src/api/types.ts``).
|
|
1
|
+
"""Wire models: a 1:1 mirror of the daemon's serialized domain model and of
|
|
2
|
+
its endpoint contract.
|
|
4
3
|
|
|
5
4
|
All Rust enums serialize ``snake_case``. Timestamps are unix seconds (``int``).
|
|
6
5
|
Read models tolerate unknown fields (forward-compat); request models are plain
|
|
@@ -19,10 +18,6 @@ from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
|
|
|
19
18
|
RuntimeState = Literal["stopped", "starting", "running", "crashed"]
|
|
20
19
|
RUNTIME_STATES: tuple[RuntimeState, ...] = ("stopped", "starting", "running", "crashed")
|
|
21
20
|
|
|
22
|
-
HostMode = Literal["linux_engine_spoof", "local_native_windows"]
|
|
23
|
-
HOST_MODES: tuple[HostMode, ...] = ("linux_engine_spoof", "local_native_windows")
|
|
24
|
-
|
|
25
|
-
|
|
26
21
|
GeoMode = Literal["strict_expected", "follow_exit", "off"]
|
|
27
22
|
GEO_MODES: tuple[GeoMode, ...] = ("strict_expected", "follow_exit", "off")
|
|
28
23
|
|
|
@@ -156,7 +151,6 @@ class PersonaMisc(_ReadModel):
|
|
|
156
151
|
get_installed_related_apps: bool
|
|
157
152
|
max_touch_points: int
|
|
158
153
|
pdf_viewer_enabled: bool
|
|
159
|
-
headless_guards: bool
|
|
160
154
|
|
|
161
155
|
|
|
162
156
|
class Persona(_ReadModel):
|
|
@@ -210,7 +204,6 @@ class Profile(_ReadModel):
|
|
|
210
204
|
seed: str
|
|
211
205
|
engine_version: str
|
|
212
206
|
persona: Persona
|
|
213
|
-
host_mode: HostMode
|
|
214
207
|
gpu_persona: Optional[str] = None
|
|
215
208
|
group_id: Optional[str] = None
|
|
216
209
|
proxy_id: Optional[str] = None
|
|
@@ -249,7 +242,6 @@ class PresetConfig(_ReadModel):
|
|
|
249
242
|
|
|
250
243
|
# Pin the batch to one engine build. Omit for per-profile version diversity.
|
|
251
244
|
engine_version: Optional[str] = None
|
|
252
|
-
host_mode: Optional[HostMode] = None
|
|
253
245
|
geo_mode: Optional[GeoMode] = None
|
|
254
246
|
# The geo GATE's expectation, not the persona's drawn region.
|
|
255
247
|
expected_country: Optional[str] = None
|
|
@@ -349,7 +341,6 @@ class ProxyRow(Proxy):
|
|
|
349
341
|
class CreateProfileBody(_WriteModel):
|
|
350
342
|
name: str
|
|
351
343
|
seed: Optional[str] = None
|
|
352
|
-
host_mode: Optional[HostMode] = None
|
|
353
344
|
geo_mode: Optional[GeoMode] = None
|
|
354
345
|
expected_country: Optional[str] = None
|
|
355
346
|
#: Draw the identity for this region (ISO-3166 alpha-2, e.g. ``"DE"``): language,
|
|
@@ -370,11 +361,9 @@ class CreateProfileBody(_WriteModel):
|
|
|
370
361
|
|
|
371
362
|
|
|
372
363
|
class UpdateProfileBody(_WriteModel):
|
|
373
|
-
#
|
|
374
|
-
#
|
|
375
|
-
#
|
|
376
|
-
# persona hangs off the seed; what does change a running profile's region is its
|
|
377
|
-
# exit, through geo_mode.
|
|
364
|
+
# seed and persona_country are fixed at create time (C5). The drawn region
|
|
365
|
+
# cannot move, because the whole persona hangs off the seed; what does change a
|
|
366
|
+
# running profile's region is its exit, through geo_mode.
|
|
378
367
|
name: Optional[str] = None
|
|
379
368
|
geo_mode: Optional[GeoMode] = None
|
|
380
369
|
expected_country: Optional[str] = None
|
|
@@ -941,7 +930,7 @@ EVENT_ADAPTER: TypeAdapter[Event] = TypeAdapter(Event)
|
|
|
941
930
|
|
|
942
931
|
__all__ = [
|
|
943
932
|
# enums
|
|
944
|
-
"RuntimeState", "RUNTIME_STATES",
|
|
933
|
+
"RuntimeState", "RUNTIME_STATES",
|
|
945
934
|
"GeoMode", "GEO_MODES", "ProxyKind", "PROXY_KINDS",
|
|
946
935
|
"Rotation", "ROTATIONS", "SessionKind", "OsFamily", "InputAction",
|
|
947
936
|
"RunActor", "RunOutcome", "StepKind",
|
|
@@ -1060,6 +1049,25 @@ class ExitClaimView(BaseModel):
|
|
|
1060
1049
|
expires_at: int
|
|
1061
1050
|
|
|
1062
1051
|
|
|
1052
|
+
class ProfileCounts(_ReadModel):
|
|
1053
|
+
"""How many profiles match, split by runtime state (``GET /v1/profiles/count``).
|
|
1054
|
+
|
|
1055
|
+
``total`` counts ROWS, so it equals the sum of the four states for every state
|
|
1056
|
+
this daemon can read; a row in a state a newer build introduced counts in
|
|
1057
|
+
``total`` alone rather than being folded into ``stopped``.
|
|
1058
|
+
|
|
1059
|
+
The ``state`` filter is the one the route ignores. A caller labelling all four
|
|
1060
|
+
scopes is sitting in one of them, so narrowing by it would answer the other
|
|
1061
|
+
three with zero.
|
|
1062
|
+
"""
|
|
1063
|
+
|
|
1064
|
+
total: int
|
|
1065
|
+
stopped: int = 0
|
|
1066
|
+
starting: int = 0
|
|
1067
|
+
running: int = 0
|
|
1068
|
+
crashed: int = 0
|
|
1069
|
+
|
|
1070
|
+
|
|
1063
1071
|
class ExitStatus(BaseModel):
|
|
1064
1072
|
"""A profile's exit-address situation.
|
|
1065
1073
|
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
"""What the browser asks (interruptions), the bytes that go in and out
|
|
2
2
|
(artifacts), and what the licence says (account).
|
|
3
|
-
|
|
4
|
-
Interruption shapes mirror ``web-ui/src/api/types/credentials.ts``, the shared
|
|
5
|
-
source of contract truth.
|
|
6
3
|
"""
|
|
7
4
|
|
|
8
5
|
from __future__ import annotations
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"""The account layer: mailboxes, passkeys and the one door a cookie value leaves
|
|
2
2
|
through.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
is a REST-only route deliberately kept off every screen.
|
|
4
|
+
The cookie reveal has no counterpart in the management app: it is a REST-only
|
|
5
|
+
route deliberately kept off every screen.
|
|
7
6
|
"""
|
|
8
7
|
|
|
9
8
|
from __future__ import annotations
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Videos and recordings (``/v1/videos``, ``/v1/recordings``).
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
directory or a file name (the daemon may be on another machine), so files are
|
|
3
|
+
A video row never carries a directory or a file name (the daemon may be on another machine), so files are
|
|
5
4
|
fetched through the grant route (:meth:`~scalebrowser.AsyncScalebrowserClient.video_access`
|
|
6
5
|
→ :meth:`~scalebrowser.AsyncScalebrowserClient.download_video_file`).
|
|
7
6
|
|
|
@@ -21,7 +20,7 @@ RecordingState = Literal["recording", "finishing", "finished", "failed"]
|
|
|
21
20
|
VideoState = Literal["queued", "rendering", "done", "failed", "cancelled"]
|
|
22
21
|
ExportPresetName = Literal["web", "social", "4k", "editing", "gif"]
|
|
23
22
|
AudioStatus = Literal["ok", "silent", "off", "failed"]
|
|
24
|
-
RenderStage = Literal["planning", "narration", "compositing", "encoding", "finishing"]
|
|
23
|
+
RenderStage = Literal["planning", "narration", "compositing", "encoding", "finishing", "extras"]
|
|
25
24
|
# Die Extras tragen dasselbe Praefix wie das Schluessel-Register des Daemons
|
|
26
25
|
# (`extra:<preset>`): ein zweiter Export ist unter dem Namen erreichbar, unter
|
|
27
26
|
# dem er abgelegt wurde, nicht unter einem, den der Aufrufer erfindet.
|
|
@@ -95,9 +94,11 @@ class TimestampState(_ReadModel):
|
|
|
95
94
|
|
|
96
95
|
|
|
97
96
|
class TranscriptWord(_ReadModel):
|
|
97
|
+
"""One spoken word and when it is heard in the finished video, in milliseconds from its start."""
|
|
98
|
+
|
|
98
99
|
text: str
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
start_ms: int
|
|
101
|
+
end_ms: int
|
|
101
102
|
|
|
102
103
|
|
|
103
104
|
class Video(_ReadModel):
|
|
@@ -143,7 +144,10 @@ class Recording(_ReadModel):
|
|
|
143
144
|
kind: RecordingKind
|
|
144
145
|
state: RecordingState
|
|
145
146
|
title: Optional[str] = None
|
|
147
|
+
#: The id of the stored preset the recording was made under, ``None`` when it named none.
|
|
146
148
|
preset_id: Optional[str] = None
|
|
149
|
+
#: That preset's name as it read when the recording started.
|
|
150
|
+
preset_name: Optional[str] = None
|
|
147
151
|
preset: dict[str, Any]
|
|
148
152
|
manifest_version: int
|
|
149
153
|
css_width: int
|
|
@@ -237,10 +241,12 @@ class VideosCapacity(_ReadModel):
|
|
|
237
241
|
class VideosStorage(_ReadModel):
|
|
238
242
|
recordings_bytes: int
|
|
239
243
|
videos_bytes: int
|
|
244
|
+
#: The main files of the videos this machine still shares.
|
|
240
245
|
link_bytes: int
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
#: Known only to the control plane; ``None`` here.
|
|
247
|
+
link_limit: Optional[int] = None
|
|
248
|
+
uploads_today: Optional[int] = None
|
|
249
|
+
uploads_limit: Optional[int] = None
|
|
244
250
|
capacity: VideosCapacity
|
|
245
251
|
|
|
246
252
|
|
|
@@ -257,10 +263,23 @@ class OpenH264Status(_ReadModel):
|
|
|
257
263
|
notice: str
|
|
258
264
|
|
|
259
265
|
|
|
266
|
+
class BlockedExport(_ReadModel):
|
|
267
|
+
"""One export the daemon refuses, e.g. ``4k`` while a 3840-wide capture is
|
|
268
|
+
not offered. ``reason`` starts with its snake code."""
|
|
269
|
+
|
|
270
|
+
export: ExportPresetName
|
|
271
|
+
reason: str
|
|
272
|
+
|
|
273
|
+
|
|
260
274
|
class CodecsStatus(_ReadModel):
|
|
261
275
|
openh264: OpenH264Status
|
|
262
276
|
#: Hardware encoders that passed a real test encode, in preference order.
|
|
263
277
|
hardware: list[str]
|
|
278
|
+
#: Whether the hardware probe has run. Before it, an empty ``hardware`` means
|
|
279
|
+
#: "not asked yet", not "none".
|
|
280
|
+
probed: bool = False
|
|
281
|
+
#: Exports this daemon refuses whatever the order says, with the reason.
|
|
282
|
+
exports_blocked: list[BlockedExport] = []
|
|
264
283
|
|
|
265
284
|
|
|
266
285
|
class MediaAsset(_ReadModel):
|
|
@@ -359,3 +378,19 @@ class ShareLinkLocal(_ReadModel):
|
|
|
359
378
|
download: bool
|
|
360
379
|
updated_at: int
|
|
361
380
|
error: Optional[str] = None
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class ShareStatus(ShareLinkLocal):
|
|
385
|
+
"""``GET /v1/videos/:id/share``: the order, and the link's access code when
|
|
386
|
+
this machine keeps one (sealed at rest).
|
|
387
|
+
|
|
388
|
+
A link the daemon makes for a preset with ``export.destination.passcode``
|
|
389
|
+
gets a generated code of ten characters, and this is where its owner reads
|
|
390
|
+
it. ``Video.share``, ``video_changed`` and the ``video`` tool never carry the
|
|
391
|
+
code.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
code: Optional[str] = None
|
|
395
|
+
"""Absent on the wire when this machine keeps no code: the link has none, or
|
|
396
|
+
its revocation is ordered. Reads as ``None`` then."""
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"""Async quickstart: start a profile, drive it over direct-CDP, humanize a click,
|
|
2
|
-
stop it.
|
|
3
|
-
|
|
4
|
-
SCALEBROWSER_TOKEN=… SCALEBROWSER_PROFILE_ID=… python examples/quickstart.py
|
|
5
|
-
|
|
6
|
-
Requires a running Scalebrowser daemon (the driver plane is direct-CDP, not
|
|
7
|
-
Playwright).
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import asyncio
|
|
13
|
-
import os
|
|
14
|
-
|
|
15
|
-
from scalebrowser import AsyncScalebrowserClient, CreateProfileBody
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
async def main() -> None:
|
|
19
|
-
base_url = os.environ.get("SCALEBROWSER_BASE_URL", "http://127.0.0.1:8787")
|
|
20
|
-
token = os.environ.get("SCALEBROWSER_TOKEN")
|
|
21
|
-
|
|
22
|
-
async with AsyncScalebrowserClient(base_url, token=token) as sb:
|
|
23
|
-
# Reuse a profile if given, otherwise create one.
|
|
24
|
-
profile_id = os.environ.get("SCALEBROWSER_PROFILE_ID")
|
|
25
|
-
if profile_id is None:
|
|
26
|
-
profile = await sb.create_profile(CreateProfileBody(name="quickstart"))
|
|
27
|
-
profile_id = profile.id
|
|
28
|
-
print(f"created profile {profile_id}")
|
|
29
|
-
|
|
30
|
-
# start → direct-CDP connect → navigate → humanized click → stop
|
|
31
|
-
async with sb.launch(profile_id, headless=True) as page:
|
|
32
|
-
await page.navigate("https://example.com")
|
|
33
|
-
title = await page.evaluate("document.title")
|
|
34
|
-
print(f"page title: {title!r}")
|
|
35
|
-
|
|
36
|
-
# Humanized trusted input goes through the daemon (G8), not raw CDP.
|
|
37
|
-
await page.humanize_move(120, 240)
|
|
38
|
-
await page.humanize_click(120, 240)
|
|
39
|
-
print("humanized click sent")
|
|
40
|
-
|
|
41
|
-
print("profile stopped")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if __name__ == "__main__":
|
|
45
|
-
asyncio.run(main())
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"""Synchronous quickstart — same flow as ``quickstart.py`` without async/await.
|
|
2
|
-
|
|
3
|
-
SCALEBROWSER_TOKEN=… SCALEBROWSER_PROFILE_ID=… python examples/quickstart_sync.py
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
import os
|
|
9
|
-
|
|
10
|
-
from scalebrowser import CreateProfileBody, ScalebrowserClient
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def main() -> None:
|
|
14
|
-
base_url = os.environ.get("SCALEBROWSER_BASE_URL", "http://127.0.0.1:8787")
|
|
15
|
-
token = os.environ.get("SCALEBROWSER_TOKEN")
|
|
16
|
-
|
|
17
|
-
with ScalebrowserClient(base_url, token=token) as sb:
|
|
18
|
-
profile_id = os.environ.get("SCALEBROWSER_PROFILE_ID")
|
|
19
|
-
if profile_id is None:
|
|
20
|
-
profile = sb.create_profile(CreateProfileBody(name="quickstart-sync"))
|
|
21
|
-
profile_id = profile.id
|
|
22
|
-
print(f"created profile {profile_id}")
|
|
23
|
-
|
|
24
|
-
with sb.launch(profile_id, headless=True) as page:
|
|
25
|
-
page.navigate("https://example.com")
|
|
26
|
-
print(f"page title: {page.evaluate('document.title')!r}")
|
|
27
|
-
page.humanize_move(120, 240)
|
|
28
|
-
page.humanize_click(120, 240)
|
|
29
|
-
print("humanized click sent")
|
|
30
|
-
|
|
31
|
-
print("profile stopped")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if __name__ == "__main__":
|
|
35
|
-
main()
|
|
File without changes
|