-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmain.py
205 lines (178 loc) · 9.65 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
"""
Технически, плагины являются модулями и мало чем отличаются от базовых систем загружаемых в loader.py,
но т.к. "модули" уже заняты - пусть будут плагины. Основные отличия плагинов:
- Хранятся в директории src/plugins/, содержимое которой занесено в .gitignore терминала.
- Загружаются и запускаются динамически после запуска всех базовых систем терминала. Первыми завершают свою работу.
- Терминал никак не зависит от плагинов.
Путь до плагина выглядит как mdmTerminal2/src/plugins/folder/main.py, где:
folder - директория с плагином, ее имя не имеет значения.
main.py - динамически загружаемый файл модуля. Терминал игнорирует остальные файлы и никак их не использует.
Для успешной инициализации плагина, main.py должен содержать определенные свойства и точку входа (Main).
"""
import threading
import requests
import logger
from languages import LANG_CODE
from modules_manager import DynamicModule, Say, NM, EQ
from utils import REQUEST_ERRORS
"""
Обязательно. Не пустое имя плагина, тип - str.
Максимальная длинна 30 символов, не должно содержать пробельных символов, запятых и быть строго в нижнем регистре.
Имя плагина является его идентификатором и должно быть уникально.
"""
NAME = 'forismatic'
"""
Опционально. Версия API под которую написан плагин, тип - int.
Если оно меньше config.ConfigHandler.API то плагин не будет загружен, а в лог будет выдано сообщение.
API терминала увеличивается когда публичные методы, их вызов или результат вызова (у cfg, log, owner) изменяется.
API не увеличивается при добавлении новых методов.
Призван защитить терминал от неправильной работы плагинов.
По факту бесполезен, если None или меньше нуля также будет проигнорирован.
"""
API = 665
"""
Опционально. None или итерируемый объект.
Содержит секции из конфига, удаленное изменение которых вызовет метод reload() у точки входа (при наличии).
Если вызов reload завершится ошибкой, то больше он вызываться не будет (пока терминал не перезапустится).
Если объект dict и значение секции итерируемое, то будут сравниваться ключи в секции.
Примеры:
# Вызов при изменении [settings] lang или [modules] allow
{'settings': ('lang',), 'modules': {'allow': ''}}
# Вызов при изменении секции mpd, models или log
('mpd', 'models' 'log')
"""
CFG_RELOAD = {'settings': ('lang',)}
"""
Опционально.
Если bool(DISABLE) == True, терминал прекратит проверку модуля и не загрузит плагин.
Проверяется первым.
"""
# DISABLE = False
"""
Опционально. None или tuple(int, int, int).
Задает минимальную версию терминала на которой плагин будет запущен.
"""
TERMINAL_VER_MIN = (0, 0, 0)
"""
Опционально. None или tuple(int, int, int).
Задает максимальную версию терминала на которой плагин будет запущен.
"""
TERMINAL_VER_MAX = (9999, 9999, 9999)
class Main(threading.Thread):
"""
Обязательно. Точка входа в плагин, должна быть callable.
Ожидается что это объект класса, экземпляр которого будет создан, но может быть и методом.
Должен принять 3 аргумента, вызов: Main(cfg=self.cfg, log=self._get_log(name), owner=self.own)
Может содержать служебные методы и свойства, все они опциональны. Методы должны быть строго callable:
Методы: start, reload, stop, join.
Свойства: disable.
"""
INTERVAL = 3600 * 12
def __init__(self, cfg, log, owner):
"""
Конструктор плагина.
:param cfg: ссылка на экземпляр config.ConfigHandler.
:param log: ссылка на специальный логгер, вызов: log(msg, lvl=logger.Debug)
:param owner: ссылка на экземпляр loader.Loader
"""
super().__init__()
self.cfg = cfg
self.log = log
self.own = owner
self._wait = threading.Event()
self._work = False
self._events = ('start_record', 'stop_record', 'start_talking', 'stop_talking', 'voice_activated')
"""
Опционально.
Если bool(self.disable) == True, терминал проигнорирует уже инициализированный плагин.
Проверяется после создания всех плагинов, до вызова start().
PS: В данном примере свойство используется для своих нужд, после вызова start, и не играет никакой роли.
Но так лучше не делать :)
"""
self.disable = False
def start(self):
"""
Опционально. Вызывается после того как все плагины будут созданы.
Вызов метода также будет отражен в логе.
При любой ошибке терминал сочтет плагин сломанным и будет его игнорировать.
:return: None
"""
self.reload()
self._work = True
super().start()
def join(self, timeout=None):
"""
Опционально. Вызывается при завершении терминала. Будет вызван и при отсутствии start().
В лог будут добавлены сообщения до и после вызова.
:return: None
"""
self._unsubscribe()
self._work = False
self._wait.set()
super().join(timeout)
def reload(self):
"""
Опционально. Вызывается при изменении конфигурации согласно CFG_RELOAD.
Вызов этого метода будет отражен в логе.
При любой ошибке терминал сочтет reload() сломанным и больше не будет его вызывать.
:return: None
"""
self.disable = LANG_CODE.get('ISO') != 'ru'
if self.disable:
self._unsubscribe()
else:
self._subscribe()
def stop(self):
"""
Опционально. Вызывается при завершении терминала, если join() отсутствует.
Вызов этого метода будет отражен в логе.
:return: None
"""
raise RuntimeError('Never!')
def run(self):
while self._work:
self._wait.wait(self.INTERVAL)
if self._wait.is_set():
self._wait.clear()
continue
if self.disable:
continue
try:
msg = random_quotes()
except RuntimeError as e:
self.log(e, logger.WARN)
else:
self.own.say(msg)
def _callback(self, *_):
self._wait.set()
def _mod_callback(self, *_):
try:
return Say(random_quotes())
except RuntimeError as e:
self.log(e, logger.WARN)
def _subscribe(self):
self.own.subscribe(self._events, self._callback)
self.own.insert_module(DynamicModule(self._mod_callback, NM, [['скажи афоризм', EQ], ['расскажи афоризм', EQ]]))
def _unsubscribe(self):
self.own.unsubscribe(self._events, self._callback)
self.own.extract_module(self._mod_callback)
def random_quotes() -> str:
params = {
'method': 'getQuote',
'format': 'json',
'lang': 'ru',
'key': '',
}
try:
result = requests.post('http://api.forismatic.com/api/1.0/', params=params)
except REQUEST_ERRORS as e:
raise RuntimeError('Request error: {}'.format(e))
if not result.ok:
raise RuntimeError('Server error {}:{}'.format(result.status_code, result.reason))
try:
msg = result.json()['quoteText'][:200]
except (TypeError, KeyError, ValueError) as e:
raise RuntimeError('Parsing error: {}, {}'.format(e, result.text[:200]))
if not msg:
raise RuntimeError('Empty quote')
return msg