feory 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.
- feory-0.1.0/PKG-INFO +111 -0
- feory-0.1.0/README.md +90 -0
- feory-0.1.0/feory/__init__.py +47 -0
- feory-0.1.0/feory/client.py +313 -0
- feory-0.1.0/feory/exceptions.py +23 -0
- feory-0.1.0/feory/server.py +127 -0
- feory-0.1.0/feory/widgets.py +259 -0
- feory-0.1.0/feory.egg-info/PKG-INFO +111 -0
- feory-0.1.0/feory.egg-info/SOURCES.txt +12 -0
- feory-0.1.0/feory.egg-info/dependency_links.txt +1 -0
- feory-0.1.0/feory.egg-info/top_level.txt +1 -0
- feory-0.1.0/pyproject.toml +25 -0
- feory-0.1.0/setup.cfg +4 -0
- feory-0.1.0/setup.py +19 -0
feory-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: feory
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal Python SDK for Feory Android App - Dynamic UI Builder & Bot Control Bridge
|
|
5
|
+
Home-page: https://github.com/zizo0/feory
|
|
6
|
+
Author: zizo0
|
|
7
|
+
Author-email: zizo0 <zly30257@gmail.com>
|
|
8
|
+
Project-URL: Homepage, https://github.com/zizo0/feory
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/zizo0/feory/issues
|
|
10
|
+
Keywords: feory,android,bot,ui-builder,automation,telegram,bridge
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
15
|
+
Classifier: Topic :: Communications
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
|
|
22
|
+
# Feory Python SDK ⚡
|
|
23
|
+
|
|
24
|
+
Universal Python SDK & Bridge for the **Feory Android App**.
|
|
25
|
+
Create dynamic Bento-style Android UIs, manage bots, handle bi-directional events, and control servers remotely from your mobile device.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🚀 Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install feory
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 🛠️ Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import feory
|
|
41
|
+
|
|
42
|
+
# 1. Initialize with your bot token from the Feory App
|
|
43
|
+
client = feory.Client(
|
|
44
|
+
token="feory_tok_core_829a1b",
|
|
45
|
+
bot_name="My Awesome Python Bot",
|
|
46
|
+
port=8765
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# 2. Register Dynamic UI Widgets
|
|
50
|
+
client.register_button(
|
|
51
|
+
id="btn_start_scrape",
|
|
52
|
+
label="Start Data Pipeline",
|
|
53
|
+
category="general",
|
|
54
|
+
color_scheme="LILAC",
|
|
55
|
+
icon="play"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
client.register_slider(
|
|
59
|
+
id="slider_threads",
|
|
60
|
+
label="Worker Threads",
|
|
61
|
+
min_value=1,
|
|
62
|
+
max_value=32,
|
|
63
|
+
default_value=8
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
client.register_metric_card(
|
|
67
|
+
id="stat_processed",
|
|
68
|
+
title="Items Processed",
|
|
69
|
+
value="12,450",
|
|
70
|
+
badge="+14% today",
|
|
71
|
+
accent_color="GREEN"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# 3. Handle Events from Feory Android App
|
|
75
|
+
@client.on_action("btn_start_scrape")
|
|
76
|
+
def handle_scrape(payload):
|
|
77
|
+
client.log("Starting scraping tasks...", "INFO")
|
|
78
|
+
# Your automation logic here
|
|
79
|
+
return {"status": "started", "items": 100}
|
|
80
|
+
|
|
81
|
+
@client.on_value_change("slider_threads")
|
|
82
|
+
def handle_thread_change(new_val):
|
|
83
|
+
client.log(f"Thread pool updated to: {new_val}", "INFO")
|
|
84
|
+
|
|
85
|
+
# 4. Keep your bot server running
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
client.run_forever()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 📖 Features
|
|
93
|
+
|
|
94
|
+
- **Dynamic Android UI Generation**: Inject buttons, sliders, toggles, text inputs, dropdowns, and Bento metric cards on the fly without touching Kotlin code.
|
|
95
|
+
- **Two-Way Event Bus**: Receive instant button clicks and slider callbacks directly in your Python functions.
|
|
96
|
+
- **Real-Time Telemetry & Terminal Logs**: Stream live console logs directly to the Feory App terminal.
|
|
97
|
+
- **Resource Lock & Collision Prevention**: Coordinate multiple bots accessing the same channels or resources with built-in Mutex locks.
|
|
98
|
+
- **Zero-Dependency Core**: Uses Python standard library by default for maximum portability.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 🔒 Authentication
|
|
103
|
+
|
|
104
|
+
Every bot connects securely using its unique **Feory Token** (`feory_tok_...`). You can create, manage, and copy tokens directly inside the **Tokens** screen of the Feory Android App.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 👤 Author
|
|
109
|
+
|
|
110
|
+
Developed by **zizo0**
|
|
111
|
+
Email: `zly30257@gmail.com`
|
feory-0.1.0/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Feory Python SDK ⚡
|
|
2
|
+
|
|
3
|
+
Universal Python SDK & Bridge for the **Feory Android App**.
|
|
4
|
+
Create dynamic Bento-style Android UIs, manage bots, handle bi-directional events, and control servers remotely from your mobile device.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 🚀 Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install feory
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 🛠️ Quick Start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import feory
|
|
20
|
+
|
|
21
|
+
# 1. Initialize with your bot token from the Feory App
|
|
22
|
+
client = feory.Client(
|
|
23
|
+
token="feory_tok_core_829a1b",
|
|
24
|
+
bot_name="My Awesome Python Bot",
|
|
25
|
+
port=8765
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# 2. Register Dynamic UI Widgets
|
|
29
|
+
client.register_button(
|
|
30
|
+
id="btn_start_scrape",
|
|
31
|
+
label="Start Data Pipeline",
|
|
32
|
+
category="general",
|
|
33
|
+
color_scheme="LILAC",
|
|
34
|
+
icon="play"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
client.register_slider(
|
|
38
|
+
id="slider_threads",
|
|
39
|
+
label="Worker Threads",
|
|
40
|
+
min_value=1,
|
|
41
|
+
max_value=32,
|
|
42
|
+
default_value=8
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
client.register_metric_card(
|
|
46
|
+
id="stat_processed",
|
|
47
|
+
title="Items Processed",
|
|
48
|
+
value="12,450",
|
|
49
|
+
badge="+14% today",
|
|
50
|
+
accent_color="GREEN"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# 3. Handle Events from Feory Android App
|
|
54
|
+
@client.on_action("btn_start_scrape")
|
|
55
|
+
def handle_scrape(payload):
|
|
56
|
+
client.log("Starting scraping tasks...", "INFO")
|
|
57
|
+
# Your automation logic here
|
|
58
|
+
return {"status": "started", "items": 100}
|
|
59
|
+
|
|
60
|
+
@client.on_value_change("slider_threads")
|
|
61
|
+
def handle_thread_change(new_val):
|
|
62
|
+
client.log(f"Thread pool updated to: {new_val}", "INFO")
|
|
63
|
+
|
|
64
|
+
# 4. Keep your bot server running
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
client.run_forever()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 📖 Features
|
|
72
|
+
|
|
73
|
+
- **Dynamic Android UI Generation**: Inject buttons, sliders, toggles, text inputs, dropdowns, and Bento metric cards on the fly without touching Kotlin code.
|
|
74
|
+
- **Two-Way Event Bus**: Receive instant button clicks and slider callbacks directly in your Python functions.
|
|
75
|
+
- **Real-Time Telemetry & Terminal Logs**: Stream live console logs directly to the Feory App terminal.
|
|
76
|
+
- **Resource Lock & Collision Prevention**: Coordinate multiple bots accessing the same channels or resources with built-in Mutex locks.
|
|
77
|
+
- **Zero-Dependency Core**: Uses Python standard library by default for maximum portability.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 🔒 Authentication
|
|
82
|
+
|
|
83
|
+
Every bot connects securely using its unique **Feory Token** (`feory_tok_...`). You can create, manage, and copy tokens directly inside the **Tokens** screen of the Feory Android App.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 👤 Author
|
|
88
|
+
|
|
89
|
+
Developed by **zizo0**
|
|
90
|
+
Email: `zly30257@gmail.com`
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Feory Python SDK - Universal Bridge for Android Dynamic UI & Bot Control
|
|
3
|
+
Author: zizo0
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .client import Client, FeoryClient
|
|
7
|
+
from .widgets import (
|
|
8
|
+
Widget,
|
|
9
|
+
ButtonWidget,
|
|
10
|
+
SliderWidget,
|
|
11
|
+
ToggleWidget,
|
|
12
|
+
TextFieldWidget,
|
|
13
|
+
MetricWidget,
|
|
14
|
+
DropdownWidget,
|
|
15
|
+
CategorySection,
|
|
16
|
+
WidgetType,
|
|
17
|
+
)
|
|
18
|
+
from .server import FeoryServer
|
|
19
|
+
from .exceptions import (
|
|
20
|
+
FeoryError,
|
|
21
|
+
FeoryAuthError,
|
|
22
|
+
FeoryConnectionError,
|
|
23
|
+
FeoryLockError,
|
|
24
|
+
FeorySchemaError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
__author__ = "zizo0"
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Client",
|
|
31
|
+
"FeoryClient",
|
|
32
|
+
"Widget",
|
|
33
|
+
"ButtonWidget",
|
|
34
|
+
"SliderWidget",
|
|
35
|
+
"ToggleWidget",
|
|
36
|
+
"TextFieldWidget",
|
|
37
|
+
"MetricWidget",
|
|
38
|
+
"DropdownWidget",
|
|
39
|
+
"CategorySection",
|
|
40
|
+
"WidgetType",
|
|
41
|
+
"FeoryServer",
|
|
42
|
+
"FeoryError",
|
|
43
|
+
"FeoryAuthError",
|
|
44
|
+
"FeoryConnectionError",
|
|
45
|
+
"FeoryLockError",
|
|
46
|
+
"FeorySchemaError",
|
|
47
|
+
]
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Feory Python SDK Client - Main Interface
|
|
3
|
+
Author: zizo0
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from typing import Callable, Dict, Any, List, Optional, Union
|
|
9
|
+
from .widgets import (
|
|
10
|
+
Widget,
|
|
11
|
+
ButtonWidget,
|
|
12
|
+
SliderWidget,
|
|
13
|
+
ToggleWidget,
|
|
14
|
+
TextFieldWidget,
|
|
15
|
+
MetricWidget,
|
|
16
|
+
DropdownWidget,
|
|
17
|
+
CategorySection,
|
|
18
|
+
WidgetType,
|
|
19
|
+
)
|
|
20
|
+
from .server import FeoryServer
|
|
21
|
+
from .exceptions import FeoryAuthError, FeoryLockError, FeorySchemaError
|
|
22
|
+
|
|
23
|
+
class Client:
|
|
24
|
+
"""
|
|
25
|
+
Feory Universal Client.
|
|
26
|
+
Connects your Python bot or server to Feory Android App.
|
|
27
|
+
"""
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
token: str,
|
|
31
|
+
bot_name: str = "Universal Python Bot",
|
|
32
|
+
category: str = "Automation",
|
|
33
|
+
description: str = "Universal Python Bot instance running on local server",
|
|
34
|
+
host: str = "0.0.0.0",
|
|
35
|
+
port: int = 8765,
|
|
36
|
+
auto_start_server: bool = True,
|
|
37
|
+
):
|
|
38
|
+
if not token:
|
|
39
|
+
raise FeoryAuthError("A valid token must be provided to initialize Feory Client.")
|
|
40
|
+
|
|
41
|
+
self.token = token
|
|
42
|
+
self.bot_name = bot_name
|
|
43
|
+
self.category = category
|
|
44
|
+
self.description = description
|
|
45
|
+
self.status = "ONLINE"
|
|
46
|
+
|
|
47
|
+
self.categories: Dict[str, CategorySection] = {
|
|
48
|
+
"general": CategorySection(id="general", title="التحكم العام", icon="tune"),
|
|
49
|
+
"metrics": CategorySection(id="metrics", title="الإحصائيات والأداء", icon="trending_up"),
|
|
50
|
+
"settings": CategorySection(id="settings", title="الإعدادات والتخصيص", icon="settings"),
|
|
51
|
+
}
|
|
52
|
+
self.widgets: Dict[str, Widget] = {}
|
|
53
|
+
self.action_handlers: Dict[str, Callable] = {}
|
|
54
|
+
self.value_handlers: Dict[str, Callable] = {}
|
|
55
|
+
self.active_locks: Dict[str, str] = {}
|
|
56
|
+
|
|
57
|
+
# Initialize embedded server
|
|
58
|
+
self.server = FeoryServer(client=self, host=host, port=port)
|
|
59
|
+
if auto_start_server:
|
|
60
|
+
self.server.start()
|
|
61
|
+
|
|
62
|
+
# --- UI Registration Methods ---
|
|
63
|
+
|
|
64
|
+
def register_category(self, id: str, title: str, description: Optional[str] = None, icon: Optional[str] = None) -> 'Client':
|
|
65
|
+
"""Registers a custom category group."""
|
|
66
|
+
self.categories[id] = CategorySection(id=id, title=title, description=description, icon=icon)
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def register_button(
|
|
70
|
+
self,
|
|
71
|
+
id: str,
|
|
72
|
+
label: str,
|
|
73
|
+
category: str = "general",
|
|
74
|
+
action_id: Optional[str] = None,
|
|
75
|
+
color_scheme: str = "LILAC",
|
|
76
|
+
variant: str = "FILLED",
|
|
77
|
+
icon: Optional[str] = "play",
|
|
78
|
+
description: Optional[str] = None,
|
|
79
|
+
handler: Optional[Callable] = None,
|
|
80
|
+
) -> ButtonWidget:
|
|
81
|
+
"""Registers an interactive button in the Feory Android UI."""
|
|
82
|
+
btn = ButtonWidget(
|
|
83
|
+
id=id,
|
|
84
|
+
label=label,
|
|
85
|
+
category=category,
|
|
86
|
+
action_id=action_id or id,
|
|
87
|
+
color_scheme=color_scheme,
|
|
88
|
+
variant=variant,
|
|
89
|
+
icon=icon,
|
|
90
|
+
description=description,
|
|
91
|
+
)
|
|
92
|
+
self.widgets[id] = btn
|
|
93
|
+
if handler:
|
|
94
|
+
self.action_handlers[action_id or id] = handler
|
|
95
|
+
return btn
|
|
96
|
+
|
|
97
|
+
def register_slider(
|
|
98
|
+
self,
|
|
99
|
+
id: str,
|
|
100
|
+
label: str,
|
|
101
|
+
category: str = "general",
|
|
102
|
+
min_value: float = 0.0,
|
|
103
|
+
max_value: float = 100.0,
|
|
104
|
+
default_value: float = 50.0,
|
|
105
|
+
step: Optional[float] = 1.0,
|
|
106
|
+
unit: Optional[str] = None,
|
|
107
|
+
description: Optional[str] = None,
|
|
108
|
+
handler: Optional[Callable] = None,
|
|
109
|
+
) -> SliderWidget:
|
|
110
|
+
"""Registers a dynamic range slider widget."""
|
|
111
|
+
slider = SliderWidget(
|
|
112
|
+
id=id,
|
|
113
|
+
label=label,
|
|
114
|
+
category=category,
|
|
115
|
+
min_value=min_value,
|
|
116
|
+
max_value=max_value,
|
|
117
|
+
current_value=default_value,
|
|
118
|
+
step=step,
|
|
119
|
+
unit=unit,
|
|
120
|
+
description=description,
|
|
121
|
+
)
|
|
122
|
+
self.widgets[id] = slider
|
|
123
|
+
if handler:
|
|
124
|
+
self.value_handlers[id] = handler
|
|
125
|
+
return slider
|
|
126
|
+
|
|
127
|
+
def register_toggle(
|
|
128
|
+
self,
|
|
129
|
+
id: str,
|
|
130
|
+
label: str,
|
|
131
|
+
category: str = "general",
|
|
132
|
+
default_state: bool = False,
|
|
133
|
+
description: Optional[str] = None,
|
|
134
|
+
handler: Optional[Callable] = None,
|
|
135
|
+
) -> ToggleWidget:
|
|
136
|
+
"""Registers a switch toggle widget."""
|
|
137
|
+
toggle = ToggleWidget(
|
|
138
|
+
id=id,
|
|
139
|
+
label=label,
|
|
140
|
+
category=category,
|
|
141
|
+
is_checked=default_state,
|
|
142
|
+
description=description,
|
|
143
|
+
)
|
|
144
|
+
self.widgets[id] = toggle
|
|
145
|
+
if handler:
|
|
146
|
+
self.value_handlers[id] = handler
|
|
147
|
+
return toggle
|
|
148
|
+
|
|
149
|
+
def register_text_input(
|
|
150
|
+
self,
|
|
151
|
+
id: str,
|
|
152
|
+
label: str,
|
|
153
|
+
placeholder: str = "Enter value...",
|
|
154
|
+
default_value: str = "",
|
|
155
|
+
category: str = "general",
|
|
156
|
+
is_secret: bool = False,
|
|
157
|
+
description: Optional[str] = None,
|
|
158
|
+
handler: Optional[Callable] = None,
|
|
159
|
+
) -> TextFieldWidget:
|
|
160
|
+
"""Registers a text input field widget."""
|
|
161
|
+
field = TextFieldWidget(
|
|
162
|
+
id=id,
|
|
163
|
+
label=label,
|
|
164
|
+
category=category,
|
|
165
|
+
placeholder=placeholder,
|
|
166
|
+
current_value=default_value,
|
|
167
|
+
is_secret=is_secret,
|
|
168
|
+
description=description,
|
|
169
|
+
)
|
|
170
|
+
self.widgets[id] = field
|
|
171
|
+
if handler:
|
|
172
|
+
self.value_handlers[id] = handler
|
|
173
|
+
return field
|
|
174
|
+
|
|
175
|
+
def register_metric_card(
|
|
176
|
+
self,
|
|
177
|
+
id: str,
|
|
178
|
+
title: str,
|
|
179
|
+
value: Union[str, int, float],
|
|
180
|
+
subtitle: Optional[str] = None,
|
|
181
|
+
badge: Optional[str] = None,
|
|
182
|
+
category: str = "metrics",
|
|
183
|
+
accent_color: str = "LILAC",
|
|
184
|
+
icon: Optional[str] = "trending_up",
|
|
185
|
+
) -> MetricWidget:
|
|
186
|
+
"""Registers a styled Bento Stat / Metric card."""
|
|
187
|
+
metric = MetricWidget(
|
|
188
|
+
id=id,
|
|
189
|
+
title=title,
|
|
190
|
+
value=value,
|
|
191
|
+
subtitle=subtitle,
|
|
192
|
+
badge=badge,
|
|
193
|
+
category=category,
|
|
194
|
+
accent_color=accent_color,
|
|
195
|
+
icon=icon,
|
|
196
|
+
)
|
|
197
|
+
self.widgets[id] = metric
|
|
198
|
+
return metric
|
|
199
|
+
|
|
200
|
+
def register_dropdown(
|
|
201
|
+
self,
|
|
202
|
+
id: str,
|
|
203
|
+
label: str,
|
|
204
|
+
options: List[str],
|
|
205
|
+
default_option: Optional[str] = None,
|
|
206
|
+
category: str = "general",
|
|
207
|
+
description: Optional[str] = None,
|
|
208
|
+
handler: Optional[Callable] = None,
|
|
209
|
+
) -> DropdownWidget:
|
|
210
|
+
"""Registers a selection dropdown widget."""
|
|
211
|
+
dropdown = DropdownWidget(
|
|
212
|
+
id=id,
|
|
213
|
+
label=label,
|
|
214
|
+
options=options,
|
|
215
|
+
selected_option=default_option,
|
|
216
|
+
category=category,
|
|
217
|
+
description=description,
|
|
218
|
+
)
|
|
219
|
+
self.widgets[id] = dropdown
|
|
220
|
+
if handler:
|
|
221
|
+
self.value_handlers[id] = handler
|
|
222
|
+
return dropdown
|
|
223
|
+
|
|
224
|
+
# --- Decorators for Event Handlers ---
|
|
225
|
+
|
|
226
|
+
def on_action(self, action_id: str):
|
|
227
|
+
"""Decorator to bind a Python function to a button click / action event."""
|
|
228
|
+
def decorator(func: Callable):
|
|
229
|
+
self.action_handlers[action_id] = func
|
|
230
|
+
return func
|
|
231
|
+
return decorator
|
|
232
|
+
|
|
233
|
+
def on_value_change(self, widget_id: str):
|
|
234
|
+
"""Decorator to bind a Python function to a slider, toggle, or input field change."""
|
|
235
|
+
def decorator(func: Callable):
|
|
236
|
+
self.value_handlers[widget_id] = func
|
|
237
|
+
return func
|
|
238
|
+
return decorator
|
|
239
|
+
|
|
240
|
+
# --- State and Realtime Methods ---
|
|
241
|
+
|
|
242
|
+
def set_status(self, status: str):
|
|
243
|
+
"""Updates the bot's status (ONLINE, IDLE, BUSY, OFFLINE)."""
|
|
244
|
+
self.status = status
|
|
245
|
+
self.log(f"[*] Bot status updated to: {status}", "INFO")
|
|
246
|
+
|
|
247
|
+
def log(self, message: str, level: str = "INFO"):
|
|
248
|
+
"""Sends a log line to the Feory App Terminal."""
|
|
249
|
+
timestamp_str = time.strftime("%H:%M:%S")
|
|
250
|
+
print(f"[{timestamp_str}] [{level}] {message}")
|
|
251
|
+
|
|
252
|
+
def update_widget(self, widget_id: str, **kwargs):
|
|
253
|
+
"""Updates properties of an existing widget dynamically."""
|
|
254
|
+
if widget_id in self.widgets:
|
|
255
|
+
widget = self.widgets[widget_id]
|
|
256
|
+
for key, val in kwargs.items():
|
|
257
|
+
if hasattr(widget, key):
|
|
258
|
+
setattr(widget, key, val)
|
|
259
|
+
|
|
260
|
+
def acquire_lock(self, resource_key: str) -> bool:
|
|
261
|
+
"""Acquires a resource lock to prevent collision with other bots."""
|
|
262
|
+
if resource_key in self.active_locks:
|
|
263
|
+
return False
|
|
264
|
+
self.active_locks[resource_key] = self.bot_name
|
|
265
|
+
self.log(f"[✓] Mutex lock acquired for '{resource_key}'", "INFO")
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
def release_lock(self, resource_key: str):
|
|
269
|
+
"""Releases a previously acquired mutex lock."""
|
|
270
|
+
if resource_key in self.active_locks:
|
|
271
|
+
del self.active_locks[resource_key]
|
|
272
|
+
self.log(f"[✓] Mutex lock released for '{resource_key}'", "INFO")
|
|
273
|
+
|
|
274
|
+
# --- Internal Dispatchers ---
|
|
275
|
+
|
|
276
|
+
def _dispatch_action(self, action_id: str, widget_id: str, payload: Dict[str, Any]):
|
|
277
|
+
handler = self.action_handlers.get(action_id) or self.action_handlers.get(widget_id)
|
|
278
|
+
if handler:
|
|
279
|
+
try:
|
|
280
|
+
self.log(f"[▶] Action invoked from App: {action_id}", "INFO")
|
|
281
|
+
return handler(payload)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
self.log(f"[!] Error in action handler '{action_id}': {e}", "ERROR")
|
|
284
|
+
return {"error": str(e)}
|
|
285
|
+
return {"status": "unhandled"}
|
|
286
|
+
|
|
287
|
+
def get_schema_dict(self) -> Dict[str, Any]:
|
|
288
|
+
"""Exports the full dynamic UI schema as JSON-serializable dictionary."""
|
|
289
|
+
return {
|
|
290
|
+
"version": "1.0.0",
|
|
291
|
+
"botName": self.bot_name,
|
|
292
|
+
"category": self.category,
|
|
293
|
+
"status": self.status,
|
|
294
|
+
"categories": [c.to_dict() for c in self.categories.values()],
|
|
295
|
+
"widgets": [w.to_dict() for w in self.widgets.values()],
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
def export_schema_json(self) -> str:
|
|
299
|
+
"""Returns the schema as JSON string."""
|
|
300
|
+
return json.dumps(self.get_schema_dict(), ensure_ascii=False, indent=2)
|
|
301
|
+
|
|
302
|
+
def run_forever(self):
|
|
303
|
+
"""Blocks main thread to keep bot service alive."""
|
|
304
|
+
self.log("[*] Feory Bot running. Press Ctrl+C to terminate.", "INFO")
|
|
305
|
+
try:
|
|
306
|
+
while True:
|
|
307
|
+
time.sleep(1)
|
|
308
|
+
except KeyboardInterrupt:
|
|
309
|
+
self.log("[*] Shutting down Feory Bot...", "INFO")
|
|
310
|
+
self.server.stop()
|
|
311
|
+
|
|
312
|
+
# Alias
|
|
313
|
+
FeoryClient = Client
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Feory Custom Exception Classes
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class FeoryError(Exception):
|
|
6
|
+
"""Base exception for Feory SDK."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class FeoryAuthError(FeoryError):
|
|
10
|
+
"""Raised when authentication with token fails."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class FeoryConnectionError(FeoryError):
|
|
14
|
+
"""Raised when connection to Feory App fails."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
class FeoryLockError(FeoryError):
|
|
18
|
+
"""Raised when resource lock acquisition fails due to collision."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
class FeorySchemaError(FeoryError):
|
|
22
|
+
"""Raised when UI schema registration contains invalid structure."""
|
|
23
|
+
pass
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Feory Lightweight Local Server Bridge
|
|
3
|
+
Zero-dependency HTTP JSON-RPC / REST bridge for Feory Android App
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
10
|
+
from typing import Callable, Dict, Any, Optional
|
|
11
|
+
|
|
12
|
+
class FeoryHTTPHandler(BaseHTTPRequestHandler):
|
|
13
|
+
client_instance = None
|
|
14
|
+
|
|
15
|
+
def _set_headers(self, status=200, content_type="application/json"):
|
|
16
|
+
self.send_response(status)
|
|
17
|
+
self.send_header("Content-Type", content_type)
|
|
18
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
19
|
+
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
20
|
+
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
21
|
+
self.end_headers()
|
|
22
|
+
|
|
23
|
+
def do_OPTIONS(self):
|
|
24
|
+
self._set_headers(204)
|
|
25
|
+
|
|
26
|
+
def do_GET(self):
|
|
27
|
+
if self.path == "/health" or self.path == "/":
|
|
28
|
+
self._set_headers(200)
|
|
29
|
+
res = {
|
|
30
|
+
"status": "ONLINE",
|
|
31
|
+
"service": "Feory Universal Bot Server",
|
|
32
|
+
"version": "0.1.0",
|
|
33
|
+
"timestamp": time.time(),
|
|
34
|
+
}
|
|
35
|
+
self.wfile.write(json.dumps(res).encode("utf-8"))
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
if self.path == "/schema":
|
|
39
|
+
self._set_headers(200)
|
|
40
|
+
schema = self.client_instance.get_schema_dict() if self.client_instance else {}
|
|
41
|
+
self.wfile.write(json.dumps(schema).encode("utf-8"))
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
self._set_headers(404)
|
|
45
|
+
self.wfile.write(json.dumps({"error": "Endpoint not found"}).encode("utf-8"))
|
|
46
|
+
|
|
47
|
+
def do_POST(self):
|
|
48
|
+
content_length = int(self.headers.get("Content-Length", 0))
|
|
49
|
+
post_data = self.rfile.read(content_length)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
payload = json.loads(post_data.decode("utf-8")) if post_data else {}
|
|
53
|
+
except Exception as e:
|
|
54
|
+
self._set_headers(400)
|
|
55
|
+
self.wfile.write(json.dumps({"error": f"Invalid JSON payload: {e}"}).encode("utf-8"))
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Authenticate Token
|
|
59
|
+
auth_header = self.headers.get("Authorization", "")
|
|
60
|
+
token = auth_header.replace("Bearer ", "").strip()
|
|
61
|
+
if not token and "token" in payload:
|
|
62
|
+
token = payload.get("token")
|
|
63
|
+
|
|
64
|
+
if self.client_instance and self.client_instance.token:
|
|
65
|
+
if token != self.client_instance.token:
|
|
66
|
+
self._set_headers(401)
|
|
67
|
+
self.wfile.write(json.dumps({"error": "Unauthorized: Invalid Bot Token"}).encode("utf-8"))
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
# Route Endpoints
|
|
71
|
+
if self.path == "/action" or self.path == "/events":
|
|
72
|
+
action_id = payload.get("actionId") or payload.get("action")
|
|
73
|
+
widget_id = payload.get("widgetId", "")
|
|
74
|
+
args = payload.get("payload", {})
|
|
75
|
+
|
|
76
|
+
if self.client_instance:
|
|
77
|
+
result = self.client_instance._dispatch_action(action_id, widget_id, args)
|
|
78
|
+
self._set_headers(200)
|
|
79
|
+
self.wfile.write(json.dumps({"success": True, "actionId": action_id, "result": result}).encode("utf-8"))
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if self.path == "/handshake":
|
|
83
|
+
self._set_headers(200)
|
|
84
|
+
info = {
|
|
85
|
+
"success": True,
|
|
86
|
+
"botName": self.client_instance.bot_name if self.client_instance else "FeoryBot",
|
|
87
|
+
"status": self.client_instance.status if self.client_instance else "ONLINE",
|
|
88
|
+
"widgetsCount": len(self.client_instance.widgets) if self.client_instance else 0
|
|
89
|
+
}
|
|
90
|
+
self.wfile.write(json.dumps(info).encode("utf-8"))
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
self._set_headers(404)
|
|
94
|
+
self.wfile.write(json.dumps({"error": "Unknown POST route"}).encode("utf-8"))
|
|
95
|
+
|
|
96
|
+
def log_message(self, format, *args):
|
|
97
|
+
# Suppress noisy default logging
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
class FeoryServer:
|
|
101
|
+
"""Embedded Zero-dependency HTTP Server for Feory Bot & App coordination."""
|
|
102
|
+
def __init__(self, client, host: str = "0.0.0.0", port: int = 8765):
|
|
103
|
+
self.client = client
|
|
104
|
+
self.host = host
|
|
105
|
+
self.port = port
|
|
106
|
+
self.server: Optional[HTTPServer] = None
|
|
107
|
+
self.thread: Optional[threading.Thread] = None
|
|
108
|
+
self.is_running = False
|
|
109
|
+
|
|
110
|
+
def start(self):
|
|
111
|
+
"""Starts the server in a background daemon thread."""
|
|
112
|
+
if self.is_running:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
FeoryHTTPHandler.client_instance = self.client
|
|
116
|
+
self.server = HTTPServer((self.host, self.port), FeoryHTTPHandler)
|
|
117
|
+
self.is_running = True
|
|
118
|
+
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
|
119
|
+
self.thread.start()
|
|
120
|
+
self.client.log(f"[✓] Feory Server listening on http://{self.host}:{self.port}", "SUCCESS")
|
|
121
|
+
|
|
122
|
+
def stop(self):
|
|
123
|
+
"""Stops the server."""
|
|
124
|
+
if self.server:
|
|
125
|
+
self.server.shutdown()
|
|
126
|
+
self.server.server_close()
|
|
127
|
+
self.is_running = False
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Feory Dynamic UI Widget Definitions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Dict, Any, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
class WidgetType(str, Enum):
|
|
10
|
+
BUTTON = "BUTTON"
|
|
11
|
+
SLIDER = "SLIDER"
|
|
12
|
+
TOGGLE = "TOGGLE"
|
|
13
|
+
TEXT_INPUT = "TEXT_INPUT"
|
|
14
|
+
METRIC_CARD = "METRIC_CARD"
|
|
15
|
+
DROPDOWN = "DROPDOWN"
|
|
16
|
+
HEADER = "HEADER"
|
|
17
|
+
|
|
18
|
+
class Widget:
|
|
19
|
+
"""Base class for all dynamic UI elements rendered in Feory Android app."""
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
id: str,
|
|
23
|
+
type: WidgetType,
|
|
24
|
+
label: str,
|
|
25
|
+
category: str = "general",
|
|
26
|
+
description: Optional[str] = None,
|
|
27
|
+
is_enabled: bool = True,
|
|
28
|
+
is_visible: bool = True,
|
|
29
|
+
icon: Optional[str] = None,
|
|
30
|
+
style: Optional[Dict[str, Any]] = None,
|
|
31
|
+
):
|
|
32
|
+
self.id = id
|
|
33
|
+
self.type = type
|
|
34
|
+
self.label = label
|
|
35
|
+
self.category = category
|
|
36
|
+
self.description = description
|
|
37
|
+
self.is_enabled = is_enabled
|
|
38
|
+
self.is_visible = is_visible
|
|
39
|
+
self.icon = icon
|
|
40
|
+
self.style = style or {}
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
43
|
+
data = {
|
|
44
|
+
"id": self.id,
|
|
45
|
+
"type": self.type.value if isinstance(self.type, WidgetType) else str(self.type),
|
|
46
|
+
"label": self.label,
|
|
47
|
+
"category": self.category,
|
|
48
|
+
"isEnabled": self.is_enabled,
|
|
49
|
+
"isVisible": self.is_visible,
|
|
50
|
+
}
|
|
51
|
+
if self.description:
|
|
52
|
+
data["description"] = self.description
|
|
53
|
+
if self.icon:
|
|
54
|
+
data["icon"] = self.icon
|
|
55
|
+
if self.style:
|
|
56
|
+
data["style"] = self.style
|
|
57
|
+
return data
|
|
58
|
+
|
|
59
|
+
class ButtonWidget(Widget):
|
|
60
|
+
"""Interactive Button Widget."""
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
id: str,
|
|
64
|
+
label: str,
|
|
65
|
+
category: str = "general",
|
|
66
|
+
action_id: Optional[str] = None,
|
|
67
|
+
color_scheme: str = "LILAC", # LILAC, GREEN, AMBER, ROSE, BLUE
|
|
68
|
+
variant: str = "FILLED", # FILLED, OUTLINED, TONAL
|
|
69
|
+
icon: Optional[str] = "play",
|
|
70
|
+
description: Optional[str] = None,
|
|
71
|
+
):
|
|
72
|
+
super().__init__(
|
|
73
|
+
id=id,
|
|
74
|
+
type=WidgetType.BUTTON,
|
|
75
|
+
label=label,
|
|
76
|
+
category=category,
|
|
77
|
+
description=description,
|
|
78
|
+
icon=icon,
|
|
79
|
+
style={"colorScheme": color_scheme, "variant": variant}
|
|
80
|
+
)
|
|
81
|
+
self.action_id = action_id or id
|
|
82
|
+
|
|
83
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
84
|
+
d = super().to_dict()
|
|
85
|
+
d["actionId"] = self.action_id
|
|
86
|
+
return d
|
|
87
|
+
|
|
88
|
+
class SliderWidget(Widget):
|
|
89
|
+
"""Continuous or Discrete Range Slider Widget."""
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
id: str,
|
|
93
|
+
label: str,
|
|
94
|
+
category: str = "general",
|
|
95
|
+
min_value: float = 0.0,
|
|
96
|
+
max_value: float = 100.0,
|
|
97
|
+
current_value: float = 50.0,
|
|
98
|
+
step: Optional[float] = None,
|
|
99
|
+
unit: Optional[str] = None,
|
|
100
|
+
description: Optional[str] = None,
|
|
101
|
+
):
|
|
102
|
+
super().__init__(
|
|
103
|
+
id=id,
|
|
104
|
+
type=WidgetType.SLIDER,
|
|
105
|
+
label=label,
|
|
106
|
+
category=category,
|
|
107
|
+
description=description,
|
|
108
|
+
icon="tune"
|
|
109
|
+
)
|
|
110
|
+
self.min_value = min_value
|
|
111
|
+
self.max_value = max_value
|
|
112
|
+
self.current_value = current_value
|
|
113
|
+
self.step = step
|
|
114
|
+
self.unit = unit
|
|
115
|
+
|
|
116
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
117
|
+
d = super().to_dict()
|
|
118
|
+
d["minValue"] = self.min_value
|
|
119
|
+
d["maxValue"] = self.max_value
|
|
120
|
+
d["currentValue"] = self.current_value
|
|
121
|
+
if self.step is not None:
|
|
122
|
+
d["step"] = self.step
|
|
123
|
+
if self.unit:
|
|
124
|
+
d["unit"] = self.unit
|
|
125
|
+
return d
|
|
126
|
+
|
|
127
|
+
class ToggleWidget(Widget):
|
|
128
|
+
"""Switch Toggle Widget."""
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
id: str,
|
|
132
|
+
label: str,
|
|
133
|
+
category: str = "general",
|
|
134
|
+
is_checked: bool = False,
|
|
135
|
+
description: Optional[str] = None,
|
|
136
|
+
):
|
|
137
|
+
super().__init__(
|
|
138
|
+
id=id,
|
|
139
|
+
type=WidgetType.TOGGLE,
|
|
140
|
+
label=label,
|
|
141
|
+
category=category,
|
|
142
|
+
description=description,
|
|
143
|
+
icon="toggle_on"
|
|
144
|
+
)
|
|
145
|
+
self.is_checked = is_checked
|
|
146
|
+
|
|
147
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
148
|
+
d = super().to_dict()
|
|
149
|
+
d["isChecked"] = self.is_checked
|
|
150
|
+
return d
|
|
151
|
+
|
|
152
|
+
class TextFieldWidget(Widget):
|
|
153
|
+
"""Text Input Field Widget."""
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
id: str,
|
|
157
|
+
label: str,
|
|
158
|
+
category: str = "general",
|
|
159
|
+
placeholder: str = "",
|
|
160
|
+
current_value: str = "",
|
|
161
|
+
is_secret: bool = False,
|
|
162
|
+
description: Optional[str] = None,
|
|
163
|
+
):
|
|
164
|
+
super().__init__(
|
|
165
|
+
id=id,
|
|
166
|
+
type=WidgetType.TEXT_INPUT,
|
|
167
|
+
label=label,
|
|
168
|
+
category=category,
|
|
169
|
+
description=description,
|
|
170
|
+
icon="edit"
|
|
171
|
+
)
|
|
172
|
+
self.placeholder = placeholder
|
|
173
|
+
self.current_value = current_value
|
|
174
|
+
self.is_secret = is_secret
|
|
175
|
+
|
|
176
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
177
|
+
d = super().to_dict()
|
|
178
|
+
d["placeholder"] = self.placeholder
|
|
179
|
+
d["currentValue"] = self.current_value
|
|
180
|
+
d["isSecret"] = self.is_secret
|
|
181
|
+
return d
|
|
182
|
+
|
|
183
|
+
class MetricWidget(Widget):
|
|
184
|
+
"""Bento-style Metric / Stat Card Widget."""
|
|
185
|
+
def __init__(
|
|
186
|
+
self,
|
|
187
|
+
id: str,
|
|
188
|
+
title: str,
|
|
189
|
+
value: Union[str, int, float],
|
|
190
|
+
subtitle: Optional[str] = None,
|
|
191
|
+
category: str = "metrics",
|
|
192
|
+
badge: Optional[str] = None,
|
|
193
|
+
accent_color: str = "LILAC",
|
|
194
|
+
icon: Optional[str] = "trending_up",
|
|
195
|
+
):
|
|
196
|
+
super().__init__(
|
|
197
|
+
id=id,
|
|
198
|
+
type=WidgetType.METRIC_CARD,
|
|
199
|
+
label=title,
|
|
200
|
+
category=category,
|
|
201
|
+
icon=icon,
|
|
202
|
+
style={"accentColor": accent_color}
|
|
203
|
+
)
|
|
204
|
+
self.value = str(value)
|
|
205
|
+
self.subtitle = subtitle
|
|
206
|
+
self.badge = badge
|
|
207
|
+
|
|
208
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
209
|
+
d = super().to_dict()
|
|
210
|
+
d["metricValue"] = self.value
|
|
211
|
+
if self.subtitle:
|
|
212
|
+
d["subtitle"] = self.subtitle
|
|
213
|
+
if self.badge:
|
|
214
|
+
d["badge"] = self.badge
|
|
215
|
+
return d
|
|
216
|
+
|
|
217
|
+
class DropdownWidget(Widget):
|
|
218
|
+
"""Selection Dropdown Widget."""
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
id: str,
|
|
222
|
+
label: str,
|
|
223
|
+
options: List[str],
|
|
224
|
+
selected_option: Optional[str] = None,
|
|
225
|
+
category: str = "general",
|
|
226
|
+
description: Optional[str] = None,
|
|
227
|
+
):
|
|
228
|
+
super().__init__(
|
|
229
|
+
id=id,
|
|
230
|
+
type=WidgetType.DROPDOWN,
|
|
231
|
+
label=label,
|
|
232
|
+
category=category,
|
|
233
|
+
description=description,
|
|
234
|
+
icon="list"
|
|
235
|
+
)
|
|
236
|
+
self.options = options
|
|
237
|
+
self.selected_option = selected_option or (options[0] if options else "")
|
|
238
|
+
|
|
239
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
240
|
+
d = super().to_dict()
|
|
241
|
+
d["options"] = self.options
|
|
242
|
+
d["selectedOption"] = self.selected_option
|
|
243
|
+
return d
|
|
244
|
+
|
|
245
|
+
class CategorySection:
|
|
246
|
+
"""Logical grouping of widgets in Feory App."""
|
|
247
|
+
def __init__(self, id: str, title: str, description: Optional[str] = None, icon: Optional[str] = None):
|
|
248
|
+
self.id = id
|
|
249
|
+
self.title = title
|
|
250
|
+
self.description = description
|
|
251
|
+
self.icon = icon
|
|
252
|
+
|
|
253
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
254
|
+
d = {"id": self.id, "title": self.title}
|
|
255
|
+
if self.description:
|
|
256
|
+
d["description"] = self.description
|
|
257
|
+
if self.icon:
|
|
258
|
+
d["icon"] = self.icon
|
|
259
|
+
return d
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: feory
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal Python SDK for Feory Android App - Dynamic UI Builder & Bot Control Bridge
|
|
5
|
+
Home-page: https://github.com/zizo0/feory
|
|
6
|
+
Author: zizo0
|
|
7
|
+
Author-email: zizo0 <zly30257@gmail.com>
|
|
8
|
+
Project-URL: Homepage, https://github.com/zizo0/feory
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/zizo0/feory/issues
|
|
10
|
+
Keywords: feory,android,bot,ui-builder,automation,telegram,bridge
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
15
|
+
Classifier: Topic :: Communications
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
|
|
22
|
+
# Feory Python SDK ⚡
|
|
23
|
+
|
|
24
|
+
Universal Python SDK & Bridge for the **Feory Android App**.
|
|
25
|
+
Create dynamic Bento-style Android UIs, manage bots, handle bi-directional events, and control servers remotely from your mobile device.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🚀 Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install feory
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 🛠️ Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import feory
|
|
41
|
+
|
|
42
|
+
# 1. Initialize with your bot token from the Feory App
|
|
43
|
+
client = feory.Client(
|
|
44
|
+
token="feory_tok_core_829a1b",
|
|
45
|
+
bot_name="My Awesome Python Bot",
|
|
46
|
+
port=8765
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# 2. Register Dynamic UI Widgets
|
|
50
|
+
client.register_button(
|
|
51
|
+
id="btn_start_scrape",
|
|
52
|
+
label="Start Data Pipeline",
|
|
53
|
+
category="general",
|
|
54
|
+
color_scheme="LILAC",
|
|
55
|
+
icon="play"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
client.register_slider(
|
|
59
|
+
id="slider_threads",
|
|
60
|
+
label="Worker Threads",
|
|
61
|
+
min_value=1,
|
|
62
|
+
max_value=32,
|
|
63
|
+
default_value=8
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
client.register_metric_card(
|
|
67
|
+
id="stat_processed",
|
|
68
|
+
title="Items Processed",
|
|
69
|
+
value="12,450",
|
|
70
|
+
badge="+14% today",
|
|
71
|
+
accent_color="GREEN"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# 3. Handle Events from Feory Android App
|
|
75
|
+
@client.on_action("btn_start_scrape")
|
|
76
|
+
def handle_scrape(payload):
|
|
77
|
+
client.log("Starting scraping tasks...", "INFO")
|
|
78
|
+
# Your automation logic here
|
|
79
|
+
return {"status": "started", "items": 100}
|
|
80
|
+
|
|
81
|
+
@client.on_value_change("slider_threads")
|
|
82
|
+
def handle_thread_change(new_val):
|
|
83
|
+
client.log(f"Thread pool updated to: {new_val}", "INFO")
|
|
84
|
+
|
|
85
|
+
# 4. Keep your bot server running
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
client.run_forever()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 📖 Features
|
|
93
|
+
|
|
94
|
+
- **Dynamic Android UI Generation**: Inject buttons, sliders, toggles, text inputs, dropdowns, and Bento metric cards on the fly without touching Kotlin code.
|
|
95
|
+
- **Two-Way Event Bus**: Receive instant button clicks and slider callbacks directly in your Python functions.
|
|
96
|
+
- **Real-Time Telemetry & Terminal Logs**: Stream live console logs directly to the Feory App terminal.
|
|
97
|
+
- **Resource Lock & Collision Prevention**: Coordinate multiple bots accessing the same channels or resources with built-in Mutex locks.
|
|
98
|
+
- **Zero-Dependency Core**: Uses Python standard library by default for maximum portability.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 🔒 Authentication
|
|
103
|
+
|
|
104
|
+
Every bot connects securely using its unique **Feory Token** (`feory_tok_...`). You can create, manage, and copy tokens directly inside the **Tokens** screen of the Feory Android App.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 👤 Author
|
|
109
|
+
|
|
110
|
+
Developed by **zizo0**
|
|
111
|
+
Email: `zly30257@gmail.com`
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
feory/__init__.py
|
|
5
|
+
feory/client.py
|
|
6
|
+
feory/exceptions.py
|
|
7
|
+
feory/server.py
|
|
8
|
+
feory/widgets.py
|
|
9
|
+
feory.egg-info/PKG-INFO
|
|
10
|
+
feory.egg-info/SOURCES.txt
|
|
11
|
+
feory.egg-info/dependency_links.txt
|
|
12
|
+
feory.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
feory
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "feory"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="zizo0", email="zly30257@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "Universal Python SDK for Feory Android App - Dynamic UI Builder & Bot Control Bridge"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Software Development :: User Interfaces",
|
|
19
|
+
"Topic :: Communications",
|
|
20
|
+
]
|
|
21
|
+
keywords = ["feory", "android", "bot", "ui-builder", "automation", "telegram", "bridge"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
"Homepage" = "https://github.com/zizo0/feory"
|
|
25
|
+
"Bug Tracker" = "https://github.com/zizo0/feory/issues"
|
feory-0.1.0/setup.cfg
ADDED
feory-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="feory",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
author="zizo0",
|
|
7
|
+
author_email="zly30257@gmail.com",
|
|
8
|
+
description="Universal Python SDK for Feory Android App - Dynamic UI Builder & Bot Control Bridge",
|
|
9
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
10
|
+
long_description_content_type="text/markdown",
|
|
11
|
+
url="https://github.com/zizo0/feory",
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
classifiers=[
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
],
|
|
18
|
+
python_requires=">=3.8",
|
|
19
|
+
)
|