mobile-vendor-bridge 0.1.2__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.
- mobile_vendor_bridge-0.1.2/.gitignore +15 -0
- mobile_vendor_bridge-0.1.2/CHANGELOG.md +21 -0
- mobile_vendor_bridge-0.1.2/LICENSE +21 -0
- mobile_vendor_bridge-0.1.2/PKG-INFO +110 -0
- mobile_vendor_bridge-0.1.2/README.md +78 -0
- mobile_vendor_bridge-0.1.2/docs/AFFILIATE.md +30 -0
- mobile_vendor_bridge-0.1.2/docs/FAQ.md +25 -0
- mobile_vendor_bridge-0.1.2/mobile_vendor_bridge/__init__.py +3 -0
- mobile_vendor_bridge-0.1.2/mobile_vendor_bridge/cli.py +78 -0
- mobile_vendor_bridge-0.1.2/mobile_vendor_bridge/deal.py +25 -0
- mobile_vendor_bridge-0.1.2/mobile_vendor_bridge/normalize.py +23 -0
- mobile_vendor_bridge-0.1.2/mobile_vendor_bridge/parsers.py +102 -0
- mobile_vendor_bridge-0.1.2/mobile_vendor_bridge/schema.py +15 -0
- mobile_vendor_bridge-0.1.2/pyproject.toml +68 -0
- mobile_vendor_bridge-0.1.2/tests/fixtures/duoplus.json +9 -0
- mobile_vendor_bridge-0.1.2/tests/fixtures/geelark.json +8 -0
- mobile_vendor_bridge-0.1.2/tests/fixtures/morelogin.json +8 -0
- mobile_vendor_bridge-0.1.2/tests/test_cli.py +9 -0
- mobile_vendor_bridge-0.1.2/tests/test_fixtures.py +34 -0
- mobile_vendor_bridge-0.1.2/tests/test_normalize.py +12 -0
- mobile_vendor_bridge-0.1.2/tests/test_parsers.py +21 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v0.1.2 — 2026-06-12
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Expanded FAQ, Install sections, examples, fixtures, AUDIT stubs
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## v0.1.1 — 2026-06-12
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Professional README, CLI upgrades, expanded tests
|
|
15
|
+
|
|
16
|
+
## v0.1.0 — 2026-06-12
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Initial release — cloud phone vertical Account D
|
|
21
|
+
|
|
@@ -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.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mobile-vendor-bridge
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: GeeLark & MoreLogin to Multilogin — parse mobile cloud phone exports and gap audit. CLI: mobile-bridge.
|
|
5
|
+
Project-URL: Homepage, https://pypi.org/project/mobile-vendor-bridge/
|
|
6
|
+
Project-URL: Documentation, https://pypi.org/project/mobile-vendor-bridge/
|
|
7
|
+
Project-URL: Repository, https://pypi.org/project/mobile-vendor-bridge/
|
|
8
|
+
Project-URL: Changelog, https://pypi.org/project/mobile-vendor-bridge/
|
|
9
|
+
Author: mobile-vendor-bridge contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: android-profile-export,cloud-phone-migration,competitor-phone-import,duoplus-export,geelark-export,geelark-to-multilogin,gologin-mobile,mobile-gap-audit,mobile-vendor-import,morelogin-migration,phone-export-parser,phone-profile-import,vendor-normalize
|
|
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
|
+
# mobile-vendor-bridge
|
|
34
|
+
|
|
35
|
+
**GeeLark, MoreLogin, GoLogin & DuoPlus to Multilogin** — parse mobile cloud phone exports with migration gap audit.
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/mobile-vendor-bridge/)
|
|
38
|
+
[](https://pypi.org/project/mobile-vendor-bridge/)
|
|
39
|
+
[](https://pypi.org/project/mobile-vendor-bridge/)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install mobile-vendor-bridge
|
|
43
|
+
mobile-bridge detect export.json && mobile-bridge parse export.json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
CLI: **`mobile-bridge`** · Python **3.10+**
|
|
47
|
+
|
|
48
|
+
> **Coupon hubs:** `MIN50` — [anti-detect.github.io](https://anti-detect.github.io/) · [SaaSVerdict](https://saasverdict.com/). [Affiliate disclosure](docs/AFFILIATE.md).
|
|
49
|
+
|
|
50
|
+
Migrate off **[GeeLark](https://www.geelark.com/)**, **[MoreLogin](https://www.morelogin.com/remote-phone)**, GoLogin mobile, or DuoPlus-style exports toward **Multilogin Cloud Phone**.
|
|
51
|
+
|
|
52
|
+
## Problem
|
|
53
|
+
|
|
54
|
+
Vendor JSON does not map 1:1 to MLX cloud phone folders. Teams need neutral schema + explicit gaps before cutover.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install mobile-vendor-bridge
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
mobile-bridge detect geelark-export.json
|
|
66
|
+
mobile-bridge parse geelark-export.json -o neutral.json
|
|
67
|
+
mobile-bridge gaps geelark-export.json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Supported vendors (detect/parse)
|
|
71
|
+
|
|
72
|
+
| Vendor | Notes |
|
|
73
|
+
|--------|-------|
|
|
74
|
+
| GeeLark | `device_id`, antidetect cloud phone |
|
|
75
|
+
| MoreLogin | `arm_device`, remote phone |
|
|
76
|
+
| GoLogin | Browser-first; gap flag for mobile |
|
|
77
|
+
| DuoPlus | `cloud_instance_id` |
|
|
78
|
+
|
|
79
|
+
## Production
|
|
80
|
+
|
|
81
|
+
Target: **[Multilogin Cloud Phone](https://multilogin.com?a_aid=saas)** · **`MIN50`**. [docs/AFFILIATE.md](docs/AFFILIATE.md)
|
|
82
|
+
|
|
83
|
+
## CLI
|
|
84
|
+
|
|
85
|
+
| Command | Description |
|
|
86
|
+
|---------|-------------|
|
|
87
|
+
| `mobile-bridge detect` | Guess vendor format |
|
|
88
|
+
| `mobile-bridge parse` | Neutral JSON |
|
|
89
|
+
| `mobile-bridge gaps` | Gap table stdout |
|
|
90
|
+
| `mobile-bridge --show-deal` | Opt-in coupons |
|
|
91
|
+
|
|
92
|
+
## Limitations
|
|
93
|
+
|
|
94
|
+
- Offline parse — no vendor API calls.
|
|
95
|
+
- MLX push not included (use MLX UI/API separately).
|
|
96
|
+
|
|
97
|
+
## Related tools (on PyPI)
|
|
98
|
+
|
|
99
|
+
| Package | CLI | Role |
|
|
100
|
+
|---------|-----|------|
|
|
101
|
+
| [antidetect-importer](https://pypi.org/project/antidetect-importer/) | `antidetect-import` | Browser vendor import |
|
|
102
|
+
| [antidetect-phone-kit](https://pypi.org/project/antidetect-phone-kit/) | `ad-phone` | Post-migration lint |
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
**Partner:** [Multilogin Cloud Phone](https://multilogin.com?a_aid=saas) · [SaaSVerdict](https://saasverdict.com/)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# mobile-vendor-bridge
|
|
2
|
+
|
|
3
|
+
**GeeLark, MoreLogin, GoLogin & DuoPlus to Multilogin** — parse mobile cloud phone exports with migration gap audit.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/mobile-vendor-bridge/)
|
|
6
|
+
[](https://pypi.org/project/mobile-vendor-bridge/)
|
|
7
|
+
[](https://pypi.org/project/mobile-vendor-bridge/)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install mobile-vendor-bridge
|
|
11
|
+
mobile-bridge detect export.json && mobile-bridge parse export.json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
CLI: **`mobile-bridge`** · Python **3.10+**
|
|
15
|
+
|
|
16
|
+
> **Coupon hubs:** `MIN50` — [anti-detect.github.io](https://anti-detect.github.io/) · [SaaSVerdict](https://saasverdict.com/). [Affiliate disclosure](docs/AFFILIATE.md).
|
|
17
|
+
|
|
18
|
+
Migrate off **[GeeLark](https://www.geelark.com/)**, **[MoreLogin](https://www.morelogin.com/remote-phone)**, GoLogin mobile, or DuoPlus-style exports toward **Multilogin Cloud Phone**.
|
|
19
|
+
|
|
20
|
+
## Problem
|
|
21
|
+
|
|
22
|
+
Vendor JSON does not map 1:1 to MLX cloud phone folders. Teams need neutral schema + explicit gaps before cutover.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install mobile-vendor-bridge
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
mobile-bridge detect geelark-export.json
|
|
34
|
+
mobile-bridge parse geelark-export.json -o neutral.json
|
|
35
|
+
mobile-bridge gaps geelark-export.json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Supported vendors (detect/parse)
|
|
39
|
+
|
|
40
|
+
| Vendor | Notes |
|
|
41
|
+
|--------|-------|
|
|
42
|
+
| GeeLark | `device_id`, antidetect cloud phone |
|
|
43
|
+
| MoreLogin | `arm_device`, remote phone |
|
|
44
|
+
| GoLogin | Browser-first; gap flag for mobile |
|
|
45
|
+
| DuoPlus | `cloud_instance_id` |
|
|
46
|
+
|
|
47
|
+
## Production
|
|
48
|
+
|
|
49
|
+
Target: **[Multilogin Cloud Phone](https://multilogin.com?a_aid=saas)** · **`MIN50`**. [docs/AFFILIATE.md](docs/AFFILIATE.md)
|
|
50
|
+
|
|
51
|
+
## CLI
|
|
52
|
+
|
|
53
|
+
| Command | Description |
|
|
54
|
+
|---------|-------------|
|
|
55
|
+
| `mobile-bridge detect` | Guess vendor format |
|
|
56
|
+
| `mobile-bridge parse` | Neutral JSON |
|
|
57
|
+
| `mobile-bridge gaps` | Gap table stdout |
|
|
58
|
+
| `mobile-bridge --show-deal` | Opt-in coupons |
|
|
59
|
+
|
|
60
|
+
## Limitations
|
|
61
|
+
|
|
62
|
+
- Offline parse — no vendor API calls.
|
|
63
|
+
- MLX push not included (use MLX UI/API separately).
|
|
64
|
+
|
|
65
|
+
## Related tools (on PyPI)
|
|
66
|
+
|
|
67
|
+
| Package | CLI | Role |
|
|
68
|
+
|---------|-----|------|
|
|
69
|
+
| [antidetect-importer](https://pypi.org/project/antidetect-importer/) | `antidetect-import` | Browser vendor import |
|
|
70
|
+
| [antidetect-phone-kit](https://pypi.org/project/antidetect-phone-kit/) | `ad-phone` | Post-migration lint |
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
**Partner:** [Multilogin Cloud Phone](https://multilogin.com?a_aid=saas) · [SaaSVerdict](https://saasverdict.com/)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Affiliate disclosure — Multilogin Cloud Phone
|
|
2
|
+
|
|
3
|
+
Optional integration with **Multilogin Cloud Phone** (ARM Android in the cloud). Links below are **affiliate partner pages**.
|
|
4
|
+
|
|
5
|
+
## When to mention MLX Cloud Phone
|
|
6
|
+
|
|
7
|
+
- App-native TikTok, Instagram, Reddit workflows (not desktop browser alone)
|
|
8
|
+
- Replacing GeeLark/MoreLogin/DuoPlus-style cloud phones at scale
|
|
9
|
+
- Phone farms needing isolated device fingerprints
|
|
10
|
+
|
|
11
|
+
Do **not** push MLX in CLI defaults or error messages.
|
|
12
|
+
|
|
13
|
+
## Coupons
|
|
14
|
+
|
|
15
|
+
| Code | Product |
|
|
16
|
+
|------|---------|
|
|
17
|
+
| `MIN50` | Multilogin Cloud Phone |
|
|
18
|
+
| `SAAS50` | Multilogin X browser |
|
|
19
|
+
|
|
20
|
+
Checkout: [multilogin.com?a_aid=saas](https://multilogin.com?a_aid=saas)
|
|
21
|
+
|
|
22
|
+
Get latest codes: [anti-detect.github.io](https://anti-detect.github.io/) · [SaaSVerdict](https://saasverdict.com/)
|
|
23
|
+
|
|
24
|
+
## What NOT to claim
|
|
25
|
+
|
|
26
|
+
No guaranteed undetectability or ban bypass. Lint/plan tools are advisory.
|
|
27
|
+
|
|
28
|
+
## FTC
|
|
29
|
+
|
|
30
|
+
`--show-deal` is opt-in only.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# FAQ — mobile-vendor-bridge
|
|
2
|
+
|
|
3
|
+
## Which vendors are supported?
|
|
4
|
+
|
|
5
|
+
Auto-detect and parse: **GeeLark**, **MoreLogin**, **GoLogin** (mobile gap flag), **DuoPlus**. Use `mobile-bridge detect` then `parse`.
|
|
6
|
+
|
|
7
|
+
## GeeLark to Multilogin migration?
|
|
8
|
+
|
|
9
|
+
`mobile-bridge parse geelark.json` → neutral JSON + `gaps` list. Create cloud phone profiles in MLX UI; code **`MIN50`** at checkout.
|
|
10
|
+
|
|
11
|
+
## MoreLogin vs Multilogin Cloud Phone?
|
|
12
|
+
|
|
13
|
+
MoreLogin ships browser + ARM cloud phone. MLX Cloud Phone is the affiliate destination for teams standardizing on Multilogin — verify gaps before cutover.
|
|
14
|
+
|
|
15
|
+
## Does this push to MLX API?
|
|
16
|
+
|
|
17
|
+
No — offline parse only. Pair with MLX UI/API separately.
|
|
18
|
+
|
|
19
|
+
## GoLogin mobile exports?
|
|
20
|
+
|
|
21
|
+
Parsed with gap `gologin_mobile_is_browser_first` — GoLogin is browser-first; app-native flows need cloud phone product.
|
|
22
|
+
|
|
23
|
+
## Latest MIN50?
|
|
24
|
+
|
|
25
|
+
`mobile-bridge --show-deal` · [anti-detect.github.io](https://anti-detect.github.io/)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""CLI for mobile-vendor-bridge."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from mobile_vendor_bridge.parsers import detect_format, parse_vendor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group(invoke_without_command=True)
|
|
15
|
+
@click.version_option(package_name="mobile-vendor-bridge")
|
|
16
|
+
@click.option("--show-deal", is_flag=True, help="Print Multilogin Cloud Phone coupon info.")
|
|
17
|
+
@click.pass_context
|
|
18
|
+
def main(ctx: click.Context, show_deal: bool) -> None:
|
|
19
|
+
"""Parse GeeLark/MoreLogin/DuoPlus cloud phone exports toward MLX migration."""
|
|
20
|
+
if show_deal:
|
|
21
|
+
from mobile_vendor_bridge.deal import print_show_deal
|
|
22
|
+
|
|
23
|
+
print_show_deal()
|
|
24
|
+
ctx.exit(0)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@main.command("detect")
|
|
28
|
+
@click.argument("input_path", type=click.Path(exists=True, path_type=Path))
|
|
29
|
+
def detect_cmd(input_path: Path) -> None:
|
|
30
|
+
"""Guess vendor format from export file."""
|
|
31
|
+
text = input_path.read_text(encoding="utf-8")
|
|
32
|
+
data = _load_payload(input_path, text)
|
|
33
|
+
fmt = detect_format(data, text)
|
|
34
|
+
click.echo(json.dumps({"format": fmt, "keys": sorted(data.keys())}, indent=2))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@main.command("parse")
|
|
38
|
+
@click.argument("input_path", type=click.Path(exists=True, path_type=Path))
|
|
39
|
+
@click.option(
|
|
40
|
+
"--format",
|
|
41
|
+
"fmt",
|
|
42
|
+
type=click.Choice(["geelark", "morelogin", "gologin", "duoplus", "auto"]),
|
|
43
|
+
default="auto",
|
|
44
|
+
)
|
|
45
|
+
@click.option("-o", "--output", type=click.Path(path_type=Path), default=None)
|
|
46
|
+
def parse_cmd(input_path: Path, fmt: str, output: Path | None) -> None:
|
|
47
|
+
"""Parse vendor JSON/YAML export to neutral schema."""
|
|
48
|
+
text = input_path.read_text(encoding="utf-8")
|
|
49
|
+
data = _load_payload(input_path, text)
|
|
50
|
+
detected = detect_format(data, text) if fmt == "auto" else fmt # type: ignore[arg-type]
|
|
51
|
+
profile = parse_vendor(data, detected)
|
|
52
|
+
out = profile.model_dump_json(indent=2)
|
|
53
|
+
if output:
|
|
54
|
+
output.write_text(out + "\n", encoding="utf-8")
|
|
55
|
+
click.echo(f"Wrote {output}")
|
|
56
|
+
else:
|
|
57
|
+
click.echo(out)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@main.command("gaps")
|
|
61
|
+
@click.argument("input_path", type=click.Path(exists=True, path_type=Path))
|
|
62
|
+
def gaps_cmd(input_path: Path) -> None:
|
|
63
|
+
"""Print migration gap table for an export."""
|
|
64
|
+
text = input_path.read_text(encoding="utf-8")
|
|
65
|
+
data = _load_payload(input_path, text)
|
|
66
|
+
profile = parse_vendor(data, detect_format(data, text))
|
|
67
|
+
click.echo(f"vendor\t{profile.vendor}")
|
|
68
|
+
click.echo(f"profile\t{profile.profile_name}")
|
|
69
|
+
for gap in profile.gaps:
|
|
70
|
+
click.echo(f"gap\t{gap}")
|
|
71
|
+
if not profile.gaps:
|
|
72
|
+
click.echo("gap\tnone")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _load_payload(path: Path, text: str) -> dict:
|
|
76
|
+
yaml_file = path.suffix in {".yaml", ".yml"}
|
|
77
|
+
payload = (yaml.safe_load(text) or {}) if yaml_file else json.loads(text)
|
|
78
|
+
return payload if isinstance(payload, dict) else {"data": payload}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 mobile-bridge)
|
|
8
|
+
|
|
9
|
+
Migrating from GeeLark or MoreLogin cloud phones into MLX?
|
|
10
|
+
Parse exports offline — MLX Cloud Phone is only needed at push time.
|
|
11
|
+
|
|
12
|
+
Multilogin Cloud Phone (verify eligibility before checkout)
|
|
13
|
+
|
|
14
|
+
MIN50 — cloud phone (eligible new purchases)
|
|
15
|
+
|
|
16
|
+
https://multilogin.com?a_aid=saas
|
|
17
|
+
Get latest codes:
|
|
18
|
+
https://anti-detect.github.io/
|
|
19
|
+
https://saasverdict.com/
|
|
20
|
+
|
|
21
|
+
Disclosure: we may earn a commission. Offers change; confirm on vendor site."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def print_show_deal() -> None:
|
|
25
|
+
click.echo(SHOW_DEAL_TEXT)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Normalize vendor exports — re-exports."""
|
|
2
|
+
|
|
3
|
+
from mobile_vendor_bridge.parsers import (
|
|
4
|
+
VendorFormat,
|
|
5
|
+
detect_format,
|
|
6
|
+
from_duoplus,
|
|
7
|
+
from_geelark,
|
|
8
|
+
from_gologin,
|
|
9
|
+
from_morelogin,
|
|
10
|
+
parse_vendor,
|
|
11
|
+
)
|
|
12
|
+
from mobile_vendor_bridge.schema import NeutralPhoneProfile
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"NeutralPhoneProfile",
|
|
16
|
+
"VendorFormat",
|
|
17
|
+
"detect_format",
|
|
18
|
+
"parse_vendor",
|
|
19
|
+
"from_geelark",
|
|
20
|
+
"from_morelogin",
|
|
21
|
+
"from_gologin",
|
|
22
|
+
"from_duoplus",
|
|
23
|
+
]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Vendor export parsers for cloud phone migration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from mobile_vendor_bridge.schema import NeutralPhoneProfile
|
|
8
|
+
|
|
9
|
+
VendorFormat = Literal["geelark", "morelogin", "gologin", "duoplus", "unknown"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def detect_format(data: dict[str, Any], raw_text: str = "") -> VendorFormat:
|
|
13
|
+
lowered = raw_text.lower()
|
|
14
|
+
if "geelark" in lowered or "device_id" in data:
|
|
15
|
+
return "geelark"
|
|
16
|
+
if "morelogin" in lowered or "arm_device" in data or "phone_name" in data:
|
|
17
|
+
return "morelogin"
|
|
18
|
+
if "gologin" in lowered:
|
|
19
|
+
return "gologin"
|
|
20
|
+
if "duoplus" in lowered or "duo_plus" in data or data.get("vendor") == "duoplus":
|
|
21
|
+
return "duoplus"
|
|
22
|
+
if "installed_apps" in data:
|
|
23
|
+
return "morelogin"
|
|
24
|
+
return "unknown"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_vendor(data: dict[str, Any], fmt: VendorFormat) -> NeutralPhoneProfile:
|
|
28
|
+
parsers = {
|
|
29
|
+
"geelark": from_geelark,
|
|
30
|
+
"morelogin": from_morelogin,
|
|
31
|
+
"gologin": from_gologin,
|
|
32
|
+
"duoplus": from_duoplus,
|
|
33
|
+
}
|
|
34
|
+
if fmt in parsers:
|
|
35
|
+
return parsers[fmt](data)
|
|
36
|
+
return NeutralPhoneProfile(
|
|
37
|
+
vendor="unknown",
|
|
38
|
+
profile_name=str(data.get("name") or "unknown"),
|
|
39
|
+
gaps=["unknown_vendor_format"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def from_geelark(data: dict[str, Any]) -> NeutralPhoneProfile:
|
|
44
|
+
gaps: list[str] = []
|
|
45
|
+
if not data.get("proxy"):
|
|
46
|
+
gaps.append("proxy_missing")
|
|
47
|
+
if not data.get("device_id"):
|
|
48
|
+
gaps.append("device_id_not_mapped")
|
|
49
|
+
return NeutralPhoneProfile(
|
|
50
|
+
vendor="geelark",
|
|
51
|
+
profile_name=str(data.get("name") or data.get("profile_name") or "unknown"),
|
|
52
|
+
android_version=data.get("android_version"),
|
|
53
|
+
country=data.get("country") or data.get("region"),
|
|
54
|
+
proxy=data.get("proxy"),
|
|
55
|
+
apps=list(data.get("apps") or []),
|
|
56
|
+
gaps=gaps,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def from_morelogin(data: dict[str, Any]) -> NeutralPhoneProfile:
|
|
61
|
+
gaps: list[str] = []
|
|
62
|
+
if not data.get("arm_device"):
|
|
63
|
+
gaps.append("verify_arm_cloud_not_emulator")
|
|
64
|
+
return NeutralPhoneProfile(
|
|
65
|
+
vendor="morelogin",
|
|
66
|
+
profile_name=str(data.get("phone_name") or data.get("name") or "unknown"),
|
|
67
|
+
android_version=data.get("os_version"),
|
|
68
|
+
country=data.get("location"),
|
|
69
|
+
proxy=data.get("proxy_url"),
|
|
70
|
+
apps=list(data.get("installed_apps") or []),
|
|
71
|
+
gaps=gaps,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def from_gologin(data: dict[str, Any]) -> NeutralPhoneProfile:
|
|
76
|
+
gaps: list[str] = ["gologin_mobile_is_browser_first"]
|
|
77
|
+
if not data.get("proxy"):
|
|
78
|
+
gaps.append("proxy_missing")
|
|
79
|
+
return NeutralPhoneProfile(
|
|
80
|
+
vendor="gologin",
|
|
81
|
+
profile_name=str(data.get("name") or data.get("profile_name") or "unknown"),
|
|
82
|
+
android_version=str(data.get("androidVersion") or data.get("os") or "") or None,
|
|
83
|
+
country=data.get("timezone") or data.get("country"),
|
|
84
|
+
proxy=data.get("proxy"),
|
|
85
|
+
apps=list(data.get("apps") or []),
|
|
86
|
+
gaps=gaps,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def from_duoplus(data: dict[str, Any]) -> NeutralPhoneProfile:
|
|
91
|
+
gaps: list[str] = []
|
|
92
|
+
if not data.get("cloud_instance_id"):
|
|
93
|
+
gaps.append("instance_id_not_mapped")
|
|
94
|
+
return NeutralPhoneProfile(
|
|
95
|
+
vendor="duoplus",
|
|
96
|
+
profile_name=str(data.get("profile_name") or data.get("name") or "unknown"),
|
|
97
|
+
android_version=data.get("android"),
|
|
98
|
+
country=data.get("region"),
|
|
99
|
+
proxy=data.get("proxy"),
|
|
100
|
+
apps=list(data.get("apps") or []),
|
|
101
|
+
gaps=gaps,
|
|
102
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Neutral cloud phone profile schema."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NeutralPhoneProfile(BaseModel):
|
|
9
|
+
vendor: str
|
|
10
|
+
profile_name: str
|
|
11
|
+
android_version: str | None = None
|
|
12
|
+
country: str | None = None
|
|
13
|
+
proxy: str | None = None
|
|
14
|
+
apps: list[str] = Field(default_factory=list)
|
|
15
|
+
gaps: list[str] = Field(default_factory=list)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mobile-vendor-bridge"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "GeeLark & MoreLogin to Multilogin — parse mobile cloud phone exports and gap audit. CLI: mobile-bridge."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "mobile-vendor-bridge contributors" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"mobile-vendor-import",
|
|
15
|
+
"geelark-export",
|
|
16
|
+
"morelogin-migration",
|
|
17
|
+
"gologin-mobile",
|
|
18
|
+
"cloud-phone-migration",
|
|
19
|
+
"competitor-phone-import",
|
|
20
|
+
"phone-profile-import",
|
|
21
|
+
"android-profile-export",
|
|
22
|
+
"vendor-normalize",
|
|
23
|
+
"mobile-gap-audit",
|
|
24
|
+
"geelark-to-multilogin",
|
|
25
|
+
"phone-export-parser",
|
|
26
|
+
"duoplus-export",
|
|
27
|
+
]
|
|
28
|
+
classifiers = [
|
|
29
|
+
"Development Status :: 4 - Beta",
|
|
30
|
+
"Environment :: Console",
|
|
31
|
+
"Intended Audience :: Developers",
|
|
32
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
33
|
+
"License :: OSI Approved :: MIT License",
|
|
34
|
+
"Programming Language :: Python :: 3",
|
|
35
|
+
"Programming Language :: Python :: 3.10",
|
|
36
|
+
"Programming Language :: Python :: 3.11",
|
|
37
|
+
"Programming Language :: Python :: 3.12",
|
|
38
|
+
"Programming Language :: Python :: 3.13",
|
|
39
|
+
"Typing :: Typed",
|
|
40
|
+
]
|
|
41
|
+
dependencies = [
|
|
42
|
+
"click>=8.1",
|
|
43
|
+
"pydantic>=2.5",
|
|
44
|
+
"pyyaml>=6.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.optional-dependencies]
|
|
48
|
+
dev = ["pytest>=8.0", "ruff>=0.8"]
|
|
49
|
+
|
|
50
|
+
[project.scripts]
|
|
51
|
+
mobile-bridge = "mobile_vendor_bridge.cli:main"
|
|
52
|
+
|
|
53
|
+
[project.urls]
|
|
54
|
+
Homepage = "https://pypi.org/project/mobile-vendor-bridge/"
|
|
55
|
+
Documentation = "https://pypi.org/project/mobile-vendor-bridge/"
|
|
56
|
+
Repository = "https://pypi.org/project/mobile-vendor-bridge/"
|
|
57
|
+
Changelog = "https://pypi.org/project/mobile-vendor-bridge/"
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
addopts = "-q"
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
target-version = "py310"
|
|
65
|
+
line-length = 100
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint]
|
|
68
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from mobile_vendor_bridge.parsers import detect_format, parse_vendor
|
|
5
|
+
|
|
6
|
+
FIXTURES = Path(__file__).parent / "fixtures"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _load(name: str) -> tuple[dict, str]:
|
|
10
|
+
path = FIXTURES / name
|
|
11
|
+
text = path.read_text(encoding="utf-8")
|
|
12
|
+
return json.loads(text), text
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_geelark_fixture() -> None:
|
|
16
|
+
data, text = _load("geelark.json")
|
|
17
|
+
assert detect_format(data, text) == "geelark"
|
|
18
|
+
profile = parse_vendor(data, "geelark")
|
|
19
|
+
assert profile.gaps == []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_morelogin_fixture() -> None:
|
|
23
|
+
data, text = _load("morelogin.json")
|
|
24
|
+
assert detect_format(data, text) == "morelogin"
|
|
25
|
+
profile = parse_vendor(data, "morelogin")
|
|
26
|
+
assert profile.gaps == []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_duoplus_fixture() -> None:
|
|
30
|
+
data, text = _load("duoplus.json")
|
|
31
|
+
assert detect_format(data, text) == "duoplus"
|
|
32
|
+
profile = parse_vendor(data, "duoplus")
|
|
33
|
+
assert profile.vendor == "duoplus"
|
|
34
|
+
assert profile.gaps == []
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from mobile_vendor_bridge.normalize import from_geelark, from_morelogin
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_geelark_gap() -> None:
|
|
5
|
+
p = from_geelark({"name": "g1"})
|
|
6
|
+
assert p.vendor == "geelark"
|
|
7
|
+
assert "proxy_missing" in p.gaps
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_morelogin() -> None:
|
|
11
|
+
p = from_morelogin({"phone_name": "m1", "arm_device": True})
|
|
12
|
+
assert p.vendor == "morelogin"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from mobile_vendor_bridge.parsers import detect_format, from_duoplus, from_gologin, parse_vendor
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_detect_geelark() -> None:
|
|
5
|
+
assert detect_format({"device_id": "x"}, "") == "geelark"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_gologin_gap() -> None:
|
|
9
|
+
p = from_gologin({"name": "g1"})
|
|
10
|
+
assert "gologin_mobile_is_browser_first" in p.gaps
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_duoplus() -> None:
|
|
14
|
+
p = from_duoplus({"name": "d1", "cloud_instance_id": "i1"})
|
|
15
|
+
assert p.vendor == "duoplus"
|
|
16
|
+
assert p.gaps == []
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_parse_unknown() -> None:
|
|
20
|
+
p = parse_vendor({"name": "x"}, "unknown")
|
|
21
|
+
assert "unknown_vendor_format" in p.gaps
|