surfsky-cli 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("surfsky-cli")
@@ -0,0 +1,3 @@
1
+ from .main import main
2
+
3
+ main()
File without changes
@@ -0,0 +1,340 @@
1
+ import json
2
+ from collections.abc import Awaitable, Callable
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import click
7
+ from surfsky import AsyncSurfsky
8
+
9
+ from .. import session
10
+ from ..config import Settings
11
+ from ..out import (
12
+ emit,
13
+ fingerprint_options,
14
+ json_arg,
15
+ output_options,
16
+ pass_settings,
17
+ proxy_options,
18
+ run,
19
+ scalar,
20
+ )
21
+
22
+ Factory = Callable[[AsyncSurfsky], Awaitable[Any]]
23
+
24
+
25
+ def call(
26
+ settings: Settings,
27
+ factory: Factory,
28
+ *,
29
+ text: Callable[[Any], str] | None = None,
30
+ **out: Any,
31
+ ) -> None:
32
+ async def go() -> Any:
33
+ async with settings.client() as client:
34
+ return await factory(client)
35
+
36
+ result = run(go())
37
+ emit(result, text=text(result) if text else None, **out)
38
+
39
+
40
+ @click.group()
41
+ def profile() -> None:
42
+ """Save fingerprints, proxies and cookies for reuse across sessions."""
43
+
44
+
45
+ @profile.command("list")
46
+ @click.option(
47
+ "--all", "all_", is_flag=True, help="Every profile, not just the first 100."
48
+ )
49
+ @click.option("--page", type=int, help="Page number, from 0.")
50
+ @click.option(
51
+ "--ordering",
52
+ type=click.Choice(["created", "-created", "active", "-active", "title", "-title"]),
53
+ )
54
+ @output_options
55
+ @pass_settings
56
+ def profile_list(
57
+ settings: Settings, all_: bool, page: int | None, ordering: str | None, **out: Any
58
+ ) -> None:
59
+ """List up to 100 profiles; use --all or --page for more."""
60
+
61
+ async def factory(client: AsyncSurfsky) -> Any:
62
+ if all_:
63
+ return [
64
+ item
65
+ async for item in client.profiles.iter_all(
66
+ ordering=ordering or "created" # ty: ignore[invalid-argument-type]
67
+ )
68
+ ]
69
+ return await client.profiles.list_page(
70
+ page=page,
71
+ page_len=100,
72
+ ordering=ordering, # ty: ignore[invalid-argument-type]
73
+ )
74
+
75
+ call(settings, factory, **out)
76
+
77
+
78
+ @profile.command("get")
79
+ @click.argument("uuid")
80
+ @output_options
81
+ @pass_settings
82
+ def profile_get(settings: Settings, uuid: str, **out: Any) -> None:
83
+ """Show a profile's fingerprint, proxy and saved settings."""
84
+ call(settings, lambda client: client.profiles.get(uuid), **out)
85
+
86
+
87
+ @profile.command("create")
88
+ @click.argument("title")
89
+ @click.option("--description")
90
+ @click.option(
91
+ "--cookies", "cookies_json", help="Initial cookies: a JSON array inline, or @file."
92
+ )
93
+ @fingerprint_options
94
+ @proxy_options
95
+ @output_options
96
+ @pass_settings
97
+ def profile_create(
98
+ settings: Settings,
99
+ title: str,
100
+ description: str | None,
101
+ cookies_json: str | None,
102
+ os_: str | None,
103
+ os_version: str | None,
104
+ arch: str | None,
105
+ proxy: str | None,
106
+ country: str | None,
107
+ region: str | None,
108
+ city: str | None,
109
+ proxy_type: str | None,
110
+ **out: Any,
111
+ ) -> None:
112
+ """Create a profile named TITLE. Needs --os."""
113
+ fingerprint = session.fingerprint_from(os_, os_version, arch)
114
+ if not os_ or fingerprint is None:
115
+ raise click.UsageError("--os is required: win, mac or android")
116
+ proxy_value = session.proxy_from(proxy, country, region, city, proxy_type)
117
+ cookies = json_arg(cookies_json, "--cookies") if cookies_json else None
118
+
119
+ async def factory(client: AsyncSurfsky) -> Any:
120
+ return await client.profiles.create(
121
+ title=title,
122
+ fingerprint=fingerprint,
123
+ description=description,
124
+ proxy=proxy_value,
125
+ cookies=cookies,
126
+ )
127
+
128
+ call(settings, factory, **out)
129
+
130
+
131
+ @profile.command("update")
132
+ @click.argument("uuid")
133
+ @click.option("--title")
134
+ @click.option("--description")
135
+ @proxy_options
136
+ @output_options
137
+ @pass_settings
138
+ def profile_update(
139
+ settings: Settings,
140
+ uuid: str,
141
+ title: str | None,
142
+ description: str | None,
143
+ proxy: str | None,
144
+ country: str | None,
145
+ region: str | None,
146
+ city: str | None,
147
+ proxy_type: str | None,
148
+ **out: Any,
149
+ ) -> None:
150
+ """Change a profile's title, description or proxy."""
151
+ fields: dict[str, Any] = {}
152
+ if title:
153
+ fields["title"] = title
154
+ if description is not None:
155
+ fields["description"] = description
156
+ if proxy or country:
157
+ fields["proxy"] = session.proxy_from(proxy, country, region, city, proxy_type)
158
+ if not fields:
159
+ raise click.UsageError(
160
+ "nothing to update: give --title, --description or a proxy"
161
+ )
162
+ call(settings, lambda client: client.profiles.update(uuid, **fields), **out)
163
+
164
+
165
+ @profile.command("delete")
166
+ @click.argument("uuids", nargs=-1, required=True)
167
+ @output_options
168
+ @pass_settings
169
+ def profile_delete(settings: Settings, uuids: tuple[str, ...], **out: Any) -> None:
170
+ """Delete one or more profiles (running ones are reported, not deleted)."""
171
+
172
+ async def factory(client: AsyncSurfsky) -> Any:
173
+ if len(uuids) == 1:
174
+ return await client.profiles.delete(uuids[0])
175
+ return await client.profiles.delete_many(list(uuids))
176
+
177
+ call(settings, factory, **out)
178
+
179
+
180
+ @profile.group("cookies")
181
+ def profile_cookies() -> None:
182
+ """Export or import a profile's cookies."""
183
+
184
+
185
+ @profile_cookies.command("export")
186
+ @click.argument("uuid")
187
+ @click.option(
188
+ "--format",
189
+ "fmt",
190
+ type=click.Choice(["json", "netscape"]),
191
+ default="json",
192
+ show_default=True,
193
+ )
194
+ @output_options
195
+ @pass_settings
196
+ def cookies_export(settings: Settings, uuid: str, fmt: str, **out: Any) -> None:
197
+ """Print a profile's cookies."""
198
+ call(
199
+ settings,
200
+ lambda client: client.profiles.export_cookies( # ty: ignore[no-matching-overload]
201
+ uuid, export_format=fmt
202
+ ),
203
+ text=scalar,
204
+ **out,
205
+ )
206
+
207
+
208
+ @profile_cookies.command("import")
209
+ @click.argument("uuid")
210
+ @click.argument("cookies_arg")
211
+ @output_options
212
+ @pass_settings
213
+ def cookies_import(settings: Settings, uuid: str, cookies_arg: str, **out: Any) -> None:
214
+ """Import cookies: a JSON array inline, or @file holding JSON or Netscape text."""
215
+ raw = cookies_arg
216
+ if raw.startswith("@"):
217
+ raw = Path(raw[1:]).read_text(encoding="utf-8")
218
+ try:
219
+ cookies: Any = json.loads(raw)
220
+ except ValueError:
221
+ cookies = raw # the Netscape format is one text blob
222
+
223
+ async def factory(client: AsyncSurfsky) -> Any:
224
+ await client.profiles.import_cookies(uuid, cookies)
225
+ return {"imported": uuid}
226
+
227
+ call(settings, factory, **out)
228
+
229
+
230
+ @click.group()
231
+ def proxy() -> None:
232
+ """Surfsky's proxy pools: locations and quota."""
233
+
234
+
235
+ @proxy.command("countries")
236
+ @click.option("--shared", is_flag=True, help="The shared pool instead of premium.")
237
+ @output_options
238
+ @pass_settings
239
+ def proxy_countries(settings: Settings, shared: bool, **out: Any) -> None:
240
+ """Countries with proxies."""
241
+ call(
242
+ settings,
243
+ lambda client: (
244
+ client.proxies.shared_countries() if shared else client.proxies.countries()
245
+ ),
246
+ **out,
247
+ )
248
+
249
+
250
+ @proxy.command("regions")
251
+ @click.argument("country")
252
+ @output_options
253
+ @pass_settings
254
+ def proxy_regions(settings: Settings, country: str, **out: Any) -> None:
255
+ """Regions of COUNTRY (premium)."""
256
+ call(settings, lambda client: client.proxies.regions(country), **out)
257
+
258
+
259
+ @proxy.command("cities")
260
+ @click.argument("country")
261
+ @click.argument("region")
262
+ @output_options
263
+ @pass_settings
264
+ def proxy_cities(settings: Settings, country: str, region: str, **out: Any) -> None:
265
+ """Cities of REGION in COUNTRY (premium)."""
266
+ call(settings, lambda client: client.proxies.cities(country, region), **out)
267
+
268
+
269
+ @proxy.command("quota")
270
+ @click.option("--shared", is_flag=True)
271
+ @output_options
272
+ @pass_settings
273
+ def proxy_quota(settings: Settings, shared: bool, **out: Any) -> None:
274
+ """Show remaining premium proxy traffic; --shared selects the shared pool."""
275
+ call(
276
+ settings,
277
+ lambda client: (
278
+ client.proxies.shared_quota() if shared else client.proxies.quota()
279
+ ),
280
+ **out,
281
+ )
282
+
283
+
284
+ @click.group()
285
+ def extension() -> None:
286
+ """Manage extensions; load one with session start --extension UUID."""
287
+
288
+
289
+ @extension.command("list")
290
+ @output_options
291
+ @pass_settings
292
+ def extension_list(settings: Settings, **out: Any) -> None:
293
+ """Every uploaded extension."""
294
+ call(settings, lambda client: client.extensions.list_all(), **out)
295
+
296
+
297
+ @extension.command("get")
298
+ @click.argument("uuid")
299
+ @output_options
300
+ @pass_settings
301
+ def extension_get(settings: Settings, uuid: str, **out: Any) -> None:
302
+ """Show extension details by UUID."""
303
+ call(settings, lambda client: client.extensions.get(uuid), **out)
304
+
305
+
306
+ @extension.command("upload")
307
+ @click.argument("zip_path", type=click.Path(exists=True, dir_okay=False))
308
+ @click.option("--name", required=True)
309
+ @output_options
310
+ @pass_settings
311
+ def extension_upload(settings: Settings, zip_path: str, name: str, **out: Any) -> None:
312
+ """Upload an extension ZIP (max 100 MB)."""
313
+ call(settings, lambda client: client.extensions.upload(zip_path, name), **out)
314
+
315
+
316
+ @extension.command("rename")
317
+ @click.argument("uuid")
318
+ @click.argument("name")
319
+ @output_options
320
+ @pass_settings
321
+ def extension_rename(settings: Settings, uuid: str, name: str, **out: Any) -> None:
322
+ """Rename an extension."""
323
+ call(settings, lambda client: client.extensions.update(uuid, name=name), **out)
324
+
325
+
326
+ @extension.command("delete")
327
+ @click.argument("uuid")
328
+ @output_options
329
+ @pass_settings
330
+ def extension_delete(settings: Settings, uuid: str, **out: Any) -> None:
331
+ """Delete an extension."""
332
+
333
+ async def factory(client: AsyncSurfsky) -> Any:
334
+ await client.extensions.delete(uuid)
335
+ return {"deleted": uuid}
336
+
337
+ call(settings, factory, **out)
338
+
339
+
340
+ COMMANDS: list[click.Command] = [profile, proxy, extension]