smart-bot-factory 0.3.7__py3-none-any.whl → 0.3.8__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.

Potentially problematic release.


This version of smart-bot-factory might be problematic. Click here for more details.

Files changed (45) hide show
  1. smart_bot_factory/admin/__init__.py +7 -7
  2. smart_bot_factory/admin/admin_events.py +483 -383
  3. smart_bot_factory/admin/admin_logic.py +234 -158
  4. smart_bot_factory/admin/admin_manager.py +68 -53
  5. smart_bot_factory/admin/admin_tester.py +46 -40
  6. smart_bot_factory/admin/timeout_checker.py +201 -153
  7. smart_bot_factory/aiogram_calendar/__init__.py +11 -3
  8. smart_bot_factory/aiogram_calendar/common.py +12 -18
  9. smart_bot_factory/aiogram_calendar/dialog_calendar.py +126 -64
  10. smart_bot_factory/aiogram_calendar/schemas.py +49 -28
  11. smart_bot_factory/aiogram_calendar/simple_calendar.py +94 -50
  12. smart_bot_factory/analytics/analytics_manager.py +414 -392
  13. smart_bot_factory/cli.py +204 -148
  14. smart_bot_factory/config.py +123 -102
  15. smart_bot_factory/core/bot_utils.py +474 -332
  16. smart_bot_factory/core/conversation_manager.py +287 -200
  17. smart_bot_factory/core/decorators.py +1129 -749
  18. smart_bot_factory/core/message_sender.py +287 -266
  19. smart_bot_factory/core/router.py +170 -100
  20. smart_bot_factory/core/router_manager.py +121 -83
  21. smart_bot_factory/core/states.py +4 -3
  22. smart_bot_factory/creation/__init__.py +1 -1
  23. smart_bot_factory/creation/bot_builder.py +320 -242
  24. smart_bot_factory/creation/bot_testing.py +440 -365
  25. smart_bot_factory/dashboard/__init__.py +1 -3
  26. smart_bot_factory/event/__init__.py +2 -7
  27. smart_bot_factory/handlers/handlers.py +676 -472
  28. smart_bot_factory/integrations/openai_client.py +218 -168
  29. smart_bot_factory/integrations/supabase_client.py +928 -637
  30. smart_bot_factory/message/__init__.py +18 -22
  31. smart_bot_factory/router/__init__.py +2 -2
  32. smart_bot_factory/setup_checker.py +162 -126
  33. smart_bot_factory/supabase/__init__.py +1 -1
  34. smart_bot_factory/supabase/client.py +631 -515
  35. smart_bot_factory/utils/__init__.py +2 -3
  36. smart_bot_factory/utils/debug_routing.py +38 -27
  37. smart_bot_factory/utils/prompt_loader.py +153 -120
  38. smart_bot_factory/utils/user_prompt_loader.py +55 -56
  39. smart_bot_factory/utm_link_generator.py +123 -116
  40. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/METADATA +3 -1
  41. smart_bot_factory-0.3.8.dist-info/RECORD +59 -0
  42. smart_bot_factory-0.3.7.dist-info/RECORD +0 -59
  43. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/WHEEL +0 -0
  44. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/entry_points.txt +0 -0
  45. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,14 @@
1
1
  from .common import get_user_locale
2
- from .simple_calendar import SimpleCalendar
3
2
  from .dialog_calendar import DialogCalendar
4
- from .schemas import SimpleCalendarCallback, DialogCalendarCallback, CalendarLabels
3
+ from .schemas import (CalendarLabels, DialogCalendarCallback,
4
+ SimpleCalendarCallback)
5
+ from .simple_calendar import SimpleCalendar
5
6
 
6
- __all__ = ['SimpleCalendar', 'DialogCalendar', 'SimpleCalendarCallback', 'DialogCalendarCallback', 'CalendarLabels', 'get_user_locale']
7
+ __all__ = [
8
+ "SimpleCalendar",
9
+ "DialogCalendar",
10
+ "SimpleCalendarCallback",
11
+ "DialogCalendarCallback",
12
+ "CalendarLabels",
13
+ "get_user_locale",
14
+ ]
@@ -1,41 +1,35 @@
1
1
  import calendar
