nomix-clicker 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Timofey Ivanenko
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,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: nomix-clicker
3
+ Version: 0.1.0
4
+ Summary: iOS device automation via the Nomix Clicker API — simulated touch input with AI screen recognition.
5
+ Author: Timofey Ivanenko
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://panel.nomixclicker.com
8
+ Project-URL: Documentation, https://panel.nomixclicker.com/docs
9
+ Project-URL: Repository, https://github.com/timofeyivanenko/ClickerScriptingLibrary
10
+ Keywords: automation,ios,clicker,nomix,ui-automation,device-automation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: requests>=2.32
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0; extra == "dev"
25
+ Requires-Dist: build>=1.0; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ ## NomixClicker Scripting Library
29
+
30
+ This is a library of ready-made scripts written in Python and using the free [Clicker API](https://panel.nomixclicker.com/docs).
31
+
32
+ ### What Is Clicker?
33
+
34
+ NomixClicker is a controlling dongle for iOS. It allows manual control, automatic control via API, and fully autonomous AI mode.
35
+
36
+ Part of the features are work-in-progress, see the [Roadmap](https://nomixclicker.com/).
37
+
38
+ <p align="center">
39
+ <img src="res/reddit-script-example.gif" width="480" height="360">
40
+ <br>
41
+ <em style="font-size: 0.85em; opacity: 0.7;">Example: quick script for Reddit warmup.</em>
42
+ </p>
43
+
44
+ ### Installation
45
+
46
+ Requires Python 3.10+. Install from PyPI:
47
+
48
+ ```commandline
49
+ pip install nomix-clicker
50
+ ```
51
+
52
+ Buy a Clicker device if you don't have it (delivery takes ~2 weeks to any place in the world), then open the [Panel](https://panel.nomixclicker.com/choose_device) and get your API token.
53
+
54
+ [Purchase on the official site only.](https://panel.nomixclicker.com/payment)
55
+
56
+ ### Configuration
57
+
58
+ Provide your API token and device id in either of two ways (environment variables take precedence):
59
+
60
+ **Environment variables:**
61
+
62
+ ```commandline
63
+ export NOMIX_API_KEY="your-api-key"
64
+ export NOMIX_DEVICE_ID="your-device-id"
65
+ ```
66
+
67
+ **Or a `config.json`** in your working directory (copy `config.example.json`):
68
+
69
+ ```json
70
+ {
71
+ "API_KEY": "your-api-key",
72
+ "DEVICE_ID": "your-device-id",
73
+ "API_URL": "https://panel.nomixclicker.com/clicker/v1"
74
+ }
75
+ ```
76
+
77
+ ### Quick start
78
+
79
+ ```python
80
+ from nomix_clicker import Clicker, parse_screen, open_app, DEVICE_ID
81
+
82
+ clicker = Clicker(DEVICE_ID)
83
+ open_app(clicker, "Instagram")
84
+
85
+ screen = parse_screen(clicker)
86
+ screen.find_and_click(clicker, "like")
87
+ ```
88
+
89
+ Manual control of iPhones will work out-of-the-box, no setup needed.
90
+
91
+ ### Examples
92
+
93
+ Ready-made example scripts live in the [`examples/`](examples/) folder. After installing the package, run one directly, for example:
94
+
95
+ ```commandline
96
+ python3 examples/airplane-mode-iphone-12.py
97
+ ```
98
+
99
+ - [**instagram-warmup.py**](examples/instagram-warmup.py) - Scrolling Instagram Reels, liking, and commenting with configurable probabilities.
100
+ - [**restart_all_devices.py**](examples/restart_all_devices.py) - Restarts all devices associated with your account.
101
+ - [**airplane-mode-iphone-12.py**](examples/airplane-mode-iphone-12.py) - Toggles airplane mode on and off on iPhone 12.
102
+ - [**switch-screen-viewing-iphone-12.py**](examples/switch-screen-viewing-iphone-12.py) - Turns on/off Clicker screen viewing.
103
+
104
+ ### Local development
105
+
106
+ To work on the library itself, install it in editable mode:
107
+
108
+ ```commandline
109
+ pip install -e .
110
+ ```
111
+
@@ -0,0 +1,84 @@
1
+ ## NomixClicker Scripting Library
2
+
3
+ This is a library of ready-made scripts written in Python and using the free [Clicker API](https://panel.nomixclicker.com/docs).
4
+
5
+ ### What Is Clicker?
6
+
7
+ NomixClicker is a controlling dongle for iOS. It allows manual control, automatic control via API, and fully autonomous AI mode.
8
+
9
+ Part of the features are work-in-progress, see the [Roadmap](https://nomixclicker.com/).
10
+
11
+ <p align="center">
12
+ <img src="res/reddit-script-example.gif" width="480" height="360">
13
+ <br>
14
+ <em style="font-size: 0.85em; opacity: 0.7;">Example: quick script for Reddit warmup.</em>
15
+ </p>
16
+
17
+ ### Installation
18
+
19
+ Requires Python 3.10+. Install from PyPI:
20
+
21
+ ```commandline
22
+ pip install nomix-clicker
23
+ ```
24
+
25
+ Buy a Clicker device if you don't have it (delivery takes ~2 weeks to any place in the world), then open the [Panel](https://panel.nomixclicker.com/choose_device) and get your API token.
26
+
27
+ [Purchase on the official site only.](https://panel.nomixclicker.com/payment)
28
+
29
+ ### Configuration
30
+
31
+ Provide your API token and device id in either of two ways (environment variables take precedence):
32
+
33
+ **Environment variables:**
34
+
35
+ ```commandline
36
+ export NOMIX_API_KEY="your-api-key"
37
+ export NOMIX_DEVICE_ID="your-device-id"
38
+ ```
39
+
40
+ **Or a `config.json`** in your working directory (copy `config.example.json`):
41
+
42
+ ```json
43
+ {
44
+ "API_KEY": "your-api-key",
45
+ "DEVICE_ID": "your-device-id",
46
+ "API_URL": "https://panel.nomixclicker.com/clicker/v1"
47
+ }
48
+ ```
49
+
50
+ ### Quick start
51
+
52
+ ```python
53
+ from nomix_clicker import Clicker, parse_screen, open_app, DEVICE_ID
54
+
55
+ clicker = Clicker(DEVICE_ID)
56
+ open_app(clicker, "Instagram")
57
+
58
+ screen = parse_screen(clicker)
59
+ screen.find_and_click(clicker, "like")
60
+ ```
61
+
62
+ Manual control of iPhones will work out-of-the-box, no setup needed.
63
+
64
+ ### Examples
65
+
66
+ Ready-made example scripts live in the [`examples/`](examples/) folder. After installing the package, run one directly, for example:
67
+
68
+ ```commandline
69
+ python3 examples/airplane-mode-iphone-12.py
70
+ ```
71
+
72
+ - [**instagram-warmup.py**](examples/instagram-warmup.py) - Scrolling Instagram Reels, liking, and commenting with configurable probabilities.
73
+ - [**restart_all_devices.py**](examples/restart_all_devices.py) - Restarts all devices associated with your account.
74
+ - [**airplane-mode-iphone-12.py**](examples/airplane-mode-iphone-12.py) - Toggles airplane mode on and off on iPhone 12.
75
+ - [**switch-screen-viewing-iphone-12.py**](examples/switch-screen-viewing-iphone-12.py) - Turns on/off Clicker screen viewing.
76
+
77
+ ### Local development
78
+
79
+ To work on the library itself, install it in editable mode:
80
+
81
+ ```commandline
82
+ pip install -e .
83
+ ```
84
+
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nomix-clicker"
7
+ version = "0.1.0"
8
+ description = "iOS device automation via the Nomix Clicker API — simulated touch input with AI screen recognition."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "Timofey Ivanenko" }]
13
+ keywords = ["automation", "ios", "clicker", "nomix", "ui-automation", "device-automation"]
14
+ dependencies = ["requests>=2.32"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Operating System :: OS Independent",
23
+ "Topic :: Software Development :: Testing",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ dev = ["pytest>=8.0", "build>=1.0"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://panel.nomixclicker.com"
31
+ Documentation = "https://panel.nomixclicker.com/docs"
32
+ Repository = "https://github.com/timofeyivanenko/ClickerScriptingLibrary"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,52 @@
1
+ """nomix_clicker — iOS device automation via the Nomix Clicker API.
2
+
3
+ Simulate touch input and use AI screen recognition to navigate apps.
4
+
5
+ from nomix_clicker import Clicker, parse_screen, open_app
6
+
7
+ clicker = Clicker("your-device-id")
8
+ open_app(clicker, "Instagram")
9
+ screen = parse_screen(clicker)
10
+ """
11
+
12
+ from .actions import (
13
+ chance_tap,
14
+ close_app,
15
+ find_and_click,
16
+ is_ad,
17
+ open_app,
18
+ post_comment,
19
+ random_sleep,
20
+ swipe_back,
21
+ swipe_feed,
22
+ )
23
+ from .agent import Agent
24
+ from .clicker import Clicker
25
+ from .environment import API_KEY, API_URL, DEVICE_ID
26
+ from .recognition import Element, Screen, parse_screen
27
+
28
+ __version__ = "0.1.0"
29
+
30
+ __all__ = [
31
+ # core
32
+ "Clicker",
33
+ "Agent",
34
+ "Screen",
35
+ "Element",
36
+ "parse_screen",
37
+ # actions
38
+ "open_app",
39
+ "close_app",
40
+ "find_and_click",
41
+ "swipe_feed",
42
+ "swipe_back",
43
+ "is_ad",
44
+ "chance_tap",
45
+ "post_comment",
46
+ "random_sleep",
47
+ # config
48
+ "API_URL",
49
+ "API_KEY",
50
+ "DEVICE_ID",
51
+ "__version__",
52
+ ]
@@ -0,0 +1,184 @@
1
+ import random
2
+ from time import sleep
3
+ from typing import Optional
4
+
5
+ from .clicker import Clicker
6
+ from .recognition import Screen, parse_screen
7
+
8
+ COMMENT_SECTION_KEYWORDS = [
9
+ "close comments",
10
+ "filter comments",
11
+ "add comment",
12
+ "add a comment",
13
+ "join the conversation",
14
+ ]
15
+
16
+
17
+ def open_app(clicker: Clicker, app_name: str, retries: int = 3) -> Optional[Screen]:
18
+ """Open any app via iOS Spotlight search. Retries up to `retries` times.
19
+
20
+ Returns the parsed screen on success, or None on failure.
21
+ """
22
+ for attempt in range(1, retries + 1):
23
+ print(f"Opening Spotlight (attempt {attempt}/{retries})...")
24
+ clicker.swipe((16000, 10000), down=8000, duration=300)
25
+ sleep(0.5)
26
+
27
+ print(f"Typing '{app_name}'...")
28
+ clicker.type(app_name)
29
+ sleep(2)
30
+
31
+ screen = parse_screen(clicker)
32
+ if not screen or not screen.find_and_click(clicker, app_name):
33
+ print(f"WARNING: '{app_name}' not found in Spotlight results")
34
+ close_app(clicker)
35
+ continue
36
+
37
+ sleep(3)
38
+
39
+ screen = parse_screen(clicker)
40
+ if screen and app_name.lower() in screen.app_name.lower():
41
+ return screen
42
+
43
+ print(f"WARNING: '{app_name}' did not open (current app: '{screen.app_name if screen else 'None'}')")
44
+ close_app(clicker)
45
+
46
+ print(f"ERROR: Failed to open '{app_name}' after {retries} attempts.")
47
+ return None
48
+
49
+
50
+ def _do_close_app(clicker: Clicker) -> None:
51
+ # Slow swipe up from bottom edge to open app switcher
52
+ sleep(1)
53
+ clicker.swipe((16384, 32767), up=4767, duration=1000)
54
+ sleep(5)
55
+ # Swipe up on the last app card to dismiss it
56
+ clicker.swipe((26500, 20000), up=10000, duration=300)
57
+ sleep(5)
58
+ # Tap home area to exit app switcher
59
+ clicker.click((16384, 30000), duration=100)
60
+
61
+
62
+ def close_app(clicker: Clicker, retries: int = 3) -> bool:
63
+ """Open app switcher, dismiss the last app, then tap home.
64
+
65
+ Verifies the device returned to the Home Screen after closing.
66
+ Retries up to `retries` times if the app is still in the foreground.
67
+ Returns True if the Home Screen is confirmed, False otherwise.
68
+ """
69
+ for attempt in range(1, retries + 1):
70
+ print(f"Closing app (attempt {attempt}/{retries})...")
71
+ _do_close_app(clicker)
72
+ sleep(3)
73
+ screen = parse_screen(clicker)
74
+ if not screen or screen.app_name.lower() == "home screen":
75
+ print("App closed successfully.")
76
+ return True
77
+ print(f"App still open after attempt {attempt} (screen: '{screen.app_name}'), retrying...")
78
+ print(f"Warning: could not confirm app was closed after {retries} attempts.")
79
+ return False
80
+
81
+
82
+ _AD_KEYWORDS = [
83
+ "advertising", "advertisement", "sponsored",
84
+ "contact us", "shop now", "learn more",
85
+ "install now", "send message", "get quote",
86
+ ]
87
+
88
+
89
+ def is_ad(screen: Screen) -> bool:
90
+ """Return True if the current screen looks like an ad."""
91
+ return (screen.contains(*_AD_KEYWORDS)
92
+ or any(el.content.lower() == "ad" for el in screen.elements))
93
+
94
+
95
+ def is_comment_section_opened(screen: Screen) -> bool:
96
+ """Return True if the comments sheet is currently pulled up."""
97
+ return screen.contains(*COMMENT_SECTION_KEYWORDS)
98
+
99
+
100
+ def chance_tap(clicker: Clicker, screen: Screen, name: str, chance: float) -> bool:
101
+ """Roll the dice and tap a button found on screen. Returns True if tapped."""
102
+ if random.random() >= chance:
103
+ return False
104
+ return screen.find_and_click(clicker, name)
105
+
106
+
107
+ def random_sleep(min_s: float = 0.3, max_s: float = 0.8) -> None:
108
+ """Sleep for a random duration to simulate human behavior."""
109
+ sleep(random.uniform(min_s, max_s))
110
+
111
+
112
+ def swipe_back(clicker: Clicker) -> None:
113
+ """iOS swipe-back gesture (swipe right from left edge)."""
114
+ clicker.swipe((5000, 16000), right=20000, duration=300)
115
+
116
+
117
+ def swipe_feed(clicker: Clicker) -> None:
118
+ """Swipe up to the next feed item."""
119
+ clicker.swipe((16383, 26213), up=6553, duration=100)
120
+
121
+
122
+ def find_and_click(clicker: Clicker, *keywords: str, interactive_only: bool = True) -> bool:
123
+ """Get screen, find element by keywords, and click it. Returns True if clicked."""
124
+ screen = parse_screen(clicker)
125
+ if not screen:
126
+ return False
127
+ return screen.find_and_click(clicker, *keywords, interactive_only=interactive_only)
128
+
129
+
130
+ def post_comment(
131
+ clicker: Clicker,
132
+ text: str,
133
+ input_keywords: list[str],
134
+ submit_keyword: str | list[str],
135
+ cached_coords: Optional[dict] = None,
136
+ ) -> bool:
137
+ """Find a comment input field, type text, and submit.
138
+
139
+ On the first call, parses the screen twice: once to find the input field,
140
+ and again after typing to find the send button (which only appears once the
141
+ keyboard is open). Coords are stored in `cached_coords` so subsequent calls
142
+ skip both parses entirely.
143
+ """
144
+ if cached_coords and "comment_input" in cached_coords and "comment_submit" in cached_coords:
145
+ clicker.click(cached_coords["comment_input"])
146
+ clicker.type(text)
147
+ sleep(0.5)
148
+ clicker.click(cached_coords["comment_submit"])
149
+ return True
150
+
151
+ screen = parse_screen(clicker)
152
+ if not screen:
153
+ return False
154
+
155
+ input_coords = screen.find(*input_keywords, interactive_only=False)
156
+ if not input_coords:
157
+ print("WARNING: Comment input not found")
158
+ return False
159
+
160
+ clicker.click(input_coords)
161
+ clicker.type(text)
162
+ sleep(1)
163
+
164
+ screen = parse_screen(clicker)
165
+ if not screen:
166
+ return False
167
+
168
+ keywords = submit_keyword if isinstance(submit_keyword, list) else [submit_keyword]
169
+ submit_coords = screen.find(*keywords, interactive_only=False)
170
+ if not submit_coords:
171
+ print(f"WARNING: Submit button not found, elements: {[e.content for e in screen.elements]}")
172
+ return False
173
+
174
+ if cached_coords is not None:
175
+ cached_coords["comment_input"] = input_coords
176
+ cached_coords["comment_submit"] = submit_coords
177
+ print(f"Cached comment_input at {input_coords}, comment_submit at {submit_coords}")
178
+
179
+ clicker.click(submit_coords)
180
+ sleep(1)
181
+ clicker.click((16383, 4096)) # dismiss comments sheet
182
+ sleep(0.5)
183
+
184
+ return True
@@ -0,0 +1,78 @@
1
+ import time
2
+
3
+ from .api_helper import run_agent, get_agent_task, cancel_agent_task
4
+
5
+
6
+ class Agent:
7
+ """AI agent controller for a device."""
8
+
9
+ def __init__(self, device_id: str):
10
+ self.device_id = device_id
11
+ self.current_task_id: str | None = None
12
+
13
+ def execute(self, task: str) -> str:
14
+ """Start a new agent task. Returns task_id and stores it internally."""
15
+ result = run_agent(self.device_id, task)
16
+ task_id = result.get("task_id")
17
+ if not task_id:
18
+ raise ValueError(f"API response missing 'task_id': {result}")
19
+ self.current_task_id = task_id
20
+ return self.current_task_id
21
+
22
+ def get_status(self, task_id: str | None = None) -> dict:
23
+ """Get task status. Uses current_task_id if none provided."""
24
+ tid = task_id or self.current_task_id
25
+ if not tid:
26
+ raise ValueError("No task_id -- call execute() first or pass task_id")
27
+ return get_agent_task(self.device_id, tid)
28
+
29
+ def cancel(self, task_id: str | None = None) -> dict:
30
+ """Cancel a running task."""
31
+ tid = task_id or self.current_task_id
32
+ if not tid:
33
+ raise ValueError("No task_id -- call execute() first or pass task_id")
34
+ result = cancel_agent_task(self.device_id, tid)
35
+ if tid == self.current_task_id:
36
+ self.current_task_id = None
37
+ return result
38
+
39
+ def poll(
40
+ self,
41
+ task_id: str | None = None,
42
+ interval: float = 1.0,
43
+ ) -> str | None:
44
+ """Poll task until completion, printing events to stdout."""
45
+ tid = task_id or self.current_task_id
46
+ if not tid:
47
+ raise ValueError("No task_id -- call execute() first or pass task_id")
48
+
49
+ seen_events = 0
50
+ while True:
51
+ task = get_agent_task(self.device_id, tid)
52
+ events = task.get("events", [])
53
+
54
+ for event in events[seen_events:]:
55
+ step = event.get("step", "?")
56
+ max_steps = event.get("max_steps", "?")
57
+ action = event.get("action", "?")
58
+ result = event.get("result", "?")
59
+ print(f"[{step}/{max_steps}] {action} -> {result}")
60
+ seen_events = len(events)
61
+
62
+ status = task.get("status")
63
+ if status not in ("pending", "running"):
64
+ result = task.get("result")
65
+ print(f"\n--- Agent {status}: {result}")
66
+ return result
67
+
68
+ time.sleep(interval)
69
+
70
+ def run(self, task: str) -> str | None:
71
+ """Convenience: execute a task and poll until done."""
72
+ try:
73
+ self.execute(task)
74
+ return self.poll()
75
+ except KeyboardInterrupt:
76
+ print("\n\nInterrupted — cancelling task...")
77
+ self.cancel()
78
+ return