tflows 0.0.7__tar.gz → 0.0.9.dev0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tflows
3
- Version: 0.0.7
3
+ Version: 0.0.9.dev0
4
4
  Summary: A lightweight automation and Discord bot framework that lets you build bots using a simple scripting system.
5
5
  Author: Tonie
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tflows"
7
- version = "0.0.7"
7
+ version = "0.0.9.dev0"
8
8
  description = "A lightweight automation and Discord bot framework that lets you build bots using a simple scripting system."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,220 @@
1
+ import re
2
+ import asyncio
3
+
4
+ # -----------------------
5
+ # LOG PATTERN (SAFE)
6
+ # -----------------------
7
+ LOG_PATTERN = re.compile(r"\$log\((\d+)\)\{([\s\S]*?)\}")
8
+
9
+
10
+ class Engine:
11
+ def __init__(self, registry):
12
+ self.registry = registry
13
+
14
+ # -----------------------
15
+ # VARIABLES
16
+ # -----------------------
17
+ async def replace_vars(self, ctx, text: str):
18
+
19
+ if "$ping" in text:
20
+ try:
21
+ latency = ctx._state._get_client().latency * 1000
22
+ text = text.replace("$ping", f"{latency:.2f}ms")
23
+ except:
24
+ pass
25
+
26
+ async def var_replacer(match):
27
+ name = match.group(1)
28
+ args = match.group(2) or ""
29
+
30
+ handler = self.registry.get_var(name)
31
+ if handler:
32
+ result = handler(ctx, args)
33
+
34
+ if asyncio.iscoroutine(result):
35
+ result = await result
36
+
37
+ return str(result)
38
+
39
+ return match.group(0)
40
+
41
+ pattern = r"\$(\w+)(?:\((.*?)\))?"
42
+ matches = list(re.finditer(pattern, text, re.DOTALL))
43
+
44
+ for match in reversed(matches):
45
+ try:
46
+ replacement = await var_replacer(match)
47
+ start, end = match.span()
48
+ text = text[:start] + replacement + text[end:]
49
+ except:
50
+ continue
51
+
52
+ return text
53
+
54
+ # -----------------------
55
+ # SAFE LOG SYSTEM (QUEUE STYLE, NON-BLOCKING)
56
+ # -----------------------
57
+ async def handle_log(self, ctx, text: str):
58
+ matches = list(LOG_PATTERN.finditer(text))
59
+ if not matches:
60
+ return text
61
+
62
+ bot = getattr(ctx, "bot", None) or getattr(self.registry, "bot", None)
63
+
64
+ if bot is None:
65
+ print("[tflow] ERROR: bot not found")
66
+ return text
67
+
68
+ for match in matches:
69
+ channel_id = match.group(1)
70
+ message = match.group(2)
71
+
72
+ channel = bot.get_channel(int(channel_id))
73
+
74
+ if channel is None:
75
+ try:
76
+ channel = await bot.fetch_channel(int(channel_id))
77
+ except:
78
+ continue
79
+
80
+ if channel is None:
81
+ continue
82
+
83
+ try:
84
+ message = await self.replace_vars(ctx, message)
85
+ except:
86
+ pass
87
+
88
+ try:
89
+ await channel.send(message)
90
+ print(f"[tflow] log sent -> {channel_id}")
91
+ except Exception as e:
92
+ print("[tflow] send failed:", e)
93
+
94
+ text = text.replace(match.group(0), "")
95
+
96
+ return text.strip()
97
+
98
+ # -----------------------
99
+ # EMBED PARSER
100
+ # -----------------------
101
+ async def parse_embed(self, ctx, block: str):
102
+
103
+ import discord
104
+
105
+ e = discord.Embed()
106
+ block = block.replace("\r\n", "\n")
107
+
108
+ def grab(key):
109
+ m = re.search(rf"\${key}\[(.*?)\]", block, re.DOTALL)
110
+ return m.group(1).strip() if m else None
111
+
112
+ title = grab("title")
113
+ desc = grab("desc")
114
+ footer = grab("footer")
115
+ color = grab("color")
116
+
117
+ clean = re.sub(
118
+ r"\$(title|desc|footer|color)\[.*?\]",
119
+ "",
120
+ block,
121
+ flags=re.DOTALL
122
+ ).strip()
123
+
124
+ async def apply(v):
125
+ if not v:
126
+ return None
127
+ return await self.replace_vars(ctx, v)
128
+
129
+ if title:
130
+ e.title = await apply(title)
131
+
132
+ full_desc = desc if desc else clean
133
+ e.description = await self.replace_vars(ctx, full_desc or "No content")
134
+
135
+ if footer:
136
+ e.set_footer(text=await apply(footer))
137
+
138
+ if color:
139
+ try:
140
+ c = color.replace("#", "").lower()
141
+ e.color = discord.Color.white() if c == "white" else discord.Color(int(c, 16))
142
+ except:
143
+ pass
144
+
145
+ await ctx.channel.send(embed=e)
146
+
147
+ # -----------------------
148
+ # MAIN ENGINE (FULLY STABLE FLOW)
149
+ # -----------------------
150
+ async def run(self, ctx, code: str):
151
+
152
+ lines = code.split("\n")
153
+ i = 0
154
+
155
+ while i < len(lines):
156
+ line = lines[i].strip()
157
+
158
+ i += 1 # prevent infinite loop edge cases early
159
+
160
+ if not line:
161
+ continue
162
+
163
+ # -----------------------
164
+ # EMBED BLOCK
165
+ # -----------------------
166
+ if line == "embed":
167
+ block = []
168
+
169
+ while i < len(lines):
170
+ if lines[i].strip() == "endembed":
171
+ i += 1
172
+ break
173
+ block.append(lines[i])
174
+ i += 1
175
+
176
+ await self.parse_embed(ctx, "\n".join(block))
177
+ continue
178
+
179
+ # -----------------------
180
+ # STEP 1: VARIABLES
181
+ # -----------------------
182
+ try:
183
+ line = await self.replace_vars(ctx, line)
184
+ except:
185
+ continue
186
+
187
+ # -----------------------
188
+ # STEP 2: LOG SYSTEM (SAFE SIDE EFFECT)
189
+ # -----------------------
190
+ try:
191
+ line = await self.handle_log(ctx, line)
192
+ except:
193
+ pass
194
+
195
+ # IMPORTANT: skip empty results (THIS fixes your crash)
196
+ if not line or not line.strip():
197
+ continue
198
+
199
+ # -----------------------
200
+ # STEP 3: REGISTRY FUNCTIONS
201
+ # -----------------------
202
+ parts = line.split(" ", 1)
203
+ name = parts[0].strip()
204
+ args = parts[1] if len(parts) > 1 else ""
205
+
206
+ if not name:
207
+ continue
208
+
209
+ func = self.registry.get(name)
210
+
211
+ if func:
212
+ try:
213
+ result = func(ctx, args)
214
+
215
+ if asyncio.iscoroutine(result):
216
+ await result
217
+ except Exception as e:
218
+ print(f"[tflow] function error: {name} -> {e}")
219
+ else:
220
+ print(f"[tflow] Unknown function: {name}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tflows
3
- Version: 0.0.7
3
+ Version: 0.0.9.dev0
4
4
  Summary: A lightweight automation and Discord bot framework that lets you build bots using a simple scripting system.
5
5
  Author: Tonie
6
6
  License: MIT
@@ -1,172 +0,0 @@
1
- import re
2
- import asyncio
3
-
4
- class Engine:
5
- def __init__(self, registry):
6
- self.registry = registry
7
-
8
- # -----------------------
9
- # VARIABLES (FIXED)
10
- # -----------------------
11
- async def replace_vars(self, ctx, text: str):
12
-
13
- # $ping (keep simple)
14
- if "$ping" in text:
15
- latency = ctx._state._get_client().latency * 1000
16
- text = text.replace("$ping", f"{latency:.2f}ms")
17
-
18
- async def var_replacer(match):
19
- name = match.group(1)
20
- args = match.group(2) or ""
21
-
22
- handler = self.registry.get_var(name)
23
- if handler:
24
- result = handler(ctx, args)
25
-
26
- # ✅ SUPPORT ASYNC NOW
27
- if asyncio.iscoroutine(result):
28
- result = await result
29
-
30
- return str(result)
31
-
32
- return match.group(0)
33
-
34
- # IMPORTANT: async replace loop
35
- pattern = r"\$(\w+)(?:\((.*?)\))?"
36
- matches = list(re.finditer(pattern, text, re.DOTALL))
37
-
38
- for match in reversed(matches):
39
- replacement = await var_replacer(match)
40
- start, end = match.span()
41
- text = text[:start] + replacement + text[end:]
42
-
43
- return text
44
-
45
- # -----------------------
46
- # EMBED PARSER
47
- # -----------------------
48
- async def parse_embed(self, ctx, block: str):
49
-
50
- import discord
51
- import re
52
-
53
- e = discord.Embed()
54
- block = block.replace("\r\n", "\n")
55
-
56
- # -----------------------
57
- # EXTRACT HELPERS
58
- # -----------------------
59
- def grab(key):
60
- m = re.search(rf"\${key}\[(.*?)\]", block, re.DOTALL)
61
- return m.group(1).strip() if m else None
62
-
63
- title = grab("title")
64
- desc = grab("desc")
65
- footer = grab("footer")
66
- color = grab("color")
67
-
68
- clean = re.sub(
69
- r"\$(title|desc|footer|color)\[.*?\]",
70
- "",
71
- block,
72
- flags=re.DOTALL
73
- ).strip()
74
-
75
- # -----------------------
76
- # SAFE APPLY (ALWAYS AWAIT VAR ENGINE)
77
- # -----------------------
78
- async def apply(v):
79
- if not v:
80
- return None
81
- return await self.replace_vars(ctx, v)
82
-
83
- # -----------------------
84
- # TITLE
85
- # -----------------------
86
- if title:
87
- e.title = await apply(title)
88
-
89
- # -----------------------
90
- # DESCRIPTION
91
- # -----------------------
92
- full_desc = desc if desc else clean
93
- e.description = await self.replace_vars(ctx, full_desc or "No content")
94
-
95
- # -----------------------
96
- # FOOTER
97
- # -----------------------
98
- if footer:
99
- e.set_footer(text=await apply(footer))
100
-
101
- # -----------------------
102
- # COLOR
103
- # -----------------------
104
- if color:
105
- try:
106
- c = color.replace("#", "").lower()
107
-
108
- if c == "white":
109
- e.color = discord.Color.white()
110
- else:
111
- e.color = discord.Color(int(c, 16))
112
-
113
- except Exception:
114
- pass
115
-
116
- # -----------------------
117
- # SEND
118
- # -----------------------
119
- await ctx.channel.send(embed=e)
120
-
121
- # -----------------------
122
- # MAIN ENGINE
123
- # -----------------------
124
- async def run(self, ctx, code: str):
125
-
126
- lines = code.split("\n")
127
- i = 0
128
-
129
- while i < len(lines):
130
- line = lines[i].strip()
131
-
132
- if not line:
133
- i += 1
134
- continue
135
-
136
- # -----------------------
137
- # EMBED BLOCK
138
- # -----------------------
139
- if line == "embed":
140
- i += 1
141
- block = []
142
-
143
- while i < len(lines):
144
- if lines[i].strip() == "endembed":
145
- break
146
- block.append(lines[i])
147
- i += 1
148
-
149
- await self.parse_embed(ctx, "\n".join(block))
150
- i += 1
151
- continue
152
-
153
- # -----------------------
154
- # NORMAL FUNCTIONS
155
- # -----------------------
156
- line = await self.replace_vars(ctx, line)
157
-
158
- parts = line.split(" ", 1)
159
- name = parts[0]
160
- args = parts[1] if len(parts) > 1 else ""
161
-
162
- func = self.registry.get(name)
163
-
164
- if func:
165
- result = func(ctx, args)
166
-
167
- if asyncio.iscoroutine(result):
168
- await result
169
- else:
170
- print(f"[tflow] Unknown function: {name}")
171
-
172
- i += 1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes