virtual-android-farm 0.1.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.
- virtual_android_farm/__init__.py +3 -0
- virtual_android_farm/cli.py +89 -0
- virtual_android_farm/deal.py +26 -0
- virtual_android_farm/jobs.py +73 -0
- virtual_android_farm-0.1.2.dist-info/METADATA +116 -0
- virtual_android_farm-0.1.2.dist-info/RECORD +9 -0
- virtual_android_farm-0.1.2.dist-info/WHEEL +4 -0
- virtual_android_farm-0.1.2.dist-info/entry_points.txt +2 -0
- virtual_android_farm-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""CLI for virtual-android-farm."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from virtual_android_farm.jobs import default_queue, validate_queue
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group(invoke_without_command=True)
|
|
14
|
+
@click.version_option(package_name="virtual-android-farm")
|
|
15
|
+
@click.option("--show-deal", is_flag=True, help="Print Multilogin Cloud Phone coupon info.")
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def main(ctx: click.Context, show_deal: bool) -> None:
|
|
18
|
+
"""YAML batch queue for virtual Android cloud phone farms."""
|
|
19
|
+
if show_deal:
|
|
20
|
+
from virtual_android_farm.deal import print_show_deal
|
|
21
|
+
|
|
22
|
+
print_show_deal()
|
|
23
|
+
ctx.exit(0)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@main.command("init")
|
|
27
|
+
@click.option("-d", "--dir", "farm_dir", type=click.Path(path_type=Path), default=".phone-farm")
|
|
28
|
+
def init_cmd(farm_dir: Path) -> None:
|
|
29
|
+
"""Create .phone-farm/jobs.yaml starter queue."""
|
|
30
|
+
farm_dir.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
jobs_path = farm_dir / "jobs.yaml"
|
|
32
|
+
scripts = farm_dir / "scripts"
|
|
33
|
+
scripts.mkdir(exist_ok=True)
|
|
34
|
+
queue = default_queue()
|
|
35
|
+
for job in queue.jobs:
|
|
36
|
+
job.script = f"scripts/{Path(job.script).name}"
|
|
37
|
+
script_path = farm_dir / job.script
|
|
38
|
+
if not script_path.exists():
|
|
39
|
+
script_path.write_text(
|
|
40
|
+
"#!/usr/bin/env python3\n"
|
|
41
|
+
f'"""Cloud phone child job ({job.id}) — set PHONE_ID env."""\n'
|
|
42
|
+
f'print("{job.id}", __import__("os").environ.get("PHONE_ID"))\n',
|
|
43
|
+
encoding="utf-8",
|
|
44
|
+
)
|
|
45
|
+
jobs_path.write_text(yaml.safe_dump(queue.model_dump(), sort_keys=False), encoding="utf-8")
|
|
46
|
+
click.echo(f"Created {jobs_path}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@main.command("validate")
|
|
50
|
+
@click.argument("jobs_path", type=click.Path(exists=True, path_type=Path))
|
|
51
|
+
def validate_cmd(jobs_path: Path) -> None:
|
|
52
|
+
"""Validate phone farm jobs.yaml."""
|
|
53
|
+
result = validate_queue(jobs_path)
|
|
54
|
+
if result.valid:
|
|
55
|
+
click.echo(f"VALID: {result.job_count} job(s) in {jobs_path.name}")
|
|
56
|
+
else:
|
|
57
|
+
for issue in result.issues:
|
|
58
|
+
click.echo(f"INVALID: {issue.path}: {issue.message}", err=True)
|
|
59
|
+
raise SystemExit(1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@main.command("list")
|
|
63
|
+
@click.argument("jobs_path", type=click.Path(exists=True, path_type=Path))
|
|
64
|
+
def list_cmd(jobs_path: Path) -> None:
|
|
65
|
+
"""List jobs in a phone farm queue."""
|
|
66
|
+
from virtual_android_farm.jobs import load_queue
|
|
67
|
+
|
|
68
|
+
queue = load_queue(jobs_path)
|
|
69
|
+
for job in queue.jobs:
|
|
70
|
+
flag = "on" if job.enabled else "off"
|
|
71
|
+
app = job.app or "-"
|
|
72
|
+
click.echo(f"{job.id}\t{flag}\t{app}\t{job.script}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@main.command("dry-run")
|
|
76
|
+
@click.argument("jobs_path", type=click.Path(exists=True, path_type=Path))
|
|
77
|
+
def dry_run_cmd(jobs_path: Path) -> None:
|
|
78
|
+
"""Print enabled jobs without executing (validation only)."""
|
|
79
|
+
result = validate_queue(jobs_path, check_scripts=False)
|
|
80
|
+
if not result.valid:
|
|
81
|
+
for issue in result.issues:
|
|
82
|
+
click.echo(f"INVALID: {issue.path}: {issue.message}", err=True)
|
|
83
|
+
raise SystemExit(1)
|
|
84
|
+
from virtual_android_farm.jobs import load_queue
|
|
85
|
+
|
|
86
|
+
queue = load_queue(jobs_path)
|
|
87
|
+
for job in queue.jobs:
|
|
88
|
+
if job.enabled:
|
|
89
|
+
click.echo(f"would-run\t{job.id}\t{job.script}")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Multilogin Cloud Phone affiliate output for --show-deal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
SHOW_DEAL_TEXT = """Partner info (affiliate links — optional, not required for phone-farm)
|
|
8
|
+
|
|
9
|
+
Scaling 50–150 cloud phones needs ARM farms + folder pools — not USB phone racks.
|
|
10
|
+
Multilogin Cloud Phone scales N Android profiles from one workspace.
|
|
11
|
+
|
|
12
|
+
Multilogin Cloud Phone (verify eligibility before checkout)
|
|
13
|
+
|
|
14
|
+
MIN50 — cloud phone (eligible new purchases)
|
|
15
|
+
SAAS50 — browser plans
|
|
16
|
+
|
|
17
|
+
https://multilogin.com?a_aid=saas
|
|
18
|
+
Get latest codes:
|
|
19
|
+
https://anti-detect.github.io/
|
|
20
|
+
https://saasverdict.com/
|
|
21
|
+
|
|
22
|
+
Disclosure: we may earn a commission. Offers change; confirm on vendor site."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def print_show_deal() -> None:
|
|
26
|
+
click.echo(SHOW_DEAL_TEXT)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""YAML job queue for cloud phone farms."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PhoneJob(BaseModel):
|
|
12
|
+
id: str
|
|
13
|
+
script: str
|
|
14
|
+
enabled: bool = True
|
|
15
|
+
app: str | None = None
|
|
16
|
+
concurrency: int = Field(default=1, ge=1, le=64)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PhoneFarmQueue(BaseModel):
|
|
20
|
+
version: int = 1
|
|
21
|
+
jobs: list[PhoneJob] = Field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ValidationIssue(BaseModel):
|
|
25
|
+
path: str
|
|
26
|
+
message: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class QueueValidationResult(BaseModel):
|
|
30
|
+
valid: bool
|
|
31
|
+
job_count: int
|
|
32
|
+
issues: list[ValidationIssue] = Field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_queue(path: Path | str) -> PhoneFarmQueue:
|
|
36
|
+
payload = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
|
|
37
|
+
if not isinstance(payload, dict):
|
|
38
|
+
raise ValueError("Queue file must be a YAML mapping")
|
|
39
|
+
return PhoneFarmQueue.model_validate(payload)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_queue(path: Path | str, *, check_scripts: bool = True) -> QueueValidationResult:
|
|
43
|
+
issues: list[ValidationIssue] = []
|
|
44
|
+
try:
|
|
45
|
+
queue = load_queue(path)
|
|
46
|
+
except (ValidationError, ValueError, yaml.YAMLError) as exc:
|
|
47
|
+
return QueueValidationResult(
|
|
48
|
+
valid=False,
|
|
49
|
+
job_count=0,
|
|
50
|
+
issues=[ValidationIssue(path="", message=str(exc))],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
seen: set[str] = set()
|
|
54
|
+
for index, job in enumerate(queue.jobs):
|
|
55
|
+
prefix = f"jobs[{index}]"
|
|
56
|
+
if job.id in seen:
|
|
57
|
+
msg = f"duplicate job id: {job.id!r}"
|
|
58
|
+
issues.append(ValidationIssue(path=f"{prefix}.id", message=msg))
|
|
59
|
+
seen.add(job.id)
|
|
60
|
+
if check_scripts and not Path(job.script).expanduser().exists():
|
|
61
|
+
msg = f"script not found: {job.script!r}"
|
|
62
|
+
issues.append(ValidationIssue(path=f"{prefix}.script", message=msg))
|
|
63
|
+
|
|
64
|
+
return QueueValidationResult(valid=not issues, job_count=len(queue.jobs), issues=issues)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def default_queue() -> PhoneFarmQueue:
|
|
68
|
+
return PhoneFarmQueue(
|
|
69
|
+
jobs=[
|
|
70
|
+
PhoneJob(id="tiktok-warmup", script="scripts/warmup.py", app="tiktok"),
|
|
71
|
+
PhoneJob(id="instagram-check", script="scripts/check.py", app="instagram"),
|
|
72
|
+
]
|
|
73
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: virtual-android-farm
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Virtual Android cloud phone farm — YAML batch queue for GeeLark-style phone pools. CLI: phone-farm.
|
|
5
|
+
Project-URL: Homepage, https://pypi.org/project/virtual-android-farm/
|
|
6
|
+
Project-URL: Documentation, https://pypi.org/project/virtual-android-farm/
|
|
7
|
+
Project-URL: Repository, https://pypi.org/project/virtual-android-farm/
|
|
8
|
+
Project-URL: Changelog, https://pypi.org/project/virtual-android-farm/
|
|
9
|
+
Author: virtual-android-farm contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: android-cloud-farm,batch-cloud-phone,cloud-phone-farm,duoplus-alternative,geelark-automation,mobile-cron-jobs,phone-farm-runner,phone-pool-queue,redfinger-alternative,remote-phone-farm,rpa-cloud-phone,synchronizer-phone,virtual-android-farm,vmos-cloud
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: click>=8.1
|
|
26
|
+
Requires-Dist: pydantic>=2.5
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# virtual-android-farm
|
|
34
|
+
|
|
35
|
+
**Virtual Android cloud phone farm** — YAML batch queue for GeeLark-style phone pools, synchronizer, and RPA workflows.
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/virtual-android-farm/)
|
|
38
|
+
[](https://pypi.org/project/virtual-android-farm/)
|
|
39
|
+
[](https://pypi.org/project/virtual-android-farm/)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install virtual-android-farm
|
|
43
|
+
phone-farm init && phone-farm validate .phone-farm/jobs.yaml
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
CLI: **`phone-farm`** · Python **3.10+**
|
|
47
|
+
|
|
48
|
+
> **Coupon hubs:** `MIN50` cloud phone — [anti-detect.github.io](https://anti-detect.github.io/) · [SaaSVerdict](https://saasverdict.com/). Get latest codes. [Affiliate disclosure](docs/AFFILIATE.md).
|
|
49
|
+
|
|
50
|
+
Queue **cloud phone farm** jobs without USB racks — same scaling pattern as [GeeLark phone farms](https://www.geelark.com/product/cloud-phone/) and [MoreLogin ARM cloud phones](https://www.morelogin.com/remote-phone/).
|
|
51
|
+
|
|
52
|
+
## Problem
|
|
53
|
+
|
|
54
|
+
50–150 TikTok/Instagram profiles on physical phones fail on cost, space, and sync. Teams need a validated YAML queue before batch scripts — not another spreadsheet.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install virtual-android-farm
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
phone-farm init
|
|
66
|
+
phone-farm validate .phone-farm/jobs.yaml
|
|
67
|
+
phone-farm dry-run .phone-farm/jobs.yaml
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## When farms stall (playbook)
|
|
71
|
+
|
|
72
|
+
| Symptom | Likely cause | Next step |
|
|
73
|
+
|---------|--------------|-----------|
|
|
74
|
+
| Mass bans same day | Shared emulator signatures | `ad-phone audit` per device YAML |
|
|
75
|
+
| Jobs never run | Bad script paths | `phone-farm validate` from farm dir |
|
|
76
|
+
| Vendor lock-in | GeeLark/MoreLogin export only | `mobile-vendor-bridge parse` → MLX |
|
|
77
|
+
|
|
78
|
+
**Coupon hubs:** [anti-detect.github.io](https://anti-detect.github.io/) · [SaaSVerdict](https://saasverdict.com/)
|
|
79
|
+
|
|
80
|
+
## CLI
|
|
81
|
+
|
|
82
|
+
| Command | Description |
|
|
83
|
+
|---------|-------------|
|
|
84
|
+
| `phone-farm init` | Create `.phone-farm/jobs.yaml` |
|
|
85
|
+
| `phone-farm validate` | Schema + script paths |
|
|
86
|
+
| `phone-farm list` | Tabular job list |
|
|
87
|
+
| `phone-farm dry-run` | Enabled jobs without execute |
|
|
88
|
+
| `phone-farm --show-deal` | Opt-in `MIN50` coupons |
|
|
89
|
+
|
|
90
|
+
## Limitations
|
|
91
|
+
|
|
92
|
+
- Queue planner only — does not call GeeLark, MoreLogin, or MLX APIs.
|
|
93
|
+
- You execute child scripts (ADB/API) yourself.
|
|
94
|
+
|
|
95
|
+
## Production
|
|
96
|
+
|
|
97
|
+
Scale on **[Multilogin Cloud Phone](https://multilogin.com?a_aid=saas)** folders — code **`MIN50`**. [docs/AFFILIATE.md](docs/AFFILIATE.md) · `phone-farm --show-deal`
|
|
98
|
+
|
|
99
|
+
**FAQ:** [docs/FAQ.md](docs/FAQ.md)
|
|
100
|
+
|
|
101
|
+
## Related tools (on PyPI)
|
|
102
|
+
|
|
103
|
+
| Package | CLI | Role |
|
|
104
|
+
|---------|-----|------|
|
|
105
|
+
| [antidetect-phone-kit](https://pypi.org/project/antidetect-phone-kit/) | `ad-phone` | Device lint |
|
|
106
|
+
| [app-warmup-phone](https://pypi.org/project/app-warmup-phone/) | `app-warmup` | App cadence |
|
|
107
|
+
| [cloud-phone-kit](https://pypi.org/project/cloud-phone-kit/) | `cloud-phone` | Launch readiness |
|
|
108
|
+
| [automation-farm-runner](https://pypi.org/project/automation-farm-runner/) | `farm-runner` | Browser mlx-pool |
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
**Partner:** [Multilogin Cloud Phone](https://multilogin.com?a_aid=saas) · `MIN50` · [SaaSVerdict](https://saasverdict.com/)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
virtual_android_farm/__init__.py,sha256=SU_TehiDcgmDdpoX503z828ihV1NWYJaAPIavcPwQ14,73
|
|
2
|
+
virtual_android_farm/cli.py,sha256=J-LOjZEVh2txG2VR7Buz1Ee4DA8rq6qGuARWKZ6t-wA,3183
|
|
3
|
+
virtual_android_farm/deal.py,sha256=gG0AstSSZbNWifE5tyQVGbhX87oQtet8qihMERjusUg,768
|
|
4
|
+
virtual_android_farm/jobs.py,sha256=-9zo-j5dE-nI7-RS3qpKB9TBgsCSIliTgKQ2-iKskX8,2192
|
|
5
|
+
virtual_android_farm-0.1.2.dist-info/METADATA,sha256=_AW3LW_dzOzKj68rmYn0bPWr4uxcGO-6_WDFdBoLdhM,4849
|
|
6
|
+
virtual_android_farm-0.1.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
virtual_android_farm-0.1.2.dist-info/entry_points.txt,sha256=mrQSAA5OiCXS6UdHdCnqREVA7VPdr93-NFXstenM8w0,61
|
|
8
|
+
virtual_android_farm-0.1.2.dist-info/licenses/LICENSE,sha256=otaip6XsfcpK9GxlDpOHPhrg3rRmxWFnTyjenws3X3E,1079
|
|
9
|
+
virtual_android_farm-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cdpbridge contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|