2
2
  import locale
3
+ from datetime import datetime
3
4
 
4
5
  from aiogram.types import User
5
- from datetime import datetime
6
6
 
7
7
  from .schemas import CalendarLabels
8
8
 
9
9
 
10
10
  async def get_user_locale(from_user: User) -> str:
11
- "Returns user locale in format en_US, accepts User instance from Message, CallbackData etc"
12
- loc = from_user.language_code
13
- return locale.locale_alias[loc].split(".")[0]
11
+ "Always returns ru_RU locale for consistent Russian language support"
12
+ return "ru_RU"
14
13
 
15
14
 
16
15
  class GenericCalendar:
17
16
 
18
17
  def __init__(
19
18
  self,
20
- locale: str = None,
19
+ locale: str = None, # оставляем для обратной совместимости
21
20
  cancel_btn: str = None,
22
21
  today_btn: str = None,
23
- show_alerts: bool = False
22
+ show_alerts: bool = False,
24
23
  ) -> None:
25
- """Pass labels if you need to have alternative language of buttons
24
+ """Initialize calendar with Russian language by default
26
25
 
27
26
  Parameters:
28
- locale (str): Locale calendar must have captions in (in format uk_UA), if None - default English will be used
27
+ locale (str): Ignored, always uses Russian
29
28
  cancel_btn (str): label for button Cancel to cancel date input
30
29
  today_btn (str): label for button Today to set calendar back to todays date
31
30
  show_alerts (bool): defines how the date range error would shown (defaults to False)
