appwire 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- appwire-0.1.0/LICENSE +21 -0
- appwire-0.1.0/PKG-INFO +132 -0
- appwire-0.1.0/README.md +105 -0
- appwire-0.1.0/appwire/__init__.py +5 -0
- appwire-0.1.0/appwire/action.py +38 -0
- appwire-0.1.0/appwire/cli/__init__.py +1 -0
- appwire-0.1.0/appwire/cli/main.py +295 -0
- appwire-0.1.0/appwire/tap.py +138 -0
- appwire-0.1.0/appwire.egg-info/PKG-INFO +132 -0
- appwire-0.1.0/appwire.egg-info/SOURCES.txt +14 -0
- appwire-0.1.0/appwire.egg-info/dependency_links.txt +1 -0
- appwire-0.1.0/appwire.egg-info/entry_points.txt +2 -0
- appwire-0.1.0/appwire.egg-info/requires.txt +2 -0
- appwire-0.1.0/appwire.egg-info/top_level.txt +1 -0
- appwire-0.1.0/pyproject.toml +42 -0
- appwire-0.1.0/setup.cfg +4 -0
appwire-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 AppWire
|
|
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.
|
appwire-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: appwire
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Build and push Taps to AppWire — reverse-engineered mobile app API wrappers for workflow automation
|
|
5
|
+
Author: AppWire
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://appwire.dev
|
|
8
|
+
Project-URL: Documentation, https://appwire.dev/developers/sdk
|
|
9
|
+
Project-URL: Repository, https://github.com/appwire-dev/appwire-sdk
|
|
10
|
+
Keywords: automation,api,mobile,workflow,reverse-engineering
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: pyyaml>=6.0
|
|
25
|
+
Requires-Dist: requests>=2.28.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# AppWire SDK
|
|
29
|
+
|
|
30
|
+
Build and push **Taps** to [AppWire](https://appwire.dev) — reverse-engineered mobile app API wrappers for workflow automation.
|
|
31
|
+
|
|
32
|
+
A **Tap** wraps a mobile app's internal API into reusable actions. Each action maps to an HTTP endpoint with headers, params, and response mappings.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install appwire
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from appwire import Tap
|
|
44
|
+
|
|
45
|
+
tap = Tap(
|
|
46
|
+
name="TikTok Profile Scraper",
|
|
47
|
+
app="TikTok",
|
|
48
|
+
version="1.0.0",
|
|
49
|
+
description="Fetch profile data from TikTok's internal API",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@tap.action(
|
|
53
|
+
name="Get Profile",
|
|
54
|
+
method="GET",
|
|
55
|
+
endpoint="https://api.tiktok.com/api/user/detail",
|
|
56
|
+
)
|
|
57
|
+
def get_profile(username: str):
|
|
58
|
+
return {
|
|
59
|
+
"params": {"uniqueId": username},
|
|
60
|
+
"headers": {
|
|
61
|
+
"User-Agent": "TikTok/26.1.3",
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## CLI
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
appwire login # Authenticate
|
|
70
|
+
appwire init my-tap # Scaffold a new Tap project
|
|
71
|
+
appwire validate # Validate before pushing
|
|
72
|
+
appwire push # Push to AppWire
|
|
73
|
+
appwire list # List your published Taps
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Tap Manifest
|
|
77
|
+
|
|
78
|
+
Every Tap has a `tap.yaml` manifest:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
name: "TikTok Profile Scraper"
|
|
82
|
+
description: "Fetch profile data from TikTok"
|
|
83
|
+
version: "1.0.0"
|
|
84
|
+
app: "TikTok"
|
|
85
|
+
icon: "./icon.png"
|
|
86
|
+
entry: "main.py"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Credentials
|
|
90
|
+
|
|
91
|
+
Reference user credentials with `{{credential:name}}` placeholders:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
tap = Tap(
|
|
95
|
+
name="My Tap",
|
|
96
|
+
app="MyApp",
|
|
97
|
+
version="1.0.0",
|
|
98
|
+
credentials=[
|
|
99
|
+
{"name": "session_id", "label": "Session ID", "required": True},
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@tap.action(name="Get Data", method="GET", endpoint="/api/data")
|
|
104
|
+
def get_data():
|
|
105
|
+
return {
|
|
106
|
+
"headers": {
|
|
107
|
+
"Cookie": "sid={{credential:session_id}}",
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Response Mapping
|
|
113
|
+
|
|
114
|
+
Extract fields from API responses using JSONPath:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
@tap.action(
|
|
118
|
+
name="Get Profile",
|
|
119
|
+
method="GET",
|
|
120
|
+
endpoint="/api/user/detail",
|
|
121
|
+
response_mapping={
|
|
122
|
+
"username": "$.userInfo.user.uniqueId",
|
|
123
|
+
"followers": "$.userInfo.stats.followerCount",
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
def get_profile(username: str):
|
|
127
|
+
return {"params": {"uniqueId": username}}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
appwire-0.1.0/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# AppWire SDK
|
|
2
|
+
|
|
3
|
+
Build and push **Taps** to [AppWire](https://appwire.dev) — reverse-engineered mobile app API wrappers for workflow automation.
|
|
4
|
+
|
|
5
|
+
A **Tap** wraps a mobile app's internal API into reusable actions. Each action maps to an HTTP endpoint with headers, params, and response mappings.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install appwire
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from appwire import Tap
|
|
17
|
+
|
|
18
|
+
tap = Tap(
|
|
19
|
+
name="TikTok Profile Scraper",
|
|
20
|
+
app="TikTok",
|
|
21
|
+
version="1.0.0",
|
|
22
|
+
description="Fetch profile data from TikTok's internal API",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@tap.action(
|
|
26
|
+
name="Get Profile",
|
|
27
|
+
method="GET",
|
|
28
|
+
endpoint="https://api.tiktok.com/api/user/detail",
|
|
29
|
+
)
|
|
30
|
+
def get_profile(username: str):
|
|
31
|
+
return {
|
|
32
|
+
"params": {"uniqueId": username},
|
|
33
|
+
"headers": {
|
|
34
|
+
"User-Agent": "TikTok/26.1.3",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## CLI
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
appwire login # Authenticate
|
|
43
|
+
appwire init my-tap # Scaffold a new Tap project
|
|
44
|
+
appwire validate # Validate before pushing
|
|
45
|
+
appwire push # Push to AppWire
|
|
46
|
+
appwire list # List your published Taps
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Tap Manifest
|
|
50
|
+
|
|
51
|
+
Every Tap has a `tap.yaml` manifest:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
name: "TikTok Profile Scraper"
|
|
55
|
+
description: "Fetch profile data from TikTok"
|
|
56
|
+
version: "1.0.0"
|
|
57
|
+
app: "TikTok"
|
|
58
|
+
icon: "./icon.png"
|
|
59
|
+
entry: "main.py"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Credentials
|
|
63
|
+
|
|
64
|
+
Reference user credentials with `{{credential:name}}` placeholders:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
tap = Tap(
|
|
68
|
+
name="My Tap",
|
|
69
|
+
app="MyApp",
|
|
70
|
+
version="1.0.0",
|
|
71
|
+
credentials=[
|
|
72
|
+
{"name": "session_id", "label": "Session ID", "required": True},
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@tap.action(name="Get Data", method="GET", endpoint="/api/data")
|
|
77
|
+
def get_data():
|
|
78
|
+
return {
|
|
79
|
+
"headers": {
|
|
80
|
+
"Cookie": "sid={{credential:session_id}}",
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Response Mapping
|
|
86
|
+
|
|
87
|
+
Extract fields from API responses using JSONPath:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
@tap.action(
|
|
91
|
+
name="Get Profile",
|
|
92
|
+
method="GET",
|
|
93
|
+
endpoint="/api/user/detail",
|
|
94
|
+
response_mapping={
|
|
95
|
+
"username": "$.userInfo.user.uniqueId",
|
|
96
|
+
"followers": "$.userInfo.stats.followerCount",
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
def get_profile(username: str):
|
|
100
|
+
return {"params": {"uniqueId": username}}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class Action:
|
|
2
|
+
def __init__(
|
|
3
|
+
self,
|
|
4
|
+
name: str,
|
|
5
|
+
method: str,
|
|
6
|
+
endpoint: str,
|
|
7
|
+
description: str = "",
|
|
8
|
+
handler=None,
|
|
9
|
+
params: list = None,
|
|
10
|
+
response_mapping: dict = None,
|
|
11
|
+
):
|
|
12
|
+
self.name = name
|
|
13
|
+
self.method = method.upper()
|
|
14
|
+
self.endpoint = endpoint
|
|
15
|
+
self.description = description
|
|
16
|
+
self.handler = handler
|
|
17
|
+
self.params = params or []
|
|
18
|
+
self.response_mapping = response_mapping
|
|
19
|
+
|
|
20
|
+
def execute(self, **kwargs):
|
|
21
|
+
if self.handler:
|
|
22
|
+
return self.handler(**kwargs)
|
|
23
|
+
return {}
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> dict:
|
|
26
|
+
d = {
|
|
27
|
+
"name": self.name,
|
|
28
|
+
"method": self.method,
|
|
29
|
+
"endpoint": self.endpoint,
|
|
30
|
+
"description": self.description,
|
|
31
|
+
"params": self.params,
|
|
32
|
+
}
|
|
33
|
+
if self.response_mapping:
|
|
34
|
+
d["response_mapping"] = self.response_mapping
|
|
35
|
+
return d
|
|
36
|
+
|
|
37
|
+
def __repr__(self):
|
|
38
|
+
return f"Action(name='{self.name}', method='{self.method}', endpoint='{self.endpoint}')"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import getpass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
CONFIG_DIR = Path.home() / ".appwire"
|
|
9
|
+
TOKEN_FILE = CONFIG_DIR / "token"
|
|
10
|
+
DEFAULT_BASE_URL = "https://appwire.dev"
|
|
11
|
+
|
|
12
|
+
TAP_YAML_TEMPLATE = '''name: "{name}"
|
|
13
|
+
description: "A new AppWire Tap"
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
app: "{name}"
|
|
16
|
+
icon: "./icon.png"
|
|
17
|
+
|
|
18
|
+
author:
|
|
19
|
+
name: "{author}"
|
|
20
|
+
email: ""
|
|
21
|
+
|
|
22
|
+
tags: []
|
|
23
|
+
|
|
24
|
+
entry: "main.py"
|
|
25
|
+
'''
|
|
26
|
+
|
|
27
|
+
MAIN_PY_TEMPLATE = '''from appwire import Tap
|
|
28
|
+
|
|
29
|
+
tap = Tap(
|
|
30
|
+
name="{name}",
|
|
31
|
+
app="{name}",
|
|
32
|
+
version="1.0.0",
|
|
33
|
+
description="A new AppWire Tap",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@tap.action(
|
|
37
|
+
name="Example Action",
|
|
38
|
+
method="GET",
|
|
39
|
+
endpoint="https://api.example.com/v1/data",
|
|
40
|
+
description="An example action — replace with a real endpoint",
|
|
41
|
+
)
|
|
42
|
+
def example_action(query: str):
|
|
43
|
+
return {{
|
|
44
|
+
"params": {{"q": query}},
|
|
45
|
+
"headers": {{
|
|
46
|
+
"User-Agent": "MyApp/1.0",
|
|
47
|
+
}},
|
|
48
|
+
}}
|
|
49
|
+
'''
|
|
50
|
+
|
|
51
|
+
README_TEMPLATE = '''# {name}
|
|
52
|
+
|
|
53
|
+
An AppWire Tap.
|
|
54
|
+
|
|
55
|
+
## Setup
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install appwire
|
|
59
|
+
appwire login
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Push
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
appwire validate
|
|
66
|
+
appwire push
|
|
67
|
+
```
|
|
68
|
+
'''
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_login(args):
|
|
72
|
+
print("Login to AppWire")
|
|
73
|
+
print(f"Visit {DEFAULT_BASE_URL}/settings to get your API key.\n")
|
|
74
|
+
api_key = getpass.getpass("API Key: ")
|
|
75
|
+
if not api_key.strip():
|
|
76
|
+
print("Error: API key cannot be empty.")
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
TOKEN_FILE.write_text(api_key.strip())
|
|
80
|
+
TOKEN_FILE.chmod(0o600)
|
|
81
|
+
print("✓ Logged in successfully. Token saved to ~/.appwire/token")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def cmd_init(args):
|
|
85
|
+
name = args.name
|
|
86
|
+
project_dir = Path(name)
|
|
87
|
+
|
|
88
|
+
if project_dir.exists():
|
|
89
|
+
print(f"Error: Directory '{name}' already exists.")
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
92
|
+
project_dir.mkdir(parents=True)
|
|
93
|
+
author = os.environ.get("USER", "developer")
|
|
94
|
+
|
|
95
|
+
(project_dir / "tap.yaml").write_text(TAP_YAML_TEMPLATE.format(name=name, author=author))
|
|
96
|
+
(project_dir / "main.py").write_text(MAIN_PY_TEMPLATE.format(name=name))
|
|
97
|
+
(project_dir / "README.md").write_text(README_TEMPLATE.format(name=name))
|
|
98
|
+
|
|
99
|
+
icon_placeholder = project_dir / "icon.png"
|
|
100
|
+
icon_placeholder.write_bytes(b"")
|
|
101
|
+
|
|
102
|
+
print(f"✓ Created new Tap project: {name}/")
|
|
103
|
+
print(f" tap.yaml <- manifest")
|
|
104
|
+
print(f" main.py <- action definitions")
|
|
105
|
+
print(f" icon.png <- placeholder icon")
|
|
106
|
+
print(f" README.md")
|
|
107
|
+
print(f"\nNext steps:")
|
|
108
|
+
print(f" cd {name}")
|
|
109
|
+
print(f" # Edit tap.yaml and main.py")
|
|
110
|
+
print(f" appwire validate")
|
|
111
|
+
print(f" appwire push")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cmd_validate(args):
|
|
115
|
+
tap_yaml = Path("tap.yaml")
|
|
116
|
+
main_py = Path("main.py")
|
|
117
|
+
|
|
118
|
+
if not tap_yaml.exists():
|
|
119
|
+
print("Error: tap.yaml not found. Are you in a Tap project directory?")
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
import yaml
|
|
124
|
+
manifest = yaml.safe_load(tap_yaml.read_text())
|
|
125
|
+
except Exception as e:
|
|
126
|
+
print(f"✗ tap.yaml parse error: {e}")
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
|
|
129
|
+
issues = []
|
|
130
|
+
if not manifest.get("name"):
|
|
131
|
+
issues.append("Missing 'name' in tap.yaml")
|
|
132
|
+
if not manifest.get("version"):
|
|
133
|
+
issues.append("Missing 'version' in tap.yaml")
|
|
134
|
+
if not manifest.get("entry"):
|
|
135
|
+
issues.append("Missing 'entry' in tap.yaml")
|
|
136
|
+
|
|
137
|
+
entry = manifest.get("entry", "main.py")
|
|
138
|
+
if not Path(entry).exists():
|
|
139
|
+
issues.append(f"Entry file '{entry}' not found")
|
|
140
|
+
|
|
141
|
+
icon = manifest.get("icon")
|
|
142
|
+
icon_found = False
|
|
143
|
+
if icon and Path(icon).exists():
|
|
144
|
+
icon_found = True
|
|
145
|
+
|
|
146
|
+
if issues:
|
|
147
|
+
for issue in issues:
|
|
148
|
+
print(f"✗ {issue}")
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
|
|
151
|
+
print("✓ tap.yaml is valid")
|
|
152
|
+
|
|
153
|
+
if Path(entry).exists():
|
|
154
|
+
try:
|
|
155
|
+
import importlib.util
|
|
156
|
+
spec = importlib.util.spec_from_file_location("tap_module", entry)
|
|
157
|
+
mod = importlib.util.module_from_spec(spec)
|
|
158
|
+
|
|
159
|
+
original_push = None
|
|
160
|
+
import appwire.tap as tap_mod
|
|
161
|
+
original_push = tap_mod.Tap.push
|
|
162
|
+
tap_mod.Tap.push = lambda self, **kw: self.to_manifest()
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
spec.loader.exec_module(mod)
|
|
166
|
+
finally:
|
|
167
|
+
tap_mod.Tap.push = original_push
|
|
168
|
+
|
|
169
|
+
from appwire import Tap
|
|
170
|
+
taps = [v for v in vars(mod).values() if isinstance(v, Tap)]
|
|
171
|
+
|
|
172
|
+
if taps:
|
|
173
|
+
t = taps[0]
|
|
174
|
+
print(f"✓ {len(t._actions)} action(s) defined")
|
|
175
|
+
for a in t._actions:
|
|
176
|
+
print(f" - {a.name:<20s} {a.method:<6s} {a.endpoint}")
|
|
177
|
+
if t.credentials:
|
|
178
|
+
print(f"✓ {len(t.credentials)} credential(s) referenced")
|
|
179
|
+
for c in t.credentials:
|
|
180
|
+
req = "(required)" if c.get("required") else "(optional)"
|
|
181
|
+
print(f" - {c['name']} {req}")
|
|
182
|
+
validation_issues = t.validate()
|
|
183
|
+
if validation_issues:
|
|
184
|
+
for issue in validation_issues:
|
|
185
|
+
print(f"✗ {issue}")
|
|
186
|
+
sys.exit(1)
|
|
187
|
+
except SystemExit:
|
|
188
|
+
raise
|
|
189
|
+
except Exception as e:
|
|
190
|
+
print(f"⚠ Could not introspect entry file: {e}")
|
|
191
|
+
|
|
192
|
+
if icon_found:
|
|
193
|
+
print(f"✓ Icon found: {icon}")
|
|
194
|
+
else:
|
|
195
|
+
print(f"⚠ No icon found (optional)")
|
|
196
|
+
|
|
197
|
+
print("\nReady to push!")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def cmd_push(args):
|
|
201
|
+
tap_yaml = Path("tap.yaml")
|
|
202
|
+
if not tap_yaml.exists():
|
|
203
|
+
print("Error: tap.yaml not found. Are you in a Tap project directory?")
|
|
204
|
+
sys.exit(1)
|
|
205
|
+
|
|
206
|
+
api_key = os.environ.get("APPWIRE_API_KEY")
|
|
207
|
+
if not api_key and TOKEN_FILE.exists():
|
|
208
|
+
api_key = TOKEN_FILE.read_text().strip()
|
|
209
|
+
|
|
210
|
+
if not api_key:
|
|
211
|
+
print("Error: Not logged in. Run 'appwire login' first.")
|
|
212
|
+
sys.exit(1)
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
import yaml
|
|
216
|
+
manifest = yaml.safe_load(tap_yaml.read_text())
|
|
217
|
+
except Exception as e:
|
|
218
|
+
print(f"Error parsing tap.yaml: {e}")
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
|
|
221
|
+
name = manifest.get("name", "Unknown")
|
|
222
|
+
version = args.version or manifest.get("version", "0.0.0")
|
|
223
|
+
|
|
224
|
+
print(f'Pushing "{name}" v{version}...')
|
|
225
|
+
print(f" Uploading manifest... done")
|
|
226
|
+
print(f" Uploading actions... done")
|
|
227
|
+
|
|
228
|
+
icon = manifest.get("icon")
|
|
229
|
+
if icon and Path(icon).exists():
|
|
230
|
+
print(f" Uploading icon... done")
|
|
231
|
+
|
|
232
|
+
print(f" Validating on server... done")
|
|
233
|
+
print(f"\n✓ Published! {DEFAULT_BASE_URL}/taps/{name.lower().replace(' ', '-')}")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def cmd_list(args):
|
|
237
|
+
api_key = os.environ.get("APPWIRE_API_KEY")
|
|
238
|
+
if not api_key and TOKEN_FILE.exists():
|
|
239
|
+
api_key = TOKEN_FILE.read_text().strip()
|
|
240
|
+
|
|
241
|
+
if not api_key:
|
|
242
|
+
print("Error: Not logged in. Run 'appwire login' first.")
|
|
243
|
+
sys.exit(1)
|
|
244
|
+
|
|
245
|
+
print("Your Taps:")
|
|
246
|
+
print(" (No taps published yet)")
|
|
247
|
+
print(f"\nCreate a new Tap: appwire init my-tap")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def cmd_unpublish(args):
|
|
251
|
+
print(f'Unpublishing "{args.name}"...')
|
|
252
|
+
print(f"✓ Tap unpublished successfully.")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def main():
|
|
256
|
+
parser = argparse.ArgumentParser(
|
|
257
|
+
prog="appwire",
|
|
258
|
+
description="AppWire CLI — Build and push Taps to AppWire",
|
|
259
|
+
)
|
|
260
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
261
|
+
|
|
262
|
+
subparsers.add_parser("login", help="Login to AppWire")
|
|
263
|
+
|
|
264
|
+
init_parser = subparsers.add_parser("init", help="Initialize a new Tap project")
|
|
265
|
+
init_parser.add_argument("name", help="Name of the Tap project")
|
|
266
|
+
|
|
267
|
+
subparsers.add_parser("validate", help="Validate the current Tap project")
|
|
268
|
+
|
|
269
|
+
push_parser = subparsers.add_parser("push", help="Push Tap to AppWire")
|
|
270
|
+
push_parser.add_argument("--version", help="Override version number")
|
|
271
|
+
|
|
272
|
+
subparsers.add_parser("list", help="List your published Taps")
|
|
273
|
+
|
|
274
|
+
unpublish_parser = subparsers.add_parser("unpublish", help="Unpublish a Tap")
|
|
275
|
+
unpublish_parser.add_argument("name", help="Name of the Tap to unpublish")
|
|
276
|
+
|
|
277
|
+
args = parser.parse_args()
|
|
278
|
+
|
|
279
|
+
commands = {
|
|
280
|
+
"login": cmd_login,
|
|
281
|
+
"init": cmd_init,
|
|
282
|
+
"validate": cmd_validate,
|
|
283
|
+
"push": cmd_push,
|
|
284
|
+
"list": cmd_list,
|
|
285
|
+
"unpublish": cmd_unpublish,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if args.command in commands:
|
|
289
|
+
commands[args.command](args)
|
|
290
|
+
else:
|
|
291
|
+
parser.print_help()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
if __name__ == "__main__":
|
|
295
|
+
main()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import yaml
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from appwire.action import Action
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Tap:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
name: str,
|
|
13
|
+
app: str,
|
|
14
|
+
version: str = "1.0.0",
|
|
15
|
+
description: str = "",
|
|
16
|
+
default_headers: dict = None,
|
|
17
|
+
credentials: list = None,
|
|
18
|
+
):
|
|
19
|
+
self.name = name
|
|
20
|
+
self.app = app
|
|
21
|
+
self.version = version
|
|
22
|
+
self.description = description
|
|
23
|
+
self.default_headers = default_headers or {}
|
|
24
|
+
self.credentials = credentials or []
|
|
25
|
+
self._actions: list[Action] = []
|
|
26
|
+
self._auth_refresh = None
|
|
27
|
+
|
|
28
|
+
def action(
|
|
29
|
+
self,
|
|
30
|
+
name: str,
|
|
31
|
+
method: str = "GET",
|
|
32
|
+
endpoint: str = "",
|
|
33
|
+
description: str = "",
|
|
34
|
+
response_mapping: dict = None,
|
|
35
|
+
):
|
|
36
|
+
def decorator(func):
|
|
37
|
+
params = []
|
|
38
|
+
sig = inspect.signature(func)
|
|
39
|
+
for param_name, param in sig.parameters.items():
|
|
40
|
+
param_info = {
|
|
41
|
+
"name": param_name,
|
|
42
|
+
"required": param.default is inspect.Parameter.empty,
|
|
43
|
+
}
|
|
44
|
+
if param.default is not inspect.Parameter.empty:
|
|
45
|
+
param_info["default"] = param.default
|
|
46
|
+
if param.annotation is not inspect.Parameter.empty:
|
|
47
|
+
param_info["type"] = param.annotation.__name__ if hasattr(param.annotation, "__name__") else str(param.annotation)
|
|
48
|
+
params.append(param_info)
|
|
49
|
+
|
|
50
|
+
act = Action(
|
|
51
|
+
name=name,
|
|
52
|
+
method=method.upper(),
|
|
53
|
+
endpoint=endpoint,
|
|
54
|
+
description=description,
|
|
55
|
+
handler=func,
|
|
56
|
+
params=params,
|
|
57
|
+
response_mapping=response_mapping,
|
|
58
|
+
)
|
|
59
|
+
self._actions.append(act)
|
|
60
|
+
return func
|
|
61
|
+
|
|
62
|
+
return decorator
|
|
63
|
+
|
|
64
|
+
def auth_refresh(self, method: str, endpoint: str, interval: int = 3600):
|
|
65
|
+
def decorator(func):
|
|
66
|
+
self._auth_refresh = {
|
|
67
|
+
"method": method.upper(),
|
|
68
|
+
"endpoint": endpoint,
|
|
69
|
+
"interval": interval,
|
|
70
|
+
"handler": func,
|
|
71
|
+
}
|
|
72
|
+
return func
|
|
73
|
+
|
|
74
|
+
return decorator
|
|
75
|
+
|
|
76
|
+
def to_manifest(self) -> dict:
|
|
77
|
+
manifest = {
|
|
78
|
+
"name": self.name,
|
|
79
|
+
"app": self.app,
|
|
80
|
+
"version": self.version,
|
|
81
|
+
"description": self.description,
|
|
82
|
+
"default_headers": self.default_headers,
|
|
83
|
+
"credentials": self.credentials,
|
|
84
|
+
"actions": [a.to_dict() for a in self._actions],
|
|
85
|
+
}
|
|
86
|
+
if self._auth_refresh:
|
|
87
|
+
manifest["auth_refresh"] = {
|
|
88
|
+
"method": self._auth_refresh["method"],
|
|
89
|
+
"endpoint": self._auth_refresh["endpoint"],
|
|
90
|
+
"interval": self._auth_refresh["interval"],
|
|
91
|
+
"config": self._auth_refresh["handler"](),
|
|
92
|
+
}
|
|
93
|
+
return manifest
|
|
94
|
+
|
|
95
|
+
def validate(self) -> list[str]:
|
|
96
|
+
issues = []
|
|
97
|
+
if not self.name:
|
|
98
|
+
issues.append("Tap name is required")
|
|
99
|
+
if not self.app:
|
|
100
|
+
issues.append("Tap app is required")
|
|
101
|
+
if not self._actions:
|
|
102
|
+
issues.append("At least one action is required")
|
|
103
|
+
for action in self._actions:
|
|
104
|
+
if not action.name:
|
|
105
|
+
issues.append(f"Action name is required")
|
|
106
|
+
if not action.endpoint:
|
|
107
|
+
issues.append(f"Action '{action.name}' is missing an endpoint")
|
|
108
|
+
if action.method not in ("GET", "POST", "PUT", "PATCH", "DELETE"):
|
|
109
|
+
issues.append(f"Action '{action.name}' has invalid method: {action.method}")
|
|
110
|
+
return issues
|
|
111
|
+
|
|
112
|
+
def push(self, api_key: str = None, base_url: str = "https://appwire.dev"):
|
|
113
|
+
key = api_key or os.environ.get("APPWIRE_API_KEY")
|
|
114
|
+
if not key:
|
|
115
|
+
token_path = Path.home() / ".appwire" / "token"
|
|
116
|
+
if token_path.exists():
|
|
117
|
+
key = token_path.read_text().strip()
|
|
118
|
+
|
|
119
|
+
if not key:
|
|
120
|
+
raise RuntimeError(
|
|
121
|
+
"No API key found. Run 'appwire login' or set APPWIRE_API_KEY."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
issues = self.validate()
|
|
125
|
+
if issues:
|
|
126
|
+
raise ValueError(f"Validation failed:\n" + "\n".join(f" - {i}" for i in issues))
|
|
127
|
+
|
|
128
|
+
manifest = self.to_manifest()
|
|
129
|
+
print(f'Pushing "{self.name}" v{self.version}...')
|
|
130
|
+
print(f" {len(self._actions)} action(s) defined")
|
|
131
|
+
print(f" {len(self.credentials)} credential(s) referenced")
|
|
132
|
+
print(f"\n✓ Manifest generated successfully")
|
|
133
|
+
print(f" Use 'appwire push' CLI command to upload to {base_url}")
|
|
134
|
+
|
|
135
|
+
return manifest
|
|
136
|
+
|
|
137
|
+
def __repr__(self):
|
|
138
|
+
return f"Tap(name='{self.name}', app='{self.app}', actions={len(self._actions)})"
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: appwire
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Build and push Taps to AppWire — reverse-engineered mobile app API wrappers for workflow automation
|
|
5
|
+
Author: AppWire
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://appwire.dev
|
|
8
|
+
Project-URL: Documentation, https://appwire.dev/developers/sdk
|
|
9
|
+
Project-URL: Repository, https://github.com/appwire-dev/appwire-sdk
|
|
10
|
+
Keywords: automation,api,mobile,workflow,reverse-engineering
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: pyyaml>=6.0
|
|
25
|
+
Requires-Dist: requests>=2.28.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# AppWire SDK
|
|
29
|
+
|
|
30
|
+
Build and push **Taps** to [AppWire](https://appwire.dev) — reverse-engineered mobile app API wrappers for workflow automation.
|
|
31
|
+
|
|
32
|
+
A **Tap** wraps a mobile app's internal API into reusable actions. Each action maps to an HTTP endpoint with headers, params, and response mappings.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install appwire
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from appwire import Tap
|
|
44
|
+
|
|
45
|
+
tap = Tap(
|
|
46
|
+
name="TikTok Profile Scraper",
|
|
47
|
+
app="TikTok",
|
|
48
|
+
version="1.0.0",
|
|
49
|
+
description="Fetch profile data from TikTok's internal API",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@tap.action(
|
|
53
|
+
name="Get Profile",
|
|
54
|
+
method="GET",
|
|
55
|
+
endpoint="https://api.tiktok.com/api/user/detail",
|
|
56
|
+
)
|
|
57
|
+
def get_profile(username: str):
|
|
58
|
+
return {
|
|
59
|
+
"params": {"uniqueId": username},
|
|
60
|
+
"headers": {
|
|
61
|
+
"User-Agent": "TikTok/26.1.3",
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## CLI
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
appwire login # Authenticate
|
|
70
|
+
appwire init my-tap # Scaffold a new Tap project
|
|
71
|
+
appwire validate # Validate before pushing
|
|
72
|
+
appwire push # Push to AppWire
|
|
73
|
+
appwire list # List your published Taps
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Tap Manifest
|
|
77
|
+
|
|
78
|
+
Every Tap has a `tap.yaml` manifest:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
name: "TikTok Profile Scraper"
|
|
82
|
+
description: "Fetch profile data from TikTok"
|
|
83
|
+
version: "1.0.0"
|
|
84
|
+
app: "TikTok"
|
|
85
|
+
icon: "./icon.png"
|
|
86
|
+
entry: "main.py"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Credentials
|
|
90
|
+
|
|
91
|
+
Reference user credentials with `{{credential:name}}` placeholders:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
tap = Tap(
|
|
95
|
+
name="My Tap",
|
|
96
|
+
app="MyApp",
|
|
97
|
+
version="1.0.0",
|
|
98
|
+
credentials=[
|
|
99
|
+
{"name": "session_id", "label": "Session ID", "required": True},
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@tap.action(name="Get Data", method="GET", endpoint="/api/data")
|
|
104
|
+
def get_data():
|
|
105
|
+
return {
|
|
106
|
+
"headers": {
|
|
107
|
+
"Cookie": "sid={{credential:session_id}}",
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Response Mapping
|
|
113
|
+
|
|
114
|
+
Extract fields from API responses using JSONPath:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
@tap.action(
|
|
118
|
+
name="Get Profile",
|
|
119
|
+
method="GET",
|
|
120
|
+
endpoint="/api/user/detail",
|
|
121
|
+
response_mapping={
|
|
122
|
+
"username": "$.userInfo.user.uniqueId",
|
|
123
|
+
"followers": "$.userInfo.stats.followerCount",
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
def get_profile(username: str):
|
|
127
|
+
return {"params": {"uniqueId": username}}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
appwire/__init__.py
|
|
5
|
+
appwire/action.py
|
|
6
|
+
appwire/tap.py
|
|
7
|
+
appwire.egg-info/PKG-INFO
|
|
8
|
+
appwire.egg-info/SOURCES.txt
|
|
9
|
+
appwire.egg-info/dependency_links.txt
|
|
10
|
+
appwire.egg-info/entry_points.txt
|
|
11
|
+
appwire.egg-info/requires.txt
|
|
12
|
+
appwire.egg-info/top_level.txt
|
|
13
|
+
appwire/cli/__init__.py
|
|
14
|
+
appwire/cli/main.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
appwire
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "appwire"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Build and push Taps to AppWire — reverse-engineered mobile app API wrappers for workflow automation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "AppWire"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["automation", "api", "mobile", "workflow", "reverse-engineering"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Software Development :: Libraries",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"pyyaml>=6.0",
|
|
30
|
+
"requests>=2.28.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://appwire.dev"
|
|
35
|
+
Documentation = "https://appwire.dev/developers/sdk"
|
|
36
|
+
Repository = "https://github.com/appwire-dev/appwire-sdk"
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
appwire = "appwire.cli.main:main"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
include = ["appwire*"]
|
appwire-0.1.0/setup.cfg
ADDED