anvpy 0.1.0__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.
- anvpy/__init__.py +1 -0
- anvpy/connection.py +135 -0
- anvpy/logger.py +27 -0
- anvpy/main.py +47 -0
- anvpy/run.py +42 -0
- anvpy/sync.py +228 -0
- anvpy/utils.py +15 -0
- anvpy-0.1.0.dist-info/METADATA +154 -0
- anvpy-0.1.0.dist-info/RECORD +13 -0
- anvpy-0.1.0.dist-info/WHEEL +5 -0
- anvpy-0.1.0.dist-info/entry_points.txt +2 -0
- anvpy-0.1.0.dist-info/licenses/LICENSE +9 -0
- anvpy-0.1.0.dist-info/top_level.txt +1 -0
anvpy/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
anvpy/connection.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
import socket
|
|
4
|
+
import threading
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from anvpy.logger import *
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
CONFIG_DIR = os.path.join(
|
|
11
|
+
os.path.expanduser("~"),
|
|
12
|
+
".anvpy"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
CONFIG_FILE = os.path.join(
|
|
16
|
+
CONFIG_DIR,
|
|
17
|
+
"connection.json"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def get_ip():
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
with open(CONFIG_FILE) as f:
|
|
24
|
+
data = json.load(f)
|
|
25
|
+
|
|
26
|
+
return data.get("ip")
|
|
27
|
+
|
|
28
|
+
except:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
def save_ip(ip):
|
|
32
|
+
|
|
33
|
+
os.makedirs(
|
|
34
|
+
CONFIG_DIR,
|
|
35
|
+
exist_ok=True
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
with open(CONFIG_FILE, "w") as f:
|
|
39
|
+
json.dump({
|
|
40
|
+
"ip": ip
|
|
41
|
+
}, f)
|
|
42
|
+
|
|
43
|
+
def is_phone(ip):
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
r = requests.get(
|
|
47
|
+
f"http://{ip}:5000/ping",
|
|
48
|
+
timeout=1
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
data = r.json()
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
data.get("status") == "ok" and
|
|
55
|
+
data.get("service") == "anvpy"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
except:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
def scan_network():
|
|
62
|
+
|
|
63
|
+
local_ip = socket.gethostbyname(
|
|
64
|
+
socket.gethostname()
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
subnet = ".".join(
|
|
68
|
+
local_ip.split(".")[:-1]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
found_ip = None
|
|
72
|
+
|
|
73
|
+
def check(ip):
|
|
74
|
+
nonlocal found_ip
|
|
75
|
+
|
|
76
|
+
if found_ip:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
if is_phone(ip):
|
|
80
|
+
found_ip = ip
|
|
81
|
+
|
|
82
|
+
threads = []
|
|
83
|
+
|
|
84
|
+
for i in range(1, 255):
|
|
85
|
+
|
|
86
|
+
ip = f"{subnet}.{i}"
|
|
87
|
+
|
|
88
|
+
t = threading.Thread(
|
|
89
|
+
target=check,
|
|
90
|
+
args=(ip,)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
t.start()
|
|
94
|
+
threads.append(t)
|
|
95
|
+
|
|
96
|
+
for t in threads:
|
|
97
|
+
t.join()
|
|
98
|
+
|
|
99
|
+
return found_ip
|
|
100
|
+
|
|
101
|
+
def connect(ip=None, verbose=True):
|
|
102
|
+
|
|
103
|
+
if ip is None:
|
|
104
|
+
ip = get_ip()
|
|
105
|
+
|
|
106
|
+
if ip and is_phone(ip):
|
|
107
|
+
|
|
108
|
+
save_ip(ip)
|
|
109
|
+
|
|
110
|
+
if verbose:
|
|
111
|
+
log_ok(f"Connected to {ip}")
|
|
112
|
+
|
|
113
|
+
return ip
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if ip:
|
|
117
|
+
log_warn(
|
|
118
|
+
f"Saved IP {ip} unavailable"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
log_action("Searching for device...")
|
|
122
|
+
|
|
123
|
+
found_ip = scan_network()
|
|
124
|
+
|
|
125
|
+
if found_ip:
|
|
126
|
+
|
|
127
|
+
save_ip(found_ip)
|
|
128
|
+
|
|
129
|
+
log_ok(f"Connected to {found_ip}")
|
|
130
|
+
|
|
131
|
+
return found_ip
|
|
132
|
+
|
|
133
|
+
log_error("No device found")
|
|
134
|
+
|
|
135
|
+
return None
|
anvpy/logger.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class Color:
|
|
2
|
+
RESET = "\033[0m"
|
|
3
|
+
BLUE = "\033[94m"
|
|
4
|
+
GREEN = "\033[38;5;46m"
|
|
5
|
+
YELLOW = "\033[93m"
|
|
6
|
+
RED = "\033[91m"
|
|
7
|
+
CYAN = "\033[96m"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def log_info(msg):
|
|
11
|
+
print(f"{Color.BLUE}[INFO]{Color.RESET} {msg}")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def log_ok(msg):
|
|
15
|
+
print(f"{Color.GREEN}[OK]{Color.RESET} {msg}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def log_warn(msg):
|
|
19
|
+
print(f"{Color.YELLOW}[WARN]{Color.RESET} {msg}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def log_error(msg):
|
|
23
|
+
print(f"{Color.RED}[ERROR]{Color.RESET} {msg}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def log_action(msg):
|
|
27
|
+
print(f"{Color.CYAN}[ACTION]{Color.RESET} {msg}")
|
anvpy/main.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
from anvpy.sync import sync
|
|
4
|
+
from anvpy.logger import *
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
|
|
9
|
+
parser = argparse.ArgumentParser()
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"command",
|
|
12
|
+
choices=["run"]
|
|
13
|
+
)
|
|
14
|
+
parser.add_argument("--path")
|
|
15
|
+
|
|
16
|
+
args = parser.parse_args()
|
|
17
|
+
|
|
18
|
+
if args.path:
|
|
19
|
+
project_path = args.path
|
|
20
|
+
else:
|
|
21
|
+
project_path = os.getcwd()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if args.command == "run":
|
|
25
|
+
|
|
26
|
+
if not os.path.isdir(project_path):
|
|
27
|
+
|
|
28
|
+
log_error(
|
|
29
|
+
f'Project folder not found: "{project_path}"'
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
main_file = os.path.join(
|
|
35
|
+
project_path,
|
|
36
|
+
"main.py"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if not os.path.isfile(main_file):
|
|
40
|
+
|
|
41
|
+
log_error(
|
|
42
|
+
"main.py not found"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
sync(project_path)
|
anvpy/run.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from anvpy.logger import *
|
|
3
|
+
|
|
4
|
+
def run_project(ip, project):
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
|
|
8
|
+
log_action(
|
|
9
|
+
f"Running project: {project}"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
response = requests.post(
|
|
13
|
+
f"http://{ip}:5000/run",
|
|
14
|
+
json={
|
|
15
|
+
"project": project
|
|
16
|
+
},
|
|
17
|
+
timeout=10
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
data = response.json()
|
|
21
|
+
|
|
22
|
+
if response.status_code != 200:
|
|
23
|
+
|
|
24
|
+
log_error(
|
|
25
|
+
f"Failed to run project: {project}"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
log_ok(
|
|
31
|
+
f"Project started: {project}"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return data
|
|
35
|
+
|
|
36
|
+
except Exception:
|
|
37
|
+
|
|
38
|
+
log_error(
|
|
39
|
+
f"Failed to contact device"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return None
|
anvpy/sync.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from anvpy.logger import *
|
|
5
|
+
from anvpy.utils import *
|
|
6
|
+
from anvpy.connection import *
|
|
7
|
+
from anvpy.run import *
|
|
8
|
+
|
|
9
|
+
def compute_diff(local_state, phone_state):
|
|
10
|
+
|
|
11
|
+
to_upload = []
|
|
12
|
+
to_delete = []
|
|
13
|
+
|
|
14
|
+
for path, hash_value in local_state.items():
|
|
15
|
+
|
|
16
|
+
if path not in phone_state:
|
|
17
|
+
to_upload.append(path)
|
|
18
|
+
|
|
19
|
+
elif phone_state[path] != hash_value:
|
|
20
|
+
to_upload.append(path)
|
|
21
|
+
|
|
22
|
+
for path in phone_state:
|
|
23
|
+
|
|
24
|
+
if path not in local_state:
|
|
25
|
+
to_delete.append(path)
|
|
26
|
+
|
|
27
|
+
return to_upload, to_delete
|
|
28
|
+
|
|
29
|
+
def delete_files(ip, project, files):
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
|
|
33
|
+
response = requests.post(
|
|
34
|
+
f"http://{ip}:5000/delete",
|
|
35
|
+
json={
|
|
36
|
+
"project": project,
|
|
37
|
+
"files": files
|
|
38
|
+
},
|
|
39
|
+
timeout=10
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if response.status_code != 200:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
except Exception:
|
|
48
|
+
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def get_phone_state(ip, project):
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
response = requests.get(
|
|
55
|
+
f"http://{ip}:5000/file_state",
|
|
56
|
+
params={
|
|
57
|
+
"project": project
|
|
58
|
+
},
|
|
59
|
+
timeout=5
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if response.status_code != 200:
|
|
63
|
+
|
|
64
|
+
log_error("Failed to get phone state")
|
|
65
|
+
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
return response.json()
|
|
69
|
+
|
|
70
|
+
except Exception as e:
|
|
71
|
+
|
|
72
|
+
log_error("Failed to get phone state")
|
|
73
|
+
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def upload_file(ip, project, folder, rel_path):
|
|
77
|
+
|
|
78
|
+
full_path = os.path.join(
|
|
79
|
+
folder,
|
|
80
|
+
rel_path
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
|
|
85
|
+
with open(full_path, "rb") as f:
|
|
86
|
+
|
|
87
|
+
files = {
|
|
88
|
+
"file": f
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
data = {
|
|
92
|
+
"project": project,
|
|
93
|
+
"path": rel_path
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
response = requests.post(
|
|
97
|
+
f"http://{ip}:5000/upload",
|
|
98
|
+
files=files,
|
|
99
|
+
data=data,
|
|
100
|
+
timeout=30
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
data = response.json()
|
|
104
|
+
|
|
105
|
+
if response.status_code != 200:
|
|
106
|
+
|
|
107
|
+
log_error(
|
|
108
|
+
f"Upload failed: {rel_path}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
return data
|
|
114
|
+
|
|
115
|
+
except Exception:
|
|
116
|
+
|
|
117
|
+
log_error(
|
|
118
|
+
f"Failed: {rel_path}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
def scan_files(folder):
|
|
124
|
+
|
|
125
|
+
files = {}
|
|
126
|
+
|
|
127
|
+
for root, dirs, filenames in os.walk(folder):
|
|
128
|
+
|
|
129
|
+
for filename in filenames:
|
|
130
|
+
|
|
131
|
+
full_path = os.path.join(root, filename)
|
|
132
|
+
|
|
133
|
+
rel_path = os.path.relpath(
|
|
134
|
+
full_path,
|
|
135
|
+
folder
|
|
136
|
+
).replace("\\", "/")
|
|
137
|
+
|
|
138
|
+
files[rel_path] = get_file_hash(
|
|
139
|
+
full_path
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return files
|
|
143
|
+
|
|
144
|
+
def sync(folder, run_after=True):
|
|
145
|
+
|
|
146
|
+
ip = connect(verbose=False)
|
|
147
|
+
|
|
148
|
+
if not ip:
|
|
149
|
+
log_error("Phone not found")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
project = os.path.basename(
|
|
153
|
+
os.path.abspath(folder)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
log_action(
|
|
157
|
+
f"Syncing project: {project}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
local_state = scan_files(folder)
|
|
161
|
+
|
|
162
|
+
phone_state = get_phone_state(
|
|
163
|
+
ip,
|
|
164
|
+
project
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if phone_state is None:
|
|
168
|
+
log_error("Connection lost")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
to_upload, to_delete = compute_diff(
|
|
172
|
+
local_state,
|
|
173
|
+
phone_state
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if to_upload:
|
|
177
|
+
|
|
178
|
+
log_action(
|
|
179
|
+
f"Uploading {len(to_upload)} files"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if to_delete:
|
|
183
|
+
|
|
184
|
+
log_action(
|
|
185
|
+
f"Deleting {len(to_delete)} files"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
for file in to_upload:
|
|
189
|
+
|
|
190
|
+
result = upload_file(
|
|
191
|
+
ip,
|
|
192
|
+
project,
|
|
193
|
+
folder,
|
|
194
|
+
file
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if result:
|
|
198
|
+
log_ok(f"Uploaded: {file}")
|
|
199
|
+
|
|
200
|
+
if to_delete:
|
|
201
|
+
|
|
202
|
+
result = delete_files(
|
|
203
|
+
ip,
|
|
204
|
+
project,
|
|
205
|
+
to_delete
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if result:
|
|
209
|
+
|
|
210
|
+
for file in to_delete:
|
|
211
|
+
log_ok(f"Deleted: {file}")
|
|
212
|
+
|
|
213
|
+
else:
|
|
214
|
+
|
|
215
|
+
log_error("Delete failed")
|
|
216
|
+
|
|
217
|
+
log_ok(
|
|
218
|
+
f"Sync complete "
|
|
219
|
+
f"(Uploaded: {len(to_upload)}, "
|
|
220
|
+
f"Deleted: {len(to_delete)})"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if run_after:
|
|
224
|
+
|
|
225
|
+
run_project(
|
|
226
|
+
ip,
|
|
227
|
+
project
|
|
228
|
+
)
|
anvpy/utils.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: anvpy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official CLI for AnvPy to sync and run Python projects on Android.
|
|
5
|
+
Author-email: Amar <techanvpy@gmail.com>, Ajay <ajay@vissora.cloud>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/anvlabs/anvpy-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/anvlabs/anvpy-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/anvlabs/anvpy-cli/issues
|
|
10
|
+
Keywords: python,android,cli,development,sync
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
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: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Software Development
|
|
24
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: requests
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# AnvPy CLI
|
|
33
|
+
|
|
34
|
+
**AnvPy CLI** is the official command-line interface for **AnvPy (Android Versatile Python)**, a platform that enables developers to build and run Python applications on Android.
|
|
35
|
+
|
|
36
|
+
The CLI provides a seamless desktop development workflow by synchronizing your project from your computer to your Android device and launching it directly in the AnvPy application with a single command.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
* One command project synchronization and execution
|
|
43
|
+
* Automatic device discovery on the local network
|
|
44
|
+
* Incremental synchronization using file hashing
|
|
45
|
+
* Simple setup with automatic connection management
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
* Python 3.8 or later
|
|
52
|
+
* AnvPy installed on an Android device
|
|
53
|
+
* Computer and Android device connected to the same local network (same Wi-Fi network or mobile hotspot)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
# Installation
|
|
58
|
+
|
|
59
|
+
Install directly from PyPI:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install anvpy
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
# Quick Start
|
|
68
|
+
|
|
69
|
+
Navigate to your project directory containing `main.py`:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
cd MyProject
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Run the project:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
anvpy run
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Or specify a project path:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
anvpy run --path="C:\Projects\MyProject"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
# Project Structure
|
|
90
|
+
|
|
91
|
+
AnvPy expects your project to contain a `main.py` entry point.
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
MyProject/
|
|
97
|
+
├── main.py
|
|
98
|
+
├── assets/
|
|
99
|
+
├── images/
|
|
100
|
+
├── data/
|
|
101
|
+
└── ...
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
# How It Works
|
|
107
|
+
|
|
108
|
+
When you execute:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
anvpy run
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
the CLI automatically:
|
|
115
|
+
|
|
116
|
+
1. Detects the project directory.
|
|
117
|
+
2. Verifies that `main.py` exists.
|
|
118
|
+
3. Connects to the AnvPy application running on your Android device.
|
|
119
|
+
4. Synchronizes only the files that have changed.
|
|
120
|
+
5. Removes files deleted from the Android project.
|
|
121
|
+
6. Launches the synchronized project automatically on the device.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
# Example Output
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
[ACTION] Connecting to device...
|
|
129
|
+
[OK] Connected to 192.168.1.5
|
|
130
|
+
|
|
131
|
+
[ACTION] Syncing project: MyProject
|
|
132
|
+
[OK] Uploaded: main.py
|
|
133
|
+
[OK] Uploaded: assets/logo.png
|
|
134
|
+
[OK] Deleted: assets/old_logo.png
|
|
135
|
+
|
|
136
|
+
[OK] Sync complete (Uploaded: 2, Deleted: 1)
|
|
137
|
+
|
|
138
|
+
[ACTION] Running project: MyProject
|
|
139
|
+
[OK] Project started
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
# Why AnvPy CLI?
|
|
145
|
+
|
|
146
|
+
Developing for Android often involves repetitive deployment steps such as manually copying project files, connecting a USB cable, configuring ADB, or setting up custom synchronization workflows before every test.
|
|
147
|
+
|
|
148
|
+
AnvPy CLI eliminates these repetitive tasks by automatically discovering your Android device, synchronizing only modified files, and launching your project with a single command over your local network. This significantly reduces iteration time, allowing you to focus on building your application instead of managing deployment.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
# License
|
|
153
|
+
|
|
154
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
anvpy/__init__.py,sha256=Pru0BlFBASFCFo7McHdohtKkUtgMPDwbGfyUZlE2_Vw,21
|
|
2
|
+
anvpy/connection.py,sha256=_-vw-T3REG9okI3iCFmeKDBFUMsSK-kN0RO5JXtPChA,2127
|
|
3
|
+
anvpy/logger.py,sha256=F6U9bqqjPYmTOdtITiujkkBDgKe8GKeoNAv6EgKpqpc,546
|
|
4
|
+
anvpy/main.py,sha256=uGs-YsL_MotQ-v4V3lx3pH01xnj28ILBgrDYveyilj0,874
|
|
5
|
+
anvpy/run.py,sha256=XNo1ANG1WDtrHK1Sz8naITA7ujHJSxQs1-RHkcePF98,763
|
|
6
|
+
anvpy/sync.py,sha256=5GN-_xDMNj5pkmD9GWXYy57Sfx-D5cABzOUN-gJojQs,4334
|
|
7
|
+
anvpy/utils.py,sha256=OD3T2_pHz90xcxToF08sc6Cfkkt0t7LkILaS1Qd7ogw,259
|
|
8
|
+
anvpy-0.1.0.dist-info/licenses/LICENSE,sha256=9C4JyfykmY3LdxHV3QH4XZlcKHKpegmC4TCZ5kbUo84,1082
|
|
9
|
+
anvpy-0.1.0.dist-info/METADATA,sha256=YrOZIPf4y1xCose2bF6s8rovdEoEEWw2SsDV8W9qxtA,4116
|
|
10
|
+
anvpy-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
anvpy-0.1.0.dist-info/entry_points.txt,sha256=v0dCxD2yPDNPYb_bz8kJN9ZZ_Jp3MA71ODAVNQd_aeg,42
|
|
12
|
+
anvpy-0.1.0.dist-info/top_level.txt,sha256=RY7GZCA1P3HI5-szpXn7TxQroQEqlA7tCoDcRxZPOOQ,6
|
|
13
|
+
anvpy-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ajay, Amar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
anvpy
|