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.
Files changed (32) hide show
  1. scalebrowser-0.8.0/CHANGELOG.md +54 -0
  2. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/PKG-INFO +4 -2
  3. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/pyproject.toml +11 -2
  4. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/__init__.py +2 -2
  5. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/_version.py +1 -1
  6. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/client.py +22 -2
  7. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/errors.py +2 -2
  8. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/events.py +1 -2
  9. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/models.py +25 -17
  10. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/models_control.py +0 -3
  11. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/models_identity.py +2 -3
  12. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/models_videos.py +43 -8
  13. scalebrowser-0.7.0/examples/quickstart.py +0 -45
  14. scalebrowser-0.7.0/examples/quickstart_sync.py +0 -35
  15. scalebrowser-0.7.0/tests/__init__.py +0 -0
  16. scalebrowser-0.7.0/tests/conftest.py +0 -225
  17. scalebrowser-0.7.0/tests/test_cdp.py +0 -123
  18. scalebrowser-0.7.0/tests/test_client.py +0 -445
  19. scalebrowser-0.7.0/tests/test_coverage.py +0 -524
  20. scalebrowser-0.7.0/tests/test_e2e.py +0 -47
  21. scalebrowser-0.7.0/tests/test_errors.py +0 -49
  22. scalebrowser-0.7.0/tests/test_events.py +0 -141
  23. scalebrowser-0.7.0/tests/test_prose_guard.py +0 -106
  24. scalebrowser-0.7.0/tests/test_sync.py +0 -83
  25. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/.gitignore +0 -0
  26. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/LICENSE +0 -0
  27. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/README.md +0 -0
  28. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/_http.py +0 -0
  29. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/_sync.py +0 -0
  30. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/cdp.py +0 -0
  31. {scalebrowser-0.7.0 → scalebrowser-0.8.0}/src/scalebrowser/models_runs.py +0 -0
  32. {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.7.0
4
- Summary: Official Python SDK for the Scalebrowser daemon — typed REST client + direct-CDP driver (nodriver-style).
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.7.0"
8
- description = "Official Python SDK for the Scalebrowser daemon — typed REST client + direct-CDP driver (nodriver-style)."
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",
@@ -5,4 +5,4 @@ distribution actually carries. This file is a second copy of that number, not
5
5
  its source: hatchling never reads it.
6
6
  """
7
7
 
8
- __version__ = "0.7.0"
8
+ __version__ = "0.8.0"
@@ -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) -> ShareLinkLocal:
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 ShareLinkLocal.model_validate(data)
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
- with the HTTP status from ``core::Error::http_status()`` (docs/features/api.md, SPEC §5).
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
- """Stable numeric API error codes (SPEC §5, ``crates/core/src/error.rs``)."""
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, mirroring the behaviour
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 the
2
- endpoint contract in ``docs/features/api.md`` (kept in lockstep with
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
- # host_mode, seed and persona_country are fixed at create time (C4/C5): a
374
- # DIFFERING host_mode is refused with 400, an unchanged echo is accepted. A tier
375
- # change needs recreate. The drawn region cannot move either, because the whole
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", "HostMode", "HOST_MODES",
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
- Mailbox and passkey shapes mirror ``web-ui/src/api/types/{inboxes,credentials}.ts``,
5
- the shared source of contract truth. The cookie reveal has no UI counterpart: it
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
- Mirrors ``crates/core/src/video/rows.rs``. A video row never carries a
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
- from_ms: int
100
- to_ms: int
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
- link_limit: int
242
- uploads_today: int
243
- uploads_limit: int
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