mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-18 13:41:52 +02:00
feat: SDK for new routes
This commit is contained in:
1 parent
74c507bd9b
commit
9797b8b148
10 files changed
+1115
-12
No files matched your search
+1
-1
@@ -270,7 +270,7 @@ On top of that, one parameterised test per method drives it against a fake
|
||||
server and asserts the exact verb, path, query string and JSON body it sends.
|
||||
That is what ties the table to reality rather than to a comment.
|
||||
|
||||
Of the 71 published operations, 70 are wrapped. The one omission:
|
||||
Of the 84 published operations, 83 are wrapped. The one omission:
|
||||
|
||||
- `GET /v1/remote-sessions/{id}/cdp` is a WebSocket upgrade, not a request an
|
||||
HTTP client can make, and bundling a websocket implementation would end the
|
||||
|
||||
+66
-1
@@ -2,7 +2,7 @@
|
||||
"source": "src-tauri/src/api_server.rs",
|
||||
"regenerate_with": "python3 sdk/tools/extract-api-paths.py",
|
||||
"description": "Every operation the desktop app publishes in its /openapi.json. The SDK test suites assert this list and their own coverage tables match exactly, so an endpoint added to the app fails the SDK tests until it is either wrapped or deliberately listed as omitted.",
|
||||
"operation_count": 71,
|
||||
"operation_count": 84,
|
||||
"operations": [
|
||||
{
|
||||
"operation_id": "download_browser_api",
|
||||
@@ -174,6 +174,11 @@
|
||||
"method": "POST",
|
||||
"path": "/v1/profiles/batch/stop"
|
||||
},
|
||||
{
|
||||
"operation_id": "bulk_edit_profiles",
|
||||
"method": "POST",
|
||||
"path": "/v1/profiles/bulk-edit"
|
||||
},
|
||||
{
|
||||
"operation_id": "distribute_proxies",
|
||||
"method": "POST",
|
||||
@@ -264,6 +269,16 @@
|
||||
"method": "POST",
|
||||
"path": "/v1/profiles/{id}/run-remote"
|
||||
},
|
||||
{
|
||||
"operation_id": "export_storage_state",
|
||||
"method": "GET",
|
||||
"path": "/v1/profiles/{id}/storage-state"
|
||||
},
|
||||
{
|
||||
"operation_id": "import_storage_state",
|
||||
"method": "POST",
|
||||
"path": "/v1/profiles/{id}/storage-state"
|
||||
},
|
||||
{
|
||||
"operation_id": "get_proxies",
|
||||
"method": "GET",
|
||||
@@ -294,6 +309,11 @@
|
||||
"method": "PUT",
|
||||
"path": "/v1/proxies/{id}"
|
||||
},
|
||||
{
|
||||
"operation_id": "rotate_proxy",
|
||||
"method": "POST",
|
||||
"path": "/v1/proxies/{id}/rotate"
|
||||
},
|
||||
{
|
||||
"operation_id": "get_remote_hours",
|
||||
"method": "GET",
|
||||
@@ -319,11 +339,56 @@
|
||||
"method": "GET",
|
||||
"path": "/v1/remote-sessions/{id}/cdp"
|
||||
},
|
||||
{
|
||||
"operation_id": "get_statuses",
|
||||
"method": "GET",
|
||||
"path": "/v1/statuses"
|
||||
},
|
||||
{
|
||||
"operation_id": "create_status",
|
||||
"method": "POST",
|
||||
"path": "/v1/statuses"
|
||||
},
|
||||
{
|
||||
"operation_id": "delete_status",
|
||||
"method": "DELETE",
|
||||
"path": "/v1/statuses/{id}"
|
||||
},
|
||||
{
|
||||
"operation_id": "update_status",
|
||||
"method": "PUT",
|
||||
"path": "/v1/statuses/{id}"
|
||||
},
|
||||
{
|
||||
"operation_id": "get_tags",
|
||||
"method": "GET",
|
||||
"path": "/v1/tags"
|
||||
},
|
||||
{
|
||||
"operation_id": "get_templates",
|
||||
"method": "GET",
|
||||
"path": "/v1/templates"
|
||||
},
|
||||
{
|
||||
"operation_id": "create_template",
|
||||
"method": "POST",
|
||||
"path": "/v1/templates"
|
||||
},
|
||||
{
|
||||
"operation_id": "delete_template",
|
||||
"method": "DELETE",
|
||||
"path": "/v1/templates/{id}"
|
||||
},
|
||||
{
|
||||
"operation_id": "update_template",
|
||||
"method": "PUT",
|
||||
"path": "/v1/templates/{id}"
|
||||
},
|
||||
{
|
||||
"operation_id": "create_profiles_from_template",
|
||||
"method": "POST",
|
||||
"path": "/v1/templates/{id}/profiles"
|
||||
},
|
||||
{
|
||||
"operation_id": "get_vpns",
|
||||
"method": "GET",
|
||||
|
||||
+176
-2
@@ -12,15 +12,18 @@ import { DonutConnectionError, DonutError, errorForStatus } from "./errors.mts";
|
||||
import type {
|
||||
AgentClick,
|
||||
AgentTyping,
|
||||
ApiBulkCreateResponse,
|
||||
ApiGroupResponse,
|
||||
ApiProfileResponse,
|
||||
ApiProfilesResponse,
|
||||
ApiProxyResponse,
|
||||
ApiRemoteSessionsResponse,
|
||||
ApiStatusResponse,
|
||||
ApiVpnExportResponse,
|
||||
ApiVpnResponse,
|
||||
BatchRunResponse,
|
||||
BatchStopResponse,
|
||||
BulkEditResponse,
|
||||
CookieBotConflictCheck,
|
||||
CookieBotPresetList,
|
||||
CookieBotRun,
|
||||
@@ -46,14 +49,20 @@ import type {
|
||||
PerceptionPage,
|
||||
PickedElement,
|
||||
ProfileImportBatchResult,
|
||||
ProfilePatch,
|
||||
ProfileTemplate,
|
||||
ProxyPair,
|
||||
ProxySettings,
|
||||
RemoteHoursQuota,
|
||||
RemoteSessionState,
|
||||
RotationResult,
|
||||
RunProfileResponse,
|
||||
RunRemoteResponse,
|
||||
SetCloudSyncResponse,
|
||||
StopRemoteResponse,
|
||||
StorageState,
|
||||
StorageStateImport,
|
||||
TemplateSettings,
|
||||
WayfernConfig,
|
||||
} from "./types.mts";
|
||||
|
||||
@@ -286,6 +295,10 @@ export class DonutClient {
|
||||
* resolve names.
|
||||
*/
|
||||
dns_server?: string;
|
||||
/** A status id from `listStatuses`. */
|
||||
status_id?: string;
|
||||
/** At most 20 http(s) URLs; a bare host gets `https://`. */
|
||||
start_urls?: string[];
|
||||
}): Promise<ApiProfileResponse> {
|
||||
return this.#request("POST", "/v1/profiles", body({ ...request }));
|
||||
}
|
||||
@@ -315,6 +328,10 @@ export class DonutClient {
|
||||
clear_on_close?: boolean;
|
||||
/** Same formats as on create; `""` lets the proxy resolve names again. */
|
||||
dns_server?: string;
|
||||
/** `""` clears the status. */
|
||||
status_id?: string;
|
||||
/** Replaces the start pages; `[]` removes them. */
|
||||
start_urls?: string[];
|
||||
},
|
||||
): Promise<ApiProfileResponse> {
|
||||
return this.#request("PUT", `/v1/profiles/${segment(profileId)}`, body({ ...request }));
|
||||
@@ -437,6 +454,53 @@ export class DonutClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1/profiles/{id}/storage-state
|
||||
*
|
||||
* Playwright's `storageState`: each origin's Local Storage, plus the cookies
|
||||
* when `include_cookies` is true. Readable while the browser runs, but writes
|
||||
* from the last few seconds may not be on disk yet.
|
||||
*/
|
||||
exportStorageState(
|
||||
profileId: string,
|
||||
options: { include_cookies?: boolean } = {},
|
||||
): Promise<StorageState> {
|
||||
return this.#request(
|
||||
"GET",
|
||||
`/v1/profiles/${segment(profileId)}/storage-state`,
|
||||
undefined,
|
||||
query({ include_cookies: options.include_cookies }),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/profiles/{id}/storage-state
|
||||
*
|
||||
* `state` is a Playwright `storageState`; what `context.storageState()`
|
||||
* returns can be passed as is. An item replaces the one of the same name,
|
||||
* and the origin's other items stay. The browser must be stopped.
|
||||
*/
|
||||
importStorageState(profileId: string, state: StorageState): Promise<StorageStateImport> {
|
||||
return this.#request("POST", `/v1/profiles/${segment(profileId)}/storage-state`, {
|
||||
...state,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/profiles/bulk-edit
|
||||
*
|
||||
* Every profile is checked before any is saved, so a bad value, an unknown
|
||||
* profile or a running browser changes nothing. `patch` is sent as given: a
|
||||
* `null` in it clears that field. `changed` counts only the profiles that
|
||||
* really changed.
|
||||
*/
|
||||
bulkEditProfiles(profileIds: string[], patch: ProfilePatch): Promise<BulkEditResponse> {
|
||||
return this.#request("POST", "/v1/profiles/bulk-edit", {
|
||||
profile_ids: profileIds,
|
||||
patch,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/profiles/distribute-proxies
|
||||
*
|
||||
@@ -772,6 +836,98 @@ export class DonutClient {
|
||||
return this.#request("GET", "/v1/tags");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Statuses
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** GET /v1/statuses */
|
||||
listStatuses(): Promise<ApiStatusResponse[]> {
|
||||
return this.#request("GET", "/v1/statuses");
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/statuses
|
||||
*
|
||||
* `name` is 1 to 40 characters, unique regardless of case, and `color` is
|
||||
* `#rrggbb`.
|
||||
*/
|
||||
createStatus(request: { name: string; color: string }): Promise<ApiStatusResponse> {
|
||||
return this.#request("POST", "/v1/statuses", { ...request });
|
||||
}
|
||||
|
||||
/** PUT /v1/statuses/{id} */
|
||||
updateStatus(
|
||||
statusId: string,
|
||||
request: { name: string; color: string },
|
||||
): Promise<ApiStatusResponse> {
|
||||
return this.#request("PUT", `/v1/statuses/${segment(statusId)}`, { ...request });
|
||||
}
|
||||
|
||||
/** DELETE /v1/statuses/{id}. The status is also taken off every profile that had it. */
|
||||
deleteStatus(statusId: string): Promise<void> {
|
||||
return this.#request("DELETE", `/v1/statuses/${segment(statusId)}`);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Templates
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** GET /v1/templates */
|
||||
listTemplates(): Promise<ProfileTemplate[]> {
|
||||
return this.#request("GET", "/v1/templates");
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/templates
|
||||
*
|
||||
* `name` is 1 to 60 characters, unique regardless of case. Device fields in
|
||||
* `settings.wayfern_config` are dropped: every profile made from the
|
||||
* template gets its own device.
|
||||
*/
|
||||
createTemplate(request: {
|
||||
name: string;
|
||||
settings?: TemplateSettings;
|
||||
}): Promise<ProfileTemplate> {
|
||||
return this.#request("POST", "/v1/templates", body({ ...request }));
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /v1/templates/{id}
|
||||
*
|
||||
* Replaces the whole template. Settings left out are reset to empty, so to
|
||||
* rename one, send its current `settings` back with the new name.
|
||||
*/
|
||||
updateTemplate(
|
||||
templateId: string,
|
||||
request: { name: string; settings?: TemplateSettings },
|
||||
): Promise<ProfileTemplate> {
|
||||
return this.#request("PUT", `/v1/templates/${segment(templateId)}`, body({ ...request }));
|
||||
}
|
||||
|
||||
/** DELETE /v1/templates/{id}. Profiles made from the template keep their settings. */
|
||||
deleteTemplate(templateId: string): Promise<void> {
|
||||
return this.#request("DELETE", `/v1/templates/${segment(templateId)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/templates/{id}/profiles
|
||||
*
|
||||
* Makes `count` (1 to 100) profiles with the newest downloaded Wayfern, each
|
||||
* with its own device. One profile takes `name`; several take it followed
|
||||
* by numbers. When `error` is set, creation stopped early and `profiles`
|
||||
* lists what was made.
|
||||
*/
|
||||
createProfilesFromTemplate(
|
||||
templateId: string,
|
||||
request: { count: number; name?: string },
|
||||
): Promise<ApiBulkCreateResponse> {
|
||||
return this.#request(
|
||||
"POST",
|
||||
`/v1/templates/${segment(templateId)}/profiles`,
|
||||
body({ ...request }),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Proxies
|
||||
// ------------------------------------------------------------------
|
||||
@@ -790,14 +946,21 @@ export class DonutClient {
|
||||
createProxy(request: {
|
||||
name: string;
|
||||
proxy_settings: ProxySettings;
|
||||
/** The provider's http(s) "change IP" link, used by {@link rotateProxy}. */
|
||||
rotation_url?: string;
|
||||
}): Promise<ApiProxyResponse> {
|
||||
return this.#request("POST", "/v1/proxies", { ...request });
|
||||
return this.#request("POST", "/v1/proxies", body({ ...request }));
|
||||
}
|
||||
|
||||
/** PUT /v1/proxies/{id} */
|
||||
updateProxy(
|
||||
proxyId: string,
|
||||
request: { name?: string; proxy_settings?: ProxySettings },
|
||||
request: {
|
||||
name?: string;
|
||||
proxy_settings?: ProxySettings;
|
||||
/** `""` clears the "change IP" link. */
|
||||
rotation_url?: string;
|
||||
},
|
||||
): Promise<ApiProxyResponse> {
|
||||
return this.#request("PUT", `/v1/proxies/${segment(proxyId)}`, body({ ...request }));
|
||||
}
|
||||
@@ -807,6 +970,17 @@ export class DonutClient {
|
||||
return this.#request("DELETE", `/v1/proxies/${segment(proxyId)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/proxies/{id}/rotate
|
||||
*
|
||||
* Calls the proxy's "change IP" link; every profile behind the proxy moves
|
||||
* to the new IP. A proxy without a link throws `ValidationError`, and a
|
||||
* provider that refuses or does not answer throws `BadGateway`.
|
||||
*/
|
||||
rotateProxy(proxyId: string): Promise<RotationResult> {
|
||||
return this.#request("POST", `/v1/proxies/${segment(proxyId)}/rotate`);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/proxies/import
|
||||
*
|
||||
|
||||
@@ -53,6 +53,7 @@ export const OPERATIONS: ReadonlyMap<OperationKey, string> = new Map([
|
||||
["POST /v1/profiles", "createProfile"],
|
||||
["POST /v1/profiles/batch/run", "batchRunProfiles"],
|
||||
["POST /v1/profiles/batch/stop", "batchStopProfiles"],
|
||||
["POST /v1/profiles/bulk-edit", "bulkEditProfiles"],
|
||||
["POST /v1/profiles/distribute-proxies", "distributeProxies"],
|
||||
["POST /v1/profiles/import", "importProfiles"],
|
||||
["GET /v1/profiles/import/detect", "detectImportProfiles"],
|
||||
@@ -71,17 +72,29 @@ export const OPERATIONS: ReadonlyMap<OperationKey, string> = new Map([
|
||||
["POST /v1/profiles/{id}/open-url", "openUrl"],
|
||||
["POST /v1/profiles/{id}/run", "runProfile"],
|
||||
["POST /v1/profiles/{id}/run-remote", "runProfileRemote"],
|
||||
["GET /v1/profiles/{id}/storage-state", "exportStorageState"],
|
||||
["POST /v1/profiles/{id}/storage-state", "importStorageState"],
|
||||
["GET /v1/proxies", "listProxies"],
|
||||
["POST /v1/proxies", "createProxy"],
|
||||
["POST /v1/proxies/import", "importProxies"],
|
||||
["DELETE /v1/proxies/{id}", "deleteProxy"],
|
||||
["GET /v1/proxies/{id}", "getProxy"],
|
||||
["PUT /v1/proxies/{id}", "updateProxy"],
|
||||
["POST /v1/proxies/{id}/rotate", "rotateProxy"],
|
||||
["GET /v1/remote-hours", "getRemoteHours"],
|
||||
["GET /v1/remote-sessions", "listRemoteSessions"],
|
||||
["DELETE /v1/remote-sessions/{id}", "stopRemoteSession"],
|
||||
["GET /v1/remote-sessions/{id}", "getRemoteSession"],
|
||||
["GET /v1/statuses", "listStatuses"],
|
||||
["POST /v1/statuses", "createStatus"],
|
||||
["DELETE /v1/statuses/{id}", "deleteStatus"],
|
||||
["PUT /v1/statuses/{id}", "updateStatus"],
|
||||
["GET /v1/tags", "listTags"],
|
||||
["GET /v1/templates", "listTemplates"],
|
||||
["POST /v1/templates", "createTemplate"],
|
||||
["DELETE /v1/templates/{id}", "deleteTemplate"],
|
||||
["PUT /v1/templates/{id}", "updateTemplate"],
|
||||
["POST /v1/templates/{id}/profiles", "createProfilesFromTemplate"],
|
||||
["GET /v1/vpns", "listVpns"],
|
||||
["POST /v1/vpns", "createVpn"],
|
||||
["POST /v1/vpns/import", "importVpn"],
|
||||
|
||||
@@ -43,6 +43,13 @@ export interface ApiProfile {
|
||||
* VPN. `null` when the proxy resolves them.
|
||||
*/
|
||||
dns_server?: string | null;
|
||||
/** The profile's status id (see `listStatuses`), or `null`. */
|
||||
status_id?: string | null;
|
||||
/**
|
||||
* Pages a launch from the app opens, in order, instead of the last session.
|
||||
* A run with a `url` opens only that URL.
|
||||
*/
|
||||
start_urls: string[];
|
||||
extension_group_id?: string | null;
|
||||
ephemeral: boolean;
|
||||
temporary: boolean;
|
||||
@@ -75,10 +82,135 @@ export interface ApiGroupResponse {
|
||||
profile_count: number;
|
||||
}
|
||||
|
||||
/** A named, colored label for where a profile's account stands. */
|
||||
export interface ApiStatusResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
/** `#rrggbb`. */
|
||||
color: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* One change applied to many profiles by `bulkEditProfiles`.
|
||||
*
|
||||
* A property left out keeps that field as it is. For the fields that accept
|
||||
* `null`, an explicit `null` clears the field, so the patch is sent exactly as
|
||||
* given. An unknown property is refused with 400.
|
||||
*/
|
||||
export interface ProfilePatch {
|
||||
status_id?: string | null;
|
||||
tags_add?: string[];
|
||||
tags_remove?: string[];
|
||||
note?: string | null;
|
||||
group_id?: string | null;
|
||||
extension_group_id?: string | null;
|
||||
/** `light`, `normal`, `pro`, `pro_plus`, `ultimate`, `custom`, or `null`. */
|
||||
dns_blocklist?: string | null;
|
||||
dns_server?: string | null;
|
||||
proxy_bypass_rules?: string[];
|
||||
start_urls?: string[];
|
||||
launch_hook?: string | null;
|
||||
/** Left off ephemeral and password-protected profiles, where it means nothing. */
|
||||
clear_on_close?: boolean;
|
||||
randomize_fingerprint_on_launch?: boolean;
|
||||
/** `auto`, `tcp_only` or `block`; `null` clears it. */
|
||||
webrtc_mode?: string | null;
|
||||
block_images?: boolean;
|
||||
block_webgl?: boolean;
|
||||
restore_session?: boolean;
|
||||
/** Drop the stored device, so the next launch generates a new one. */
|
||||
new_device?: boolean;
|
||||
}
|
||||
|
||||
export interface BulkEditResponse {
|
||||
/** How many of the profiles actually changed. */
|
||||
changed: number;
|
||||
}
|
||||
|
||||
/** What every profile made from a template starts with. Left out is empty. */
|
||||
export interface TemplateSettings {
|
||||
/** What the profiles made from the template are named after. */
|
||||
name_prefix?: string | null;
|
||||
proxy_id?: string | null;
|
||||
vpn_id?: string | null;
|
||||
/** Proxies to spread new profiles across, one each in turn. Wins over `proxy_id`. */
|
||||
proxy_ids?: string[];
|
||||
group_id?: string | null;
|
||||
tags?: string[];
|
||||
status_id?: string | null;
|
||||
note?: string | null;
|
||||
extension_group_id?: string | null;
|
||||
dns_blocklist?: string | null;
|
||||
dns_server?: string | null;
|
||||
proxy_bypass_rules?: string[];
|
||||
start_urls?: string[];
|
||||
launch_hook?: string | null;
|
||||
ephemeral?: boolean;
|
||||
clear_on_close?: boolean;
|
||||
/** Fingerprint settings. The device fields are always dropped. */
|
||||
wayfern_config?: WayfernConfig | null;
|
||||
}
|
||||
|
||||
export interface ProfileTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
settings: TemplateSettings;
|
||||
position: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export interface ApiBulkCreateResponse {
|
||||
/** The profiles made, in order. */
|
||||
profiles: ApiProfile[];
|
||||
/** Why creation stopped before `count` (a `{"code": ...}` payload), or `null`. */
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface StorageItem {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface OriginStorage {
|
||||
/** `https://example.com`, the way a page's `origin` reads. */
|
||||
origin: string;
|
||||
localStorage?: StorageItem[];
|
||||
}
|
||||
|
||||
/** Playwright's `storageState`: cookies and each origin's Local Storage. */
|
||||
export interface StorageState {
|
||||
/**
|
||||
* Playwright's own cookie objects (`name`, `value`, `domain`, `path`,
|
||||
* `expires`, `httpOnly`, `secure`, `sameSite`), passed through untouched.
|
||||
*/
|
||||
cookies?: Record<string, unknown>[];
|
||||
origins?: OriginStorage[];
|
||||
}
|
||||
|
||||
export interface StorageStateImport {
|
||||
origins: number;
|
||||
items: number;
|
||||
cookies_imported: number;
|
||||
cookies_replaced: number;
|
||||
/** Codes of the cookies that could not be imported. */
|
||||
cookie_errors: string[];
|
||||
}
|
||||
|
||||
export interface ApiProxyResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
proxy_settings: ProxySettings;
|
||||
/** The provider's "change IP" link, or `null`. `rotateProxy` uses it. */
|
||||
rotation_url?: string | null;
|
||||
}
|
||||
|
||||
export interface RotationResult {
|
||||
proxy_id: string;
|
||||
/** The provider's HTTP status, when it answered. */
|
||||
status?: number | null;
|
||||
elapsed_ms: number;
|
||||
/** A coded error when the IP change was not confirmed. */
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface ApiVpnResponse {
|
||||
|
||||
@@ -26,6 +26,13 @@ interface Case {
|
||||
|
||||
const LOCATOR = { role: "button", name: "Sign in" };
|
||||
|
||||
const STORAGE_STATE = {
|
||||
cookies: [{ name: "sid", value: "1", domain: ".example.com", path: "/" }],
|
||||
origins: [{ origin: "https://example.com", localStorage: [{ name: "k", value: "v" }] }],
|
||||
};
|
||||
|
||||
const TEMPLATE_SETTINGS = { name_prefix: "Shop", tags: ["eu"], proxy_ids: ["x1", "x2"] };
|
||||
|
||||
const CASES: Case[] = [
|
||||
// -- profiles ------------------------------------------------------------
|
||||
{
|
||||
@@ -60,6 +67,26 @@ const CASES: Case[] = [
|
||||
body: { name: "Bare", browser: "wayfern" },
|
||||
operation: "POST /v1/profiles",
|
||||
},
|
||||
{
|
||||
method: "createProfile",
|
||||
args: [
|
||||
{
|
||||
name: "Started",
|
||||
browser: "wayfern",
|
||||
status_id: "st1",
|
||||
start_urls: ["https://example.com"],
|
||||
},
|
||||
],
|
||||
verb: "POST",
|
||||
path: "/v1/profiles",
|
||||
body: {
|
||||
name: "Started",
|
||||
browser: "wayfern",
|
||||
status_id: "st1",
|
||||
start_urls: ["https://example.com"],
|
||||
},
|
||||
operation: "POST /v1/profiles",
|
||||
},
|
||||
{
|
||||
method: "updateProfile",
|
||||
args: [
|
||||
@@ -81,6 +108,14 @@ const CASES: Case[] = [
|
||||
},
|
||||
operation: "PUT /v1/profiles/{id}",
|
||||
},
|
||||
{
|
||||
method: "updateProfile",
|
||||
args: ["p1", { status_id: "", start_urls: [] }],
|
||||
verb: "PUT",
|
||||
path: "/v1/profiles/p1",
|
||||
body: { status_id: "", start_urls: [] },
|
||||
operation: "PUT /v1/profiles/{id}",
|
||||
},
|
||||
{
|
||||
method: "deleteProfile",
|
||||
args: ["p1"],
|
||||
@@ -202,6 +237,42 @@ const CASES: Case[] = [
|
||||
body: { content: "[]" },
|
||||
operation: "POST /v1/profiles/{id}/cookies/import",
|
||||
},
|
||||
{
|
||||
method: "exportStorageState",
|
||||
args: ["p1", { include_cookies: true }],
|
||||
verb: "GET",
|
||||
path: "/v1/profiles/p1/storage-state",
|
||||
body: null,
|
||||
query: { include_cookies: "true" },
|
||||
operation: "GET /v1/profiles/{id}/storage-state",
|
||||
},
|
||||
{
|
||||
method: "exportStorageState",
|
||||
args: ["p1"],
|
||||
verb: "GET",
|
||||
path: "/v1/profiles/p1/storage-state",
|
||||
body: null,
|
||||
operation: "GET /v1/profiles/{id}/storage-state",
|
||||
},
|
||||
{
|
||||
method: "importStorageState",
|
||||
args: ["p1", STORAGE_STATE],
|
||||
verb: "POST",
|
||||
path: "/v1/profiles/p1/storage-state",
|
||||
body: STORAGE_STATE,
|
||||
operation: "POST /v1/profiles/{id}/storage-state",
|
||||
},
|
||||
{
|
||||
method: "bulkEditProfiles",
|
||||
args: [["p1", "p2"], { status_id: null, tags_add: ["eu"], new_device: true }],
|
||||
verb: "POST",
|
||||
path: "/v1/profiles/bulk-edit",
|
||||
body: {
|
||||
profile_ids: ["p1", "p2"],
|
||||
patch: { status_id: null, tags_add: ["eu"], new_device: true },
|
||||
},
|
||||
operation: "POST /v1/profiles/bulk-edit",
|
||||
},
|
||||
// -- agent ---------------------------------------------------------------
|
||||
{
|
||||
method: "agentPerceive",
|
||||
@@ -458,6 +529,88 @@ const CASES: Case[] = [
|
||||
body: null,
|
||||
operation: "GET /v1/tags",
|
||||
},
|
||||
// -- statuses ------------------------------------------------------------
|
||||
{
|
||||
method: "listStatuses",
|
||||
args: [],
|
||||
verb: "GET",
|
||||
path: "/v1/statuses",
|
||||
body: null,
|
||||
operation: "GET /v1/statuses",
|
||||
},
|
||||
{
|
||||
method: "createStatus",
|
||||
args: [{ name: "Warming", color: "#ffaa00" }],
|
||||
verb: "POST",
|
||||
path: "/v1/statuses",
|
||||
body: { name: "Warming", color: "#ffaa00" },
|
||||
operation: "POST /v1/statuses",
|
||||
},
|
||||
{
|
||||
method: "updateStatus",
|
||||
args: ["st1", { name: "Ready", color: "#00aa55" }],
|
||||
verb: "PUT",
|
||||
path: "/v1/statuses/st1",
|
||||
body: { name: "Ready", color: "#00aa55" },
|
||||
operation: "PUT /v1/statuses/{id}",
|
||||
},
|
||||
{
|
||||
method: "deleteStatus",
|
||||
args: ["st1"],
|
||||
verb: "DELETE",
|
||||
path: "/v1/statuses/st1",
|
||||
body: null,
|
||||
operation: "DELETE /v1/statuses/{id}",
|
||||
},
|
||||
// -- templates -----------------------------------------------------------
|
||||
{
|
||||
method: "listTemplates",
|
||||
args: [],
|
||||
verb: "GET",
|
||||
path: "/v1/templates",
|
||||
body: null,
|
||||
operation: "GET /v1/templates",
|
||||
},
|
||||
{
|
||||
method: "createTemplate",
|
||||
args: [{ name: "Shop EU", settings: TEMPLATE_SETTINGS }],
|
||||
verb: "POST",
|
||||
path: "/v1/templates",
|
||||
body: { name: "Shop EU", settings: TEMPLATE_SETTINGS },
|
||||
operation: "POST /v1/templates",
|
||||
},
|
||||
{
|
||||
method: "createTemplate",
|
||||
args: [{ name: "Empty", settings: undefined }],
|
||||
verb: "POST",
|
||||
path: "/v1/templates",
|
||||
body: { name: "Empty" },
|
||||
operation: "POST /v1/templates",
|
||||
},
|
||||
{
|
||||
method: "updateTemplate",
|
||||
args: ["t1", { name: "Shop EU 2", settings: TEMPLATE_SETTINGS }],
|
||||
verb: "PUT",
|
||||
path: "/v1/templates/t1",
|
||||
body: { name: "Shop EU 2", settings: TEMPLATE_SETTINGS },
|
||||
operation: "PUT /v1/templates/{id}",
|
||||
},
|
||||
{
|
||||
method: "deleteTemplate",
|
||||
args: ["t1"],
|
||||
verb: "DELETE",
|
||||
path: "/v1/templates/t1",
|
||||
body: null,
|
||||
operation: "DELETE /v1/templates/{id}",
|
||||
},
|
||||
{
|
||||
method: "createProfilesFromTemplate",
|
||||
args: ["t1", { count: 3, name: "Shop" }],
|
||||
verb: "POST",
|
||||
path: "/v1/templates/t1/profiles",
|
||||
body: { count: 3, name: "Shop" },
|
||||
operation: "POST /v1/templates/{id}/profiles",
|
||||
},
|
||||
// -- proxies -------------------------------------------------------------
|
||||
{
|
||||
method: "listProxies",
|
||||
@@ -483,6 +636,24 @@ const CASES: Case[] = [
|
||||
body: { name: "EU", proxy_settings: { proxy_type: "http", host: "h", port: 8080 } },
|
||||
operation: "POST /v1/proxies",
|
||||
},
|
||||
{
|
||||
method: "createProxy",
|
||||
args: [
|
||||
{
|
||||
name: "Rotating",
|
||||
proxy_settings: { proxy_type: "http", host: "h", port: 8080 },
|
||||
rotation_url: "https://provider.example/rotate?key=1",
|
||||
},
|
||||
],
|
||||
verb: "POST",
|
||||
path: "/v1/proxies",
|
||||
body: {
|
||||
name: "Rotating",
|
||||
proxy_settings: { proxy_type: "http", host: "h", port: 8080 },
|
||||
rotation_url: "https://provider.example/rotate?key=1",
|
||||
},
|
||||
operation: "POST /v1/proxies",
|
||||
},
|
||||
{
|
||||
method: "updateProxy",
|
||||
args: ["x1", { name: "EU 2" }],
|
||||
@@ -499,6 +670,22 @@ const CASES: Case[] = [
|
||||
body: null,
|
||||
operation: "DELETE /v1/proxies/{id}",
|
||||
},
|
||||
{
|
||||
method: "updateProxy",
|
||||
args: ["x1", { rotation_url: "" }],
|
||||
verb: "PUT",
|
||||
path: "/v1/proxies/x1",
|
||||
body: { rotation_url: "" },
|
||||
operation: "PUT /v1/proxies/{id}",
|
||||
},
|
||||
{
|
||||
method: "rotateProxy",
|
||||
args: ["x1"],
|
||||
verb: "POST",
|
||||
path: "/v1/proxies/x1/rotate",
|
||||
body: null,
|
||||
operation: "POST /v1/proxies/{id}/rotate",
|
||||
},
|
||||
{
|
||||
method: "importProxies",
|
||||
args: [{ format: "txt", content: "h:1:u:p", name_prefix: "EU" }],
|
||||
|
||||
@@ -22,15 +22,18 @@ from .errors import DonutConnectionError, DonutError, error_for_status
|
||||
from .models import (
|
||||
AgentClick,
|
||||
AgentTyping,
|
||||
ApiBulkCreateResponse,
|
||||
ApiGroupResponse,
|
||||
ApiProfileResponse,
|
||||
ApiProfilesResponse,
|
||||
ApiProxyResponse,
|
||||
ApiRemoteSessionsResponse,
|
||||
ApiStatusResponse,
|
||||
ApiVpnExportResponse,
|
||||
ApiVpnResponse,
|
||||
BatchRunResponse,
|
||||
BatchStopResponse,
|
||||
BulkEditResponse,
|
||||
CookieBotConflictCheck,
|
||||
CookieBotPresetList,
|
||||
CookieBotRun,
|
||||
@@ -56,14 +59,20 @@ from .models import (
|
||||
PerceptionPage,
|
||||
PickedElement,
|
||||
ProfileImportBatchResult,
|
||||
ProfilePatch,
|
||||
ProfileTemplate,
|
||||
ProxyPair,
|
||||
ProxySettings,
|
||||
RemoteHoursQuota,
|
||||
RemoteSessionState,
|
||||
RotationResult,
|
||||
RunProfileResponse,
|
||||
RunRemoteResponse,
|
||||
SetCloudSyncResponse,
|
||||
StopRemoteResponse,
|
||||
StorageState,
|
||||
StorageStateImport,
|
||||
TemplateSettings,
|
||||
WayfernConfig,
|
||||
)
|
||||
|
||||
@@ -289,6 +298,8 @@ class DonutClient:
|
||||
ephemeral: Optional[bool] = None,
|
||||
temporary: Optional[bool] = None,
|
||||
dns_server: Optional[str] = None,
|
||||
status_id: Optional[str] = None,
|
||||
start_urls: Optional[Sequence[str]] = None,
|
||||
) -> ApiProfileResponse:
|
||||
"""POST /v1/profiles
|
||||
|
||||
@@ -298,7 +309,8 @@ class DonutClient:
|
||||
names through the profile's proxy or VPN: ``https://host/path``,
|
||||
``tls://host[:port]``, ``tcp://host[:port]`` or a bare ``host[:port]``;
|
||||
a bare address of a well-known resolver such as ``"1.1.1.1"`` uses its
|
||||
DNS over HTTPS endpoint.
|
||||
DNS over HTTPS endpoint. ``status_id`` comes from :meth:`list_statuses`;
|
||||
``start_urls`` is at most 20 http(s) URLs.
|
||||
"""
|
||||
return self._request(
|
||||
"POST",
|
||||
@@ -317,6 +329,8 @@ class DonutClient:
|
||||
ephemeral=ephemeral,
|
||||
temporary=temporary,
|
||||
dns_server=dns_server,
|
||||
status_id=status_id,
|
||||
start_urls=list(start_urls) if start_urls is not None else None,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -337,13 +351,16 @@ class DonutClient:
|
||||
sync_mode: Optional[str] = None,
|
||||
clear_on_close: Optional[bool] = None,
|
||||
dns_server: Optional[str] = None,
|
||||
status_id: Optional[str] = None,
|
||||
start_urls: Optional[Sequence[str]] = None,
|
||||
) -> ApiProfileResponse:
|
||||
"""PUT /v1/profiles/{id}
|
||||
|
||||
A profile's browser engine is fixed at creation, so there is no
|
||||
``browser`` argument. Pass ``proxy_id=""`` or ``vpn_id=""`` to detach
|
||||
one; leaving either unset changes nothing. ``dns_server=""`` lets the
|
||||
proxy resolve names again.
|
||||
proxy resolve names again, ``status_id=""`` clears the status, and
|
||||
``start_urls=[]`` removes the start pages.
|
||||
"""
|
||||
return self._request(
|
||||
"PUT",
|
||||
@@ -364,6 +381,8 @@ class DonutClient:
|
||||
sync_mode=sync_mode,
|
||||
clear_on_close=clear_on_close,
|
||||
dns_server=dns_server,
|
||||
status_id=status_id,
|
||||
start_urls=list(start_urls) if start_urls is not None else None,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -497,6 +516,51 @@ class DonutClient:
|
||||
body={"content": content},
|
||||
)
|
||||
|
||||
def export_storage_state(
|
||||
self, profile_id: str, *, include_cookies: Optional[bool] = None
|
||||
) -> StorageState:
|
||||
"""GET /v1/profiles/{id}/storage-state
|
||||
|
||||
Playwright's ``storageState``: each origin's Local Storage, plus the
|
||||
cookies when ``include_cookies=True``. Readable while the browser runs,
|
||||
but writes from the last few seconds may not be on disk yet.
|
||||
"""
|
||||
return self._request(
|
||||
"GET",
|
||||
f"/v1/profiles/{_segment(profile_id)}/storage-state",
|
||||
query=_query(include_cookies=include_cookies),
|
||||
)
|
||||
|
||||
def import_storage_state(self, profile_id: str, state: StorageState) -> StorageStateImport:
|
||||
"""POST /v1/profiles/{id}/storage-state
|
||||
|
||||
``state`` is a Playwright ``storageState``; what
|
||||
``context.storage_state()`` returns can be passed as is. An item
|
||||
replaces the one of the same name, and the origin's other items stay.
|
||||
The browser must be stopped.
|
||||
"""
|
||||
return self._request(
|
||||
"POST",
|
||||
f"/v1/profiles/{_segment(profile_id)}/storage-state",
|
||||
body=dict(state),
|
||||
)
|
||||
|
||||
def bulk_edit_profiles(
|
||||
self, profile_ids: Sequence[str], patch: ProfilePatch
|
||||
) -> BulkEditResponse:
|
||||
"""POST /v1/profiles/bulk-edit
|
||||
|
||||
Every profile is checked before any is saved, so a bad value, an
|
||||
unknown profile or a running browser changes nothing. ``patch`` is sent
|
||||
as given: a ``None`` in it clears that field rather than being dropped.
|
||||
``changed`` counts only the profiles that really changed.
|
||||
"""
|
||||
return self._request(
|
||||
"POST",
|
||||
"/v1/profiles/bulk-edit",
|
||||
body={"profile_ids": list(profile_ids), "patch": dict(patch)},
|
||||
)
|
||||
|
||||
def distribute_proxies(self, pairs: Sequence[ProxyPair]) -> DistributeProxiesResponse:
|
||||
"""POST /v1/profiles/distribute-proxies
|
||||
|
||||
@@ -849,6 +913,101 @@ class DonutClient:
|
||||
"""GET /v1/tags"""
|
||||
return self._request("GET", "/v1/tags")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Statuses
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def list_statuses(self) -> List[ApiStatusResponse]:
|
||||
"""GET /v1/statuses"""
|
||||
return self._request("GET", "/v1/statuses")
|
||||
|
||||
def create_status(self, *, name: str, color: str) -> ApiStatusResponse:
|
||||
"""POST /v1/statuses
|
||||
|
||||
``name`` is 1 to 40 characters, unique regardless of case, and
|
||||
``color`` is ``#rrggbb``.
|
||||
"""
|
||||
return self._request("POST", "/v1/statuses", body={"name": name, "color": color})
|
||||
|
||||
def update_status(self, status_id: str, *, name: str, color: str) -> ApiStatusResponse:
|
||||
"""PUT /v1/statuses/{id}"""
|
||||
return self._request(
|
||||
"PUT",
|
||||
f"/v1/statuses/{_segment(status_id)}",
|
||||
body={"name": name, "color": color},
|
||||
)
|
||||
|
||||
def delete_status(self, status_id: str) -> None:
|
||||
"""DELETE /v1/statuses/{id}
|
||||
|
||||
The status is also taken off every profile that had it.
|
||||
"""
|
||||
self._request("DELETE", f"/v1/statuses/{_segment(status_id)}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Templates
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def list_templates(self) -> List[ProfileTemplate]:
|
||||
"""GET /v1/templates"""
|
||||
return self._request("GET", "/v1/templates")
|
||||
|
||||
def create_template(
|
||||
self, *, name: str, settings: Optional[TemplateSettings] = None
|
||||
) -> ProfileTemplate:
|
||||
"""POST /v1/templates
|
||||
|
||||
``name`` is 1 to 60 characters, unique regardless of case. Device
|
||||
fields in ``settings["wayfern_config"]`` are dropped: every profile
|
||||
made from the template gets its own device.
|
||||
"""
|
||||
return self._request(
|
||||
"POST",
|
||||
"/v1/templates",
|
||||
body=_body(
|
||||
name=name, settings=dict(settings) if settings is not None else None
|
||||
),
|
||||
)
|
||||
|
||||
def update_template(
|
||||
self, template_id: str, *, name: str, settings: Optional[TemplateSettings] = None
|
||||
) -> ProfileTemplate:
|
||||
"""PUT /v1/templates/{id}
|
||||
|
||||
Replaces the whole template. Settings left out are reset to empty, so
|
||||
to rename one, send its current ``settings`` back with the new name.
|
||||
"""
|
||||
return self._request(
|
||||
"PUT",
|
||||
f"/v1/templates/{_segment(template_id)}",
|
||||
body=_body(
|
||||
name=name, settings=dict(settings) if settings is not None else None
|
||||
),
|
||||
)
|
||||
|
||||
def delete_template(self, template_id: str) -> None:
|
||||
"""DELETE /v1/templates/{id}
|
||||
|
||||
Profiles made from the template keep their settings.
|
||||
"""
|
||||
self._request("DELETE", f"/v1/templates/{_segment(template_id)}")
|
||||
|
||||
def create_profiles_from_template(
|
||||
self, template_id: str, *, count: int, name: Optional[str] = None
|
||||
) -> ApiBulkCreateResponse:
|
||||
"""POST /v1/templates/{id}/profiles
|
||||
|
||||
Makes ``count`` (1 to 100) profiles with the newest downloaded Wayfern,
|
||||
each with its own device. One profile takes ``name``; several take it
|
||||
followed by numbers. When ``error`` is set, creation stopped early and
|
||||
``profiles`` lists what was made.
|
||||
"""
|
||||
return self._request(
|
||||
"POST",
|
||||
f"/v1/templates/{_segment(template_id)}/profiles",
|
||||
body=_body(count=count, name=name),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Proxies
|
||||
# ------------------------------------------------------------------
|
||||
@@ -861,12 +1020,24 @@ class DonutClient:
|
||||
"""GET /v1/proxies/{id}"""
|
||||
return self._request("GET", f"/v1/proxies/{_segment(proxy_id)}")
|
||||
|
||||
def create_proxy(self, *, name: str, proxy_settings: ProxySettings) -> ApiProxyResponse:
|
||||
"""POST /v1/proxies"""
|
||||
def create_proxy(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
proxy_settings: ProxySettings,
|
||||
rotation_url: Optional[str] = None,
|
||||
) -> ApiProxyResponse:
|
||||
"""POST /v1/proxies
|
||||
|
||||
``rotation_url`` is the provider's http(s) "change IP" link, used by
|
||||
:meth:`rotate_proxy`.
|
||||
"""
|
||||
return self._request(
|
||||
"POST",
|
||||
"/v1/proxies",
|
||||
body={"name": name, "proxy_settings": dict(proxy_settings)},
|
||||
body=_body(
|
||||
name=name, proxy_settings=dict(proxy_settings), rotation_url=rotation_url
|
||||
),
|
||||
)
|
||||
|
||||
def update_proxy(
|
||||
@@ -875,14 +1046,19 @@ class DonutClient:
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
proxy_settings: Optional[ProxySettings] = None,
|
||||
rotation_url: Optional[str] = None,
|
||||
) -> ApiProxyResponse:
|
||||
"""PUT /v1/proxies/{id}"""
|
||||
"""PUT /v1/proxies/{id}
|
||||
|
||||
``rotation_url=""`` clears the "change IP" link.
|
||||
"""
|
||||
return self._request(
|
||||
"PUT",
|
||||
f"/v1/proxies/{_segment(proxy_id)}",
|
||||
body=_body(
|
||||
name=name,
|
||||
proxy_settings=dict(proxy_settings) if proxy_settings is not None else None,
|
||||
rotation_url=rotation_url,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -890,6 +1066,16 @@ class DonutClient:
|
||||
"""DELETE /v1/proxies/{id}"""
|
||||
self._request("DELETE", f"/v1/proxies/{_segment(proxy_id)}")
|
||||
|
||||
def rotate_proxy(self, proxy_id: str) -> RotationResult:
|
||||
"""POST /v1/proxies/{id}/rotate
|
||||
|
||||
Calls the proxy's "change IP" link; every profile behind the proxy
|
||||
moves to the new IP. A proxy without a link raises
|
||||
:class:`~donutbrowser.errors.ValidationError`, and a provider that
|
||||
refuses or does not answer raises :class:`~donutbrowser.errors.BadGateway`.
|
||||
"""
|
||||
return self._request("POST", f"/v1/proxies/{_segment(proxy_id)}/rotate")
|
||||
|
||||
def import_proxies(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -60,6 +60,7 @@ OPERATIONS: Dict[Operation, str] = {
|
||||
("POST", "/v1/profiles"): "create_profile",
|
||||
("POST", "/v1/profiles/batch/run"): "batch_run_profiles",
|
||||
("POST", "/v1/profiles/batch/stop"): "batch_stop_profiles",
|
||||
("POST", "/v1/profiles/bulk-edit"): "bulk_edit_profiles",
|
||||
("POST", "/v1/profiles/distribute-proxies"): "distribute_proxies",
|
||||
("POST", "/v1/profiles/import"): "import_profiles",
|
||||
("GET", "/v1/profiles/import/detect"): "detect_import_profiles",
|
||||
@@ -78,17 +79,29 @@ OPERATIONS: Dict[Operation, str] = {
|
||||
("POST", "/v1/profiles/{id}/open-url"): "open_url",
|
||||
("POST", "/v1/profiles/{id}/run"): "run_profile",
|
||||
("POST", "/v1/profiles/{id}/run-remote"): "run_profile_remote",
|
||||
("GET", "/v1/profiles/{id}/storage-state"): "export_storage_state",
|
||||
("POST", "/v1/profiles/{id}/storage-state"): "import_storage_state",
|
||||
("GET", "/v1/proxies"): "list_proxies",
|
||||
("POST", "/v1/proxies"): "create_proxy",
|
||||
("POST", "/v1/proxies/import"): "import_proxies",
|
||||
("DELETE", "/v1/proxies/{id}"): "delete_proxy",
|
||||
("GET", "/v1/proxies/{id}"): "get_proxy",
|
||||
("PUT", "/v1/proxies/{id}"): "update_proxy",
|
||||
("POST", "/v1/proxies/{id}/rotate"): "rotate_proxy",
|
||||
("GET", "/v1/remote-hours"): "get_remote_hours",
|
||||
("GET", "/v1/remote-sessions"): "list_remote_sessions",
|
||||
("DELETE", "/v1/remote-sessions/{id}"): "stop_remote_session",
|
||||
("GET", "/v1/remote-sessions/{id}"): "get_remote_session",
|
||||
("GET", "/v1/statuses"): "list_statuses",
|
||||
("POST", "/v1/statuses"): "create_status",
|
||||
("DELETE", "/v1/statuses/{id}"): "delete_status",
|
||||
("PUT", "/v1/statuses/{id}"): "update_status",
|
||||
("GET", "/v1/tags"): "list_tags",
|
||||
("GET", "/v1/templates"): "list_templates",
|
||||
("POST", "/v1/templates"): "create_template",
|
||||
("DELETE", "/v1/templates/{id}"): "delete_template",
|
||||
("PUT", "/v1/templates/{id}"): "update_template",
|
||||
("POST", "/v1/templates/{id}/profiles"): "create_profiles_from_template",
|
||||
("GET", "/v1/vpns"): "list_vpns",
|
||||
("POST", "/v1/vpns"): "create_vpn",
|
||||
("POST", "/v1/vpns/import"): "import_vpn",
|
||||
|
||||
@@ -17,14 +17,25 @@ passed straight into the next.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, TypedDict
|
||||
from typing import Any, Dict, List, Optional, TypedDict
|
||||
|
||||
__all__ = [
|
||||
"ApiProfile",
|
||||
"ApiProfilesResponse",
|
||||
"ApiProfileResponse",
|
||||
"ApiGroupResponse",
|
||||
"ApiStatusResponse",
|
||||
"ProfilePatch",
|
||||
"BulkEditResponse",
|
||||
"TemplateSettings",
|
||||
"ProfileTemplate",
|
||||
"ApiBulkCreateResponse",
|
||||
"StorageItem",
|
||||
"OriginStorage",
|
||||
"StorageState",
|
||||
"StorageStateImport",
|
||||
"ApiProxyResponse",
|
||||
"RotationResult",
|
||||
"ApiVpnResponse",
|
||||
"ApiVpnExportResponse",
|
||||
"DownloadBrowserResponse",
|
||||
@@ -102,6 +113,7 @@ class _ApiProfileRequired(TypedDict):
|
||||
tags: List[str]
|
||||
is_running: bool
|
||||
proxy_bypass_rules: List[str]
|
||||
start_urls: List[str]
|
||||
ephemeral: bool
|
||||
temporary: bool
|
||||
clear_on_close: bool
|
||||
@@ -118,6 +130,7 @@ class ApiProfile(_ApiProfileRequired, total=False):
|
||||
group_id: str
|
||||
vpn_id: str
|
||||
dns_server: str
|
||||
status_id: str
|
||||
extension_group_id: str
|
||||
host_os: str
|
||||
fingerprint_os: str
|
||||
@@ -138,12 +151,146 @@ class ApiGroupResponse(TypedDict):
|
||||
profile_count: int
|
||||
|
||||
|
||||
class ApiProxyResponse(TypedDict):
|
||||
class ApiStatusResponse(TypedDict):
|
||||
"""A named, colored label for where a profile's account stands."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
color: str
|
||||
|
||||
|
||||
class ProfilePatch(TypedDict, total=False):
|
||||
"""One change applied to many profiles by ``bulk_edit_profiles``.
|
||||
|
||||
A key left out keeps that field as it is. For the fields typed
|
||||
``Optional``, an explicit ``None`` (JSON ``null``) clears the field, so
|
||||
the patch is sent exactly as given. An unknown key is refused with 400.
|
||||
"""
|
||||
|
||||
status_id: Optional[str]
|
||||
tags_add: List[str]
|
||||
tags_remove: List[str]
|
||||
note: Optional[str]
|
||||
group_id: Optional[str]
|
||||
extension_group_id: Optional[str]
|
||||
dns_blocklist: Optional[str]
|
||||
dns_server: Optional[str]
|
||||
proxy_bypass_rules: List[str]
|
||||
start_urls: List[str]
|
||||
launch_hook: Optional[str]
|
||||
clear_on_close: bool
|
||||
randomize_fingerprint_on_launch: bool
|
||||
webrtc_mode: Optional[str]
|
||||
block_images: bool
|
||||
block_webgl: bool
|
||||
restore_session: bool
|
||||
new_device: bool
|
||||
|
||||
|
||||
class BulkEditResponse(TypedDict):
|
||||
changed: int
|
||||
|
||||
|
||||
class TemplateSettings(TypedDict, total=False):
|
||||
"""What every profile made from a template starts with.
|
||||
|
||||
A key left out is empty. ``proxy_ids``, when not empty, wins over
|
||||
``proxy_id`` and spreads new profiles across those proxies in turn.
|
||||
"""
|
||||
|
||||
name_prefix: Optional[str]
|
||||
proxy_id: Optional[str]
|
||||
vpn_id: Optional[str]
|
||||
proxy_ids: List[str]
|
||||
group_id: Optional[str]
|
||||
tags: List[str]
|
||||
status_id: Optional[str]
|
||||
note: Optional[str]
|
||||
extension_group_id: Optional[str]
|
||||
dns_blocklist: Optional[str]
|
||||
dns_server: Optional[str]
|
||||
proxy_bypass_rules: List[str]
|
||||
start_urls: List[str]
|
||||
launch_hook: Optional[str]
|
||||
ephemeral: bool
|
||||
clear_on_close: bool
|
||||
wayfern_config: Optional[WayfernConfig]
|
||||
|
||||
|
||||
class ProfileTemplate(TypedDict):
|
||||
id: str
|
||||
name: str
|
||||
settings: TemplateSettings
|
||||
position: int
|
||||
updated_at: int
|
||||
|
||||
|
||||
class _ApiBulkCreateResponseRequired(TypedDict):
|
||||
profiles: List[ApiProfile]
|
||||
|
||||
|
||||
class ApiBulkCreateResponse(_ApiBulkCreateResponseRequired, total=False):
|
||||
"""``error`` is a ``{"code": ...}`` payload when creation stopped early."""
|
||||
|
||||
error: str
|
||||
|
||||
|
||||
class StorageItem(TypedDict):
|
||||
name: str
|
||||
value: str
|
||||
|
||||
|
||||
class _OriginStorageRequired(TypedDict):
|
||||
origin: str
|
||||
|
||||
|
||||
class OriginStorage(_OriginStorageRequired, total=False):
|
||||
# Playwright's own spelling.
|
||||
localStorage: List[StorageItem]
|
||||
|
||||
|
||||
class StorageState(TypedDict, total=False):
|
||||
"""Playwright's ``storageState``: cookies and each origin's Local Storage.
|
||||
|
||||
Each cookie is Playwright's own object (``name``, ``value``, ``domain``,
|
||||
``path``, ``expires``, ``httpOnly``, ``secure``, ``sameSite``), passed
|
||||
through untouched.
|
||||
"""
|
||||
|
||||
cookies: List[Dict[str, Any]]
|
||||
origins: List[OriginStorage]
|
||||
|
||||
|
||||
class StorageStateImport(TypedDict):
|
||||
origins: int
|
||||
items: int
|
||||
cookies_imported: int
|
||||
cookies_replaced: int
|
||||
cookie_errors: List[str]
|
||||
|
||||
|
||||
class _ApiProxyResponseRequired(TypedDict):
|
||||
id: str
|
||||
name: str
|
||||
proxy_settings: ProxySettings
|
||||
|
||||
|
||||
class ApiProxyResponse(_ApiProxyResponseRequired, total=False):
|
||||
rotation_url: str
|
||||
|
||||
|
||||
class _RotationResultRequired(TypedDict):
|
||||
proxy_id: str
|
||||
elapsed_ms: int
|
||||
|
||||
|
||||
class RotationResult(_RotationResultRequired, total=False):
|
||||
"""``status`` is the provider's HTTP status, when it answered."""
|
||||
|
||||
status: int
|
||||
error: str
|
||||
|
||||
|
||||
class _ApiVpnRequired(TypedDict):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
@@ -31,6 +31,15 @@ Case = Tuple[
|
||||
|
||||
LOCATOR = {"role": "button", "name": "Sign in"}
|
||||
|
||||
STORAGE_STATE = {
|
||||
"cookies": [{"name": "sid", "value": "1", "domain": ".example.com", "path": "/"}],
|
||||
"origins": [
|
||||
{"origin": "https://example.com", "localStorage": [{"name": "k", "value": "v"}]}
|
||||
],
|
||||
}
|
||||
|
||||
TEMPLATE_SETTINGS = {"name_prefix": "Shop", "tags": ["eu"], "proxy_ids": ["x1", "x2"]}
|
||||
|
||||
CASES: List[Case] = [
|
||||
# -- profiles ----------------------------------------------------------
|
||||
("list_profiles", (), {}, "GET", "/v1/profiles", None, {}, "/v1/profiles"),
|
||||
@@ -55,6 +64,26 @@ CASES: List[Case] = [
|
||||
{},
|
||||
"/v1/profiles",
|
||||
),
|
||||
(
|
||||
"create_profile",
|
||||
(),
|
||||
{
|
||||
"name": "Started",
|
||||
"browser": "wayfern",
|
||||
"status_id": "st1",
|
||||
"start_urls": ["https://example.com"],
|
||||
},
|
||||
"POST",
|
||||
"/v1/profiles",
|
||||
{
|
||||
"name": "Started",
|
||||
"browser": "wayfern",
|
||||
"status_id": "st1",
|
||||
"start_urls": ["https://example.com"],
|
||||
},
|
||||
{},
|
||||
"/v1/profiles",
|
||||
),
|
||||
(
|
||||
"update_profile",
|
||||
("p1",),
|
||||
@@ -65,6 +94,16 @@ CASES: List[Case] = [
|
||||
{},
|
||||
"/v1/profiles/{id}",
|
||||
),
|
||||
(
|
||||
"update_profile",
|
||||
("p1",),
|
||||
{"status_id": "", "start_urls": []},
|
||||
"PUT",
|
||||
"/v1/profiles/p1",
|
||||
{"status_id": "", "start_urls": []},
|
||||
{},
|
||||
"/v1/profiles/{id}",
|
||||
),
|
||||
("delete_profile", ("p1",), {}, "DELETE", "/v1/profiles/p1", None, {}, "/v1/profiles/{id}"),
|
||||
(
|
||||
"run_profile",
|
||||
@@ -194,6 +233,49 @@ CASES: List[Case] = [
|
||||
{},
|
||||
"/v1/profiles/{id}/cookies/import",
|
||||
),
|
||||
(
|
||||
"export_storage_state",
|
||||
("p1",),
|
||||
{"include_cookies": True},
|
||||
"GET",
|
||||
"/v1/profiles/p1/storage-state",
|
||||
None,
|
||||
{"include_cookies": "true"},
|
||||
"/v1/profiles/{id}/storage-state",
|
||||
),
|
||||
(
|
||||
"export_storage_state",
|
||||
("p1",),
|
||||
{},
|
||||
"GET",
|
||||
"/v1/profiles/p1/storage-state",
|
||||
None,
|
||||
{},
|
||||
"/v1/profiles/{id}/storage-state",
|
||||
),
|
||||
(
|
||||
"import_storage_state",
|
||||
("p1", STORAGE_STATE),
|
||||
{},
|
||||
"POST",
|
||||
"/v1/profiles/p1/storage-state",
|
||||
STORAGE_STATE,
|
||||
{},
|
||||
"/v1/profiles/{id}/storage-state",
|
||||
),
|
||||
(
|
||||
"bulk_edit_profiles",
|
||||
(["p1", "p2"], {"status_id": None, "tags_add": ["eu"], "new_device": True}),
|
||||
{},
|
||||
"POST",
|
||||
"/v1/profiles/bulk-edit",
|
||||
{
|
||||
"profile_ids": ["p1", "p2"],
|
||||
"patch": {"status_id": None, "tags_add": ["eu"], "new_device": True},
|
||||
},
|
||||
{},
|
||||
"/v1/profiles/bulk-edit",
|
||||
),
|
||||
# -- agent -------------------------------------------------------------
|
||||
(
|
||||
"agent_perceive",
|
||||
@@ -440,6 +522,72 @@ CASES: List[Case] = [
|
||||
),
|
||||
("delete_group", ("g1",), {}, "DELETE", "/v1/groups/g1", None, {}, "/v1/groups/{id}"),
|
||||
("list_tags", (), {}, "GET", "/v1/tags", None, {}, "/v1/tags"),
|
||||
# -- statuses ----------------------------------------------------------
|
||||
("list_statuses", (), {}, "GET", "/v1/statuses", None, {}, "/v1/statuses"),
|
||||
(
|
||||
"create_status",
|
||||
(),
|
||||
{"name": "Warming", "color": "#ffaa00"},
|
||||
"POST",
|
||||
"/v1/statuses",
|
||||
{"name": "Warming", "color": "#ffaa00"},
|
||||
{},
|
||||
"/v1/statuses",
|
||||
),
|
||||
(
|
||||
"update_status",
|
||||
("st1",),
|
||||
{"name": "Ready", "color": "#00aa55"},
|
||||
"PUT",
|
||||
"/v1/statuses/st1",
|
||||
{"name": "Ready", "color": "#00aa55"},
|
||||
{},
|
||||
"/v1/statuses/{id}",
|
||||
),
|
||||
("delete_status", ("st1",), {}, "DELETE", "/v1/statuses/st1", None, {}, "/v1/statuses/{id}"),
|
||||
# -- templates ---------------------------------------------------------
|
||||
("list_templates", (), {}, "GET", "/v1/templates", None, {}, "/v1/templates"),
|
||||
(
|
||||
"create_template",
|
||||
(),
|
||||
{"name": "Shop EU", "settings": TEMPLATE_SETTINGS},
|
||||
"POST",
|
||||
"/v1/templates",
|
||||
{"name": "Shop EU", "settings": TEMPLATE_SETTINGS},
|
||||
{},
|
||||
"/v1/templates",
|
||||
),
|
||||
(
|
||||
"create_template",
|
||||
(),
|
||||
{"name": "Empty"},
|
||||
"POST",
|
||||
"/v1/templates",
|
||||
{"name": "Empty"},
|
||||
{},
|
||||
"/v1/templates",
|
||||
),
|
||||
(
|
||||
"update_template",
|
||||
("t1",),
|
||||
{"name": "Shop EU 2", "settings": TEMPLATE_SETTINGS},
|
||||
"PUT",
|
||||
"/v1/templates/t1",
|
||||
{"name": "Shop EU 2", "settings": TEMPLATE_SETTINGS},
|
||||
{},
|
||||
"/v1/templates/{id}",
|
||||
),
|
||||
("delete_template", ("t1",), {}, "DELETE", "/v1/templates/t1", None, {}, "/v1/templates/{id}"),
|
||||
(
|
||||
"create_profiles_from_template",
|
||||
("t1",),
|
||||
{"count": 3, "name": "Shop"},
|
||||
"POST",
|
||||
"/v1/templates/t1/profiles",
|
||||
{"count": 3, "name": "Shop"},
|
||||
{},
|
||||
"/v1/templates/{id}/profiles",
|
||||
),
|
||||
# -- proxies -----------------------------------------------------------
|
||||
("list_proxies", (), {}, "GET", "/v1/proxies", None, {}, "/v1/proxies"),
|
||||
("get_proxy", ("x1",), {}, "GET", "/v1/proxies/x1", None, {}, "/v1/proxies/{id}"),
|
||||
@@ -453,6 +601,24 @@ CASES: List[Case] = [
|
||||
{},
|
||||
"/v1/proxies",
|
||||
),
|
||||
(
|
||||
"create_proxy",
|
||||
(),
|
||||
{
|
||||
"name": "Rotating",
|
||||
"proxy_settings": {"proxy_type": "http", "host": "h", "port": 8080},
|
||||
"rotation_url": "https://provider.example/rotate?key=1",
|
||||
},
|
||||
"POST",
|
||||
"/v1/proxies",
|
||||
{
|
||||
"name": "Rotating",
|
||||
"proxy_settings": {"proxy_type": "http", "host": "h", "port": 8080},
|
||||
"rotation_url": "https://provider.example/rotate?key=1",
|
||||
},
|
||||
{},
|
||||
"/v1/proxies",
|
||||
),
|
||||
(
|
||||
"update_proxy",
|
||||
("x1",),
|
||||
@@ -464,6 +630,26 @@ CASES: List[Case] = [
|
||||
"/v1/proxies/{id}",
|
||||
),
|
||||
("delete_proxy", ("x1",), {}, "DELETE", "/v1/proxies/x1", None, {}, "/v1/proxies/{id}"),
|
||||
(
|
||||
"update_proxy",
|
||||
("x1",),
|
||||
{"rotation_url": ""},
|
||||
"PUT",
|
||||
"/v1/proxies/x1",
|
||||
{"rotation_url": ""},
|
||||
{},
|
||||
"/v1/proxies/{id}",
|
||||
),
|
||||
(
|
||||
"rotate_proxy",
|
||||
("x1",),
|
||||
{},
|
||||
"POST",
|
||||
"/v1/proxies/x1/rotate",
|
||||
None,
|
||||
{},
|
||||
"/v1/proxies/{id}/rotate",
|
||||
),
|
||||
(
|
||||
"import_proxies",
|
||||
(),
|
||||
|
||||
Reference in new issue
Block a user