32
31
  """
33
32
  self._labels = CalendarLabels()
34
- if locale:
35
- # getting month names and days of week in specified locale
36
- with calendar.different_locale(locale):
37
- self._labels.days_of_week = list(calendar.day_abbr)
38
- self._labels.months = calendar.month_abbr[1:]
39
33
 
40
34
  if cancel_btn:
41
35
  self._labels.cancel_caption = cancel_btn
@@ -56,15 +50,15 @@ class GenericCalendar:
56
50
  date = datetime(int(data.year), int(data.month), int(data.day))
57
51
  if self.min_date and self.min_date > date:
58
52
  await query.answer(
59
- f'The date have to be later {self.min_date.strftime("%d/%m/%Y")}',
60
- show_alert=self.show_alerts
53
+ f'Дата должна быть позже {self.min_date.strftime("%d/%m/%Y")}',
54
+ show_alert=self.show_alerts,
61
55
  )
62
56
  return False, None
63
57
  elif self.max_date and self.max_date < date:
64
58
  await query.answer(
65
- f'The date have to be before {self.max_date.strftime("%d/%m/%Y")}',
66
- show_alert=self.show_alerts
59
+ f'Дата должна быть раньше {self.max_date.strftime("%d/%m/%Y")}',
60
+ show_alert=self.show_alerts,
67
61
  )
68
62
  return False, None
69
63
  await query.message.delete_reply_markup() # removing inline keyboard
70
- return True, date
64
+ return True, date
@@ -1,16 +1,19 @@
1
1
  import calendar
2
2
  from datetime import datetime
3
3
 
4
- from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
5
- from aiogram.types import CallbackQuery
4
+ from aiogram.types import (CallbackQuery, InlineKeyboardButton,
5
+ InlineKeyboardMarkup)
6
6
 
7
- from .schemas import DialogCalendarCallback, DialogCalAct, highlight, superscript
8
7
  from .common import GenericCalendar
8
+ from .schemas import (DialogCalAct, DialogCalendarCallback, highlight,
9
+ superscript)
9
10
 
10
11
 
11
12
  class DialogCalendar(GenericCalendar):
12
13
 
13
- ignore_callback = DialogCalendarCallback(act=DialogCalAct.ignore).pack() # placeholder for no answer buttons
14
+ ignore_callback = DialogCalendarCallback(
15
+ act=DialogCalAct.ignore
16
+ ).pack() # placeholder for no answer buttons
14
17
 
15
18
  async def _get_month_kb(self, year: int):
16
19
  """Creates an inline keyboard with months for specified year"""
@@ -25,14 +28,22 @@ class DialogCalendar(GenericCalendar):
25
28
  years_row.append(
26
29
  InlineKeyboardButton(
27
30
  text=self._labels.cancel_caption,
28
- callback_data=DialogCalendarCallback(act=DialogCalAct.cancel, year=year, month=1, day=1).pack()
31
+ callback_data=DialogCalendarCallback(
32
+ act=DialogCalAct.cancel, year=year, month=1, day=1
33
+ ).pack(),
29
34
  )
30
35
  )
31
- years_row.append(InlineKeyboardButton(
32
- text=str(year) if year != today.year else highlight(year),
33
- callback_data=DialogCalendarCallback(act=DialogCalAct.start, year=year, month=-1, day=-1).pack()
34
- ))
35
- years_row.append(InlineKeyboardButton(text=" ", callback_data=self.ignore_callback))
36
+ years_row.append(
37
+ InlineKeyboardButton(
38
+ text=str(year) if year != today.year else highlight(year),
39
+ callback_data=DialogCalendarCallback(
40
+ act=DialogCalAct.start, year=year, month=-1, day=-1
41
+ ).pack(),
42
+ )
43
+ )
44
+ years_row.append(
45
+ InlineKeyboardButton(text=" ", callback_data=self.ignore_callback)
46
+ )
36
47
  kb.append(years_row)
37
48
  # two rows with 6 months buttons
38
49
  month6_row = []
@@ -44,21 +55,25 @@ class DialogCalendar(GenericCalendar):
44
55
  return month_str
45
56
 
46
57
  for month in range(1, 7):
47
- month6_row.append(InlineKeyboardButton(
48
- text=highlight_month(),
49
- callback_data=DialogCalendarCallback(
50
- act=DialogCalAct.set_m, year=year, month=month, day=-1
51
- ).pack()
52
- ))
58
+ month6_row.append(
59
+ InlineKeyboardButton(
60
+ text=highlight_month(),
61
+ callback_data=DialogCalendarCallback(
62
+ act=DialogCalAct.set_m, year=year, month=month, day=-1
63
+ ).pack(),
64
+ )
65
+ )
53
66
  month12_row = []
54
67
 
55
68
  for month in range(7, 13):
56
- month12_row.append(InlineKeyboardButton(
57
- text=highlight_month(),
58
- callback_data=DialogCalendarCallback(
59
- act=DialogCalAct.set_m, year=year, month=month, day=-1
60
- ).pack()
61
- ))
69
+ month12_row.append(
70
+ InlineKeyboardButton(
71
+ text=highlight_month(),
72
+ callback_data=DialogCalendarCallback(
73
+ act=DialogCalAct.set_m, year=year, month=month, day=-1
74
+ ).pack(),
75
+ )
76
+ )
62
77
 
63
78
  kb.append(month6_row)
64
79
  kb.append(month12_row)
@@ -101,23 +116,36 @@ class DialogCalendar(GenericCalendar):
101
116
  nav_row.append(
102
117
  InlineKeyboardButton(
103
118
  text=self._labels.cancel_caption,
104
- callback_data=DialogCalendarCallback(act=DialogCalAct.cancel, year=year, month=1, day=1).pack()
119
+ callback_data=DialogCalendarCallback(
120
+ act=DialogCalAct.cancel, year=year, month=1, day=1
121
+ ).pack(),
122
+ )
123
+ )
124
+ nav_row.append(
125
+ InlineKeyboardButton(
126
+ text=str(year) if year != now_year else highlight(year),
127
+ callback_data=DialogCalendarCallback(
128
+ act=DialogCalAct.start, year=year, month=-1, day=-1
129
+ ).pack(),
130
+ )
131
+ )
132
+ nav_row.append(
133
+ InlineKeyboardButton(
134
+ text=highlight_month(),
135
+ callback_data=DialogCalendarCallback(
136
+ act=DialogCalAct.set_y, year=year, month=-1, day=-1
137
+ ).pack(),
105
138
  )
106
139
  )
107
- nav_row.append(InlineKeyboardButton(
108
- text=str(year) if year != now_year else highlight(year),
109
- callback_data=DialogCalendarCallback(act=DialogCalAct.start, year=year, month=-1, day=-1).pack()
110
- ))
111
- nav_row.append(InlineKeyboardButton(
112
- text=highlight_month(),
113
- callback_data=DialogCalendarCallback(act=DialogCalAct.set_y, year=year, month=-1, day=-1).pack()
114
- ))
115
140
  kb.append(nav_row)
116
141
 
117
142
  week_days_labels_row = []
118
143
  for weekday in self._labels.days_of_week:
119
- week_days_labels_row.append(InlineKeyboardButton(
120
- text=highlight_weekday(), callback_data=self.ignore_callback))
144
+ week_days_labels_row.append(
145
+ InlineKeyboardButton(
146
+ text=highlight_weekday(), callback_data=self.ignore_callback
147
+ )
148
+ )
121
149
  kb.append(week_days_labels_row)
122
150
 
123
151
  month_calendar = calendar.monthcalendar(year, month)
@@ -126,19 +154,25 @@ class DialogCalendar(GenericCalendar):
126
154
  days_row = []
127
155
  for day in week:
128
156
  if day == 0:
129
- days_row.append(InlineKeyboardButton(text=" ", callback_data=self.ignore_callback))
157
+ days_row.append(
158
+ InlineKeyboardButton(
159
+ text=" ", callback_data=self.ignore_callback
160
+ )
161
+ )
130
162
  continue
131
- days_row.append(InlineKeyboardButton(
132
- text=highlight_day(),
133
- callback_data=DialogCalendarCallback(act=DialogCalAct.day, year=year, month=month, day=day).pack()
134
- ))
163
+ days_row.append(
164
+ InlineKeyboardButton(
165
+ text=highlight_day(),
166
+ callback_data=DialogCalendarCallback(
167
+ act=DialogCalAct.day, year=year, month=month, day=day
168
+ ).pack(),
169
+ )
170
+ )
135
171
  kb.append(days_row)
136
172
  return InlineKeyboardMarkup(row_width=7, inline_keyboard=kb)
137
173
 
138
174
  async def start_calendar(
139
- self,
140
- year: int = datetime.now().year,
141
- month: int = None
175
+ self, year: int = datetime.now().year, month: int = None
142
176
  ) -> InlineKeyboardMarkup:
143
177
  today = datetime.now()
144
178
  now_year = today.year
@@ -150,48 +184,76 @@ class DialogCalendar(GenericCalendar):
150
184
  # first row - years
151
185
  years_row = []
152
186
  for value in range(year - 2, year + 3):
153
- years_row.append(InlineKeyboardButton(
154
- text=str(value) if value != now_year else highlight(value),
155
- callback_data=DialogCalendarCallback(act=DialogCalAct.set_y, year=value, month=-1, day=-1).pack()
156
- ))
187
+ years_row.append(
188
+ InlineKeyboardButton(
189
+ text=str(value) if value != now_year else highlight(value),
190
+ callback_data=DialogCalendarCallback(
191
+ act=DialogCalAct.set_y, year=value, month=-1, day=-1
192
+ ).pack(),
193
+ )
194
+ )
157
195
  kb.append(years_row)
158
196
  # nav buttons
159
197
  nav_row = []
160
- nav_row.append(InlineKeyboardButton(
161
- text='<<',
162
- callback_data=DialogCalendarCallback(act=DialogCalAct.prev_y, year=year, month=-1, day=-1).pack()
163
- ))
164
- nav_row.append(InlineKeyboardButton(
165
- text=self._labels.cancel_caption,
166
- callback_data=DialogCalendarCallback(act=DialogCalAct.cancel, year=year, month=1, day=1).pack()
167
- ))
168
- nav_row.append(InlineKeyboardButton(
169
- text='>>',
170
- callback_data=DialogCalendarCallback(act=DialogCalAct.next_y, year=year, month=1, day=1).pack()
171
- ))
198
+ nav_row.append(
199
+ InlineKeyboardButton(
200
+ text="<<",
201
+ callback_data=DialogCalendarCallback(
202
+ act=DialogCalAct.prev_y, year=year, month=-1, day=-1
203
+ ).pack(),
204
+ )
205
+ )
206
+ nav_row.append(
207
+ InlineKeyboardButton(
208
+ text=self._labels.cancel_caption,
209
+ callback_data=DialogCalendarCallback(
210
+ act=DialogCalAct.cancel, year=year, month=1, day=1
211
+ ).pack(),
212
+ )
213
+ )
214
+ nav_row.append(
215
+ InlineKeyboardButton(
216
+ text=">>",
217
+ callback_data=DialogCalendarCallback(
218
+ act=DialogCalAct.next_y, year=year, month=1, day=1
219
+ ).pack(),
220
+ )
221
+ )
172
222
  kb.append(nav_row)
173
223
  return InlineKeyboardMarkup(row_width=5, inline_keyboard=kb)
174
224
 
175
- async def process_selection(self, query: CallbackQuery, data: DialogCalendarCallback) -> tuple:
225
+ async def process_selection(
226
+ self, query: CallbackQuery, data: DialogCalendarCallback
227
+ ) -> tuple:
176
228
  return_data = (False, None)
177
229
  if data.act == DialogCalAct.ignore:
178
230
  await query.answer(cache_time=60)
179
231
  if data.act == DialogCalAct.set_y:
180
- await query.message.edit_reply_markup(reply_markup=await self._get_month_kb(int(data.year)))
232
+ await query.message.edit_reply_markup(
233
+ reply_markup=await self._get_month_kb(int(data.year))
234
+ )
181
235
  if data.act == DialogCalAct.prev_y:
182
236
  new_year = int(data.year) - 5
183
- await query.message.edit_reply_markup(reply_markup=await self.start_calendar(year=new_year))
237
+ await query.message.edit_reply_markup(
238
+ reply_markup=await self.start_calendar(year=new_year)
239
+ )
184
240
  if data.act == DialogCalAct.next_y:
185
241
  new_year = int(data.year) + 5
186
- await query.message.edit_reply_markup(reply_markup=await self.start_calendar(year=new_year))
242
+ await query.message.edit_reply_markup(
243
+ reply_markup=await self.start_calendar(year=new_year)
244
+ )
187
245
  if data.act == DialogCalAct.start:
188
- await query.message.edit_reply_markup(reply_markup=await self.start_calendar(int(data.year)))
246
+ await query.message.edit_reply_markup(
247
+ reply_markup=await self.start_calendar(int(data.year))
248
+ )
189
249
  if data.act == DialogCalAct.set_m:
190
- await query.message.edit_reply_markup(reply_markup=await self._get_days_kb(int(data.year), int(data.month)))
250
+ await query.message.edit_reply_markup(
251
+ reply_markup=await self._get_days_kb(int(data.year), int(data.month))
252
+ )
191
253
  if data.act == DialogCalAct.day:
192
254
 
193
255
  return await self.process_day_select(data, query)
194
256
 
195
257
  if data.act == DialogCalAct.cancel:
196
258
  await query.message.delete_reply_markup()
197
- return return_data
259
+ return return_data
@@ -1,31 +1,30 @@
1
- from typing import Optional
2
1
  from enum import Enum
3
-
4
- from pydantic import BaseModel, conlist, Field
2
+ from typing import Optional
5
3
 
6
4
  from aiogram.filters.callback_data import CallbackData
5
+ from pydantic import BaseModel, Field, conlist
7
6
 
8
7
 
9
8
  class SimpleCalAct(str, Enum):
10
- ignore = 'IGNORE'
11
- prev_y = 'PREV-YEAR'
12
- next_y = 'NEXT-YEAR'
13
- prev_m = 'PREV-MONTH'
14
- next_m = 'NEXT-MONTH'
15
- cancel = 'CANCEL'
16
- today = 'TODAY'
17
- day = 'DAY'
9
+ ignore = "IGNORE"
10
+ prev_y = "PREV-YEAR"
11
+ next_y = "NEXT-YEAR"
12
+ prev_m = "PREV-MONTH"
13
+ next_m = "NEXT-MONTH"
14
+ cancel = "CANCEL"
15
+ today = "TODAY"
16
+ day = "DAY"
18
17
 
19
18
 
20
19
  class DialogCalAct(str, Enum):
21
- ignore = 'IGNORE'
22
- set_y = 'SET-YEAR'
23
- set_m = 'SET-MONTH'
24
- prev_y = 'PREV-YEAR'
25
- next_y = 'NEXT-YEAR'
26
- cancel = 'CANCEL'
27
- start = 'START'
28
- day = 'SET-DAY'
20
+ ignore = "IGNORE"
21
+ set_y = "SET-YEAR"
22
+ set_m = "SET-MONTH"
23
+ prev_y = "PREV-YEAR"
24
+ next_y = "NEXT-YEAR"
25
+ cancel = "CANCEL"
26
+ start = "START"
27
+ day = "SET-DAY"
29
28
 
30
29
 
31
30
  class CalendarCallback(CallbackData, prefix="calendar"):
@@ -45,12 +44,34 @@ class DialogCalendarCallback(CalendarCallback, prefix="dialog_calendar"):
45
44
 
46
45
  class CalendarLabels(BaseModel):
47
46
  "Schema to pass labels for calendar. Can be used to put in different languages"
48
- days_of_week: conlist(str, max_length=7, min_length=7) = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
47
+
48
+ days_of_week: conlist(str, max_length=7, min_length=7) = [
49
+ "Пн",
50
+ "Вт",
51
+ "Ср",
52
+ "Чт",
53
+ "Пт",
54
+ "Сб",
55
+ "Вс",
56
+ ]
49
57
  months: conlist(str, max_length=12, min_length=12) = [
50
- "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
58
+ "Янв",
59
+ "Фев",
60
+ "Мар",
61
+ "Апр",
62
+ "Май",
63
+ "Июн",
64
+ "Июл",
65
+ "Авг",
66
+ "Сен",
67
+ "Окт",
68
+ "Ноя",
69
+ "Дек",
51
70
  ]
52
- cancel_caption: str = Field(default='Cancel', description='Caprion for Cancel button')
53
- today_caption: str = Field(default='Today', description='Caprion for Cancel button')
71
+ cancel_caption: str = Field(
72
+ default="Отмена", description="Caprion for Cancel button"
73
+ )
74
+ today_caption: str = Field(default="Сегодня", description="Caprion for Today button")
54
75
 
55
76
 
56
77
  HIGHLIGHT_FORMAT = "[{}]"
@@ -63,16 +84,16 @@ def highlight(text):
63
84
  def superscript(text):
64
85
  normal = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-=()"
65
86
  super_s = "ᴬᴮᶜᴰᴱᶠᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾQᴿˢᵀᵁⱽᵂˣʸᶻᵃᵇᶜᵈᵉᶠᵍʰᶦʲᵏˡᵐⁿᵒᵖ۹ʳˢᵗᵘᵛʷˣʸᶻ⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾"
66
- output = ''
87
+ output = ""
67
88
  for i in text:
68
- output += (super_s[normal.index(i)] if i in normal else i)
89
+ output += super_s[normal.index(i)] if i in normal else i
69
90
  return output
70
91
 
71
92
 
72
93
  def subscript(text):
73
94
  normal = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-=()"
74
95
  sub_s = "ₐ₈CDₑբGₕᵢⱼₖₗₘₙₒₚQᵣₛₜᵤᵥwₓᵧZₐ♭꜀ᑯₑբ₉ₕᵢⱼₖₗₘₙₒₚ૧ᵣₛₜᵤᵥwₓᵧ₂₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎"
75
- output = ''
96
+ output = ""
76
97
  for i in text:
77
- output += (sub_s[normal.index(i)] if i in normal else i)
78
- return output
98
+ output += sub_s[normal.index(i)] if i in normal else i
99
+ return output