PycordViews 1.1.4__py3-none-any.whl → 1.2.0__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.
pycordViews/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from .views.easy_modified_view import EasyModifiedViews
2
2
  from .pagination.pagination_view import Pagination
3
- from .multibot.start_multibot import Multibot
3
+ from .multibot import Multibot
4
4
  from .menu.selectMenu import SelectMenu
5
5
  from .menu.menu import Menu, CustomSelect
pycordViews/menu/menu.py CHANGED
@@ -27,13 +27,6 @@ class Menu:
27
27
  self.__selectMenu.set_callable(self.__menu.custom_id, _callable=_callable)
28
28
  return self
29
29
 
30
- @property
31
- def callable(self) -> Callable:
32
- """
33
- Get the current callable menu
34
- """
35
- return self.__selectMenu.get_callable(self.__menu.custom_id)
36
-
37
30
  def add_option(self, label: str, value: str = MISSING, description: Union[str, None] = None, emoji: Union[str, Emoji, PartialEmoji, None] = None, default: bool = False) -> "Menu":
38
31
  """
39
32
  Add an option to choice.
@@ -82,7 +75,7 @@ class Menu:
82
75
  raise ComponentTypeError()
83
76
 
84
77
  @property
85
- def component(self) -> Select:
78
+ def component(self) -> "CustomSelect":
86
79
  """
87
80
  Get the component
88
81
  """
@@ -95,6 +88,13 @@ class Menu:
95
88
  """
96
89
  return self.__selectMenu
97
90
 
91
+ @property
92
+ def callable(self) -> Callable:
93
+ """
94
+ Get the current callable menu
95
+ """
96
+ return self.__selectMenu.get_callable(self.__menu.custom_id)
97
+
98
98
  class CustomSelect(Select):
99
99
  """
100
100
  Subclass of Select discord Class to use some SelectMenu functions
@@ -4,7 +4,8 @@ from .menu import Menu
4
4
 
5
5
  from typing import Union, Callable, Any
6
6
  from discord.components import ComponentType
7
- from discord import ChannelType, Member, TextChannel, ApplicationContext
7
+ from discord import ChannelType, Member, ApplicationContext
8
+ from discord.abc import GuildChannel
8
9
 
9
10
  class SelectMenu:
10
11
  """
@@ -53,9 +54,9 @@ class SelectMenu:
53
54
  """
54
55
  return self.__global_add_component(ComponentType.role_select, custom_id=custom_id, placeholder=placeholder, max_values=max_values, min_values=min_values, disabled=disabled, row=row)
55
56
 
56
- def add_mentionnable_select_menu(self, custom_id: str = None, placeholder: str = None, min_values: int = 1, max_values: int = 1, disabled=False, row=None) -> Menu:
57
+ def add_mentionable_select_menu(self, custom_id: str = None, placeholder: str = None, min_values: int = 1, max_values: int = 1, disabled=False, row=None) -> Menu:
57
58
  """
58
- Add a role select menu in the ui
59
+ Add a mentionable select menu in the ui
59
60
  :param custom_id: The ID of the select menu that gets received during an interaction. If not given then one is generated for you.
60
61
  :param placeholder: The placeholder text that is shown if nothing is selected, if any.
61
62
  :param max_values: The maximum number of items that must be chosen for this select menu. Defaults to 1 and must be between 1 and 25.
@@ -67,7 +68,7 @@ class SelectMenu:
67
68
 
68
69
  def add_channel_select_menu(self, custom_id: str = None, placeholder: str = None, min_values: int = 1, max_values: int = 1, disabled=False, row=None, channel_types: list[ChannelType] = None):
69
70
  """
70
- Add a role select menu in the ui
71
+ Add a channel select menu in the ui
71
72
  :param custom_id: The ID of the select menu that gets received during an interaction. If not given then one is generated for you.
72
73
  :param placeholder: The placeholder text that is shown if nothing is selected, if any.
73
74
  :param max_values: The maximum number of items that must be chosen for this select menu. Defaults to 1 and must be between 1 and 25.
@@ -100,8 +101,8 @@ class SelectMenu:
100
101
 
101
102
  def set_callable(self, *custom_ids: str, _callable: Union[Callable, None]) -> "SelectMenu":
102
103
  """
103
- Set a callable for the menu associated with custom_ids
104
- :param custom_ids: IDs to menus
104
+ Set a callable for menus associated with custom_ids
105
+ :param custom_ids: IDs menus
105
106
  :param _callable: The coroutine to set for all menus
106
107
  """
107
108
  self.__select_menu.set_callable(*custom_ids, _callable=_callable)
@@ -113,7 +114,7 @@ class SelectMenu:
113
114
  """
114
115
  return await self.__select_menu.respond(ctx=ctx, *args, view=self.__select_menu, **kwargs)
115
116
 
116
- async def send(self, target: Union[Member, TextChannel], *args, **kwargs) -> Any:
117
+ async def send(self, target: Union[Member, GuildChannel], *args, **kwargs) -> Any:
117
118
  """
118
119
  Send at the target
119
120
  """
@@ -128,6 +129,13 @@ class SelectMenu:
128
129
  return
129
130
  await self.__select_menu.update_items(*self.get_view.items)
130
131
 
132
+ def get_callable(self, custom_id: str) -> Union[Callable, None]:
133
+ """
134
+ Get the callable UI
135
+ :param custom_id: UI ID
136
+ """
137
+ return self.__select_menu.get_callable(custom_id)
138
+
131
139
  @property
132
140
  def get_view(self) -> EasyModifiedViews:
133
141
  """
@@ -135,9 +143,3 @@ class SelectMenu:
135
143
  """
136
144
  return self.__select_menu
137
145
 
138
- def get_callable(self, custom_id: str) -> Union[Callable, None]:
139
- """
140
- Get the callable UI
141
- :param custom_id: UI ID
142
- """
143
- return self.__select_menu.get_callable(custom_id)
@@ -1,5 +1 @@
1
- from .start_multibot import Multibot
2
- from .errors import *
3
- from .runner import Runner
4
- from .process_for_bots import ProcessBot
5
- from .process_messages import ProcessMessage
1
+ from .multibot import Multibot
@@ -0,0 +1,194 @@
1
+ from threading import Thread
2
+ from discord import Intents, Bot, ApplicationCommand
3
+ from asyncio import run_coroutine_threadsafe, new_event_loop, set_event_loop, Future, AbstractEventLoop, sleep
4
+ from time import sleep as tsleep
5
+ from .errors import BotNotStartedError, SetupCommandFunctionNotFound, CommandFileNotFoundError
6
+ from typing import Optional
7
+ from importlib import reload
8
+ from importlib.util import spec_from_file_location, module_from_spec
9
+ from os import path
10
+ from sys import modules
11
+
12
+
13
+ class DiscordBot:
14
+
15
+ def __init__(self, token: str, intents: Intents, command_prefix: Optional[str] = None):
16
+ self.__token: str = token
17
+ self.__command_prefix: str = command_prefix
18
+ self.__running_bot: Future = None
19
+ self.__loop: AbstractEventLoop = new_event_loop()
20
+ self.__thread: Thread = Thread(target=self.run_loop, daemon=True) # Thread pour exécuter l'event loop
21
+ self.__thread.start()
22
+ self.__bot: Bot = Bot(loop=self.__loop, intents=intents, command_prefix=command_prefix, help_commad=None, auto_sync_commands=False)
23
+ self.__intents: Intents = intents
24
+ self.__imported_module: list[dict[str, ...]] = []
25
+
26
+ def run_loop(self):
27
+ """Lance la boucle asyncio dans un thread séparé."""
28
+ set_event_loop(self.__loop)
29
+ self.__loop.run_forever()
30
+
31
+ def start(self) -> None:
32
+ """Démarre le bot"""
33
+
34
+ self.__running_bot: Future = run_coroutine_threadsafe(self.__bot.start(token=self.__token, reconnect=True), self.__loop)
35
+
36
+ def stop(self) -> None:
37
+ """
38
+ Stop le bot proprement depuis un autre thread
39
+ :raise: BotNotStartedError
40
+ """
41
+ if self.is_running:
42
+ # Attendre que la fermeture du bot soit terminée
43
+ run_coroutine_threadsafe(self.__stop_bot_in_thread(), self.__loop).result(timeout=30)
44
+ self.__bot = Bot(token=self.__token, intents=self.__intents, command_prefix=self.__command_prefix, help_command=None)
45
+ self.__running_bot = None
46
+ else:
47
+ raise BotNotStartedError(self.__bot.user.name)
48
+
49
+ def add_pyFile_commands(self, file: str, setup_function: str, reload_command: bool):
50
+ """
51
+ Ajoute et charge un fichier de commande bot et ses dépendances.
52
+ Les fichiers doivent avoir une fonction appelée « setup » ou un équivalent passé en paramètre.
53
+
54
+ def setup(bot: Bot) :
55
+ ...
56
+
57
+ :param bot_name : Le nom du bot à ajouter au fichier de commandes
58
+ :param file: Chemin relatif ou absolue du fichier de commande
59
+ :param setup_function : Nom de la fonction appelée par le processus pour donner l'instance de Bot.
60
+ :param reload_command : Recharge toutes les commandes dans le fichier et les dépendances. Défaut : True
61
+ """
62
+ module_name = path.splitext(path.basename(file))[0] # récupère le nom du fichier
63
+ spec = spec_from_file_location(module_name, file) # renvoie un "module spec" à partir du nom et du fichier
64
+ if spec and spec.loader:
65
+ module = module_from_spec(spec) # crée le package à partir du "module spec" s'il existe
66
+ spec.loader.exec_module(module) # charge tout le package
67
+ modules[module_name] = module # enregistre le modul dans les packages du système pour qu'il soit retrouvable lors du rechargement de celui-ci
68
+ self.__call_setup_function_in_command_file(file, module, setup_function, reload_command)
69
+ else:
70
+ raise CommandFileNotFoundError(file)
71
+
72
+ def modify_pyFile_commands(self, file: str, setup_function: str):
73
+ """
74
+ Modifie un fichier de comandes et le recharge.
75
+ Ne recharge que le fichier et non les commandes du bot !
76
+ :param file: Le chemin d'accès relatif ou absolue du fichier
77
+ """
78
+ print('ok')
79
+ module_name = path.splitext(path.basename(file))[0]
80
+ module_found = False
81
+
82
+ # Mise à jour du module et de son setup
83
+ print(self.__imported_module)
84
+ for imported in self.__imported_module:
85
+ print(imported['module'].__name__, module_name)
86
+ if imported['module'].__name__ == module_name:
87
+ print('reload module :', module_name)
88
+ reload(imported['module'])
89
+ imported['setup_function'] = setup_function
90
+ module_found = True
91
+ break
92
+
93
+ if not module_found:
94
+ raise CommandFileNotFoundError(file)
95
+
96
+ # Supprimer toutes les commandes du bot
97
+ for command in self.__bot.application_commands:
98
+ print('del commande', command.name)
99
+ self.__bot.remove_application_command(command)
100
+
101
+ # Réattacher toutes les commandes en réexécutant tous les setup
102
+ for imported in self.__imported_module:
103
+ self.__call_setup_function_in_command_file(
104
+ file,
105
+ imported['module'],
106
+ imported['setup_function'],
107
+ reload_command=False
108
+ )
109
+
110
+
111
+ def reload_commands(self, commands: Optional[list[ApplicationCommand]] = None):
112
+ """
113
+ Charge toutes les commandes du bot sur Discord
114
+ """
115
+ run_coroutine_threadsafe(self.__reload_commands(commands=commands), self.__loop).result(timeout=30)
116
+
117
+
118
+ @property
119
+ def is_running(self) -> bool:
120
+ """Renvoie si la Websocket est connectée"""
121
+ return not self.__bot.is_closed()
122
+
123
+ @property
124
+ def is_ready(self) -> bool:
125
+ """
126
+ Renvoie si le bot est ready
127
+ """
128
+ return self.__bot.is_ready()
129
+
130
+ @property
131
+ def is_ws_ratelimited(self) -> bool:
132
+ """
133
+ Renvoie si le bot est rate limit
134
+ """
135
+ return self.__bot.is_ws_ratelimited()
136
+
137
+
138
+ async def __stop_bot_in_thread(self):
139
+ """
140
+ Clear le cache du bot de manière asynchrone
141
+ """
142
+ await self.__bot.close()
143
+
144
+ def close_ascyncio_loop(self):
145
+ """
146
+ Ferme la boucle asyncio
147
+ """
148
+ if self.__loop.is_running():
149
+ self.__loop.stop()
150
+
151
+ while self.__loop.is_running():
152
+ tsleep(0.3)
153
+
154
+ self.__loop.close()
155
+
156
+ async def __reload_commands(self, commands: Optional[list[ApplicationCommand]]):
157
+ """
158
+ Recharge les commandes quand le bot est ready
159
+ """
160
+ if self.__running_bot is not None:
161
+ while not self.is_ready:
162
+ await sleep(0.3)
163
+ await self.__bot.register_commands(commands=commands, method='individual', force=False)
164
+ else:
165
+ raise BotNotStartedError(self.__token)
166
+
167
+ def __call_setup_function_in_command_file(self, file: str, module, setup_function: str, reload_command: bool):
168
+ """
169
+ Appel la fonction de setup du module pour charger toutes les commandes du bot
170
+ :param file: Le chemin d'accès du fichier de commandes
171
+ :param module: Le module préchargé
172
+ :param setup_function: Le nom de la fonction de setup
173
+ :param reload_command: Si les commandes doivent être recharger sur le bot (envoie une requête à Discord) automatiquement
174
+ """
175
+ if hasattr(module, setup_function): # si la fonction setup (ou autre) est dans le package
176
+ getattr(module, setup_function)(self.__bot)
177
+
178
+ ########## permet de modifier le dictionnaire des modules importés si celui-ci existe déjà, sinon il l'ajoute à la liste des dictionaires des modules importés. Utile car on reload tout si un des modules est modifié
179
+ find = False
180
+ for mod in self.__imported_module:
181
+ if mod['module'].__name__ == module.__name__:
182
+ mod['setup_function'] = setup_function
183
+ find = True
184
+ break
185
+
186
+ if not find:
187
+ self.__imported_module.append({'setup_function': setup_function, 'module': module})
188
+ ##########
189
+
190
+ if reload_command:
191
+ self.reload_commands()
192
+ else:
193
+ raise SetupCommandFunctionNotFound(setup_function, file)
194
+
@@ -1,10 +1,30 @@
1
1
  class MultibotError(Exception):
2
2
  pass
3
3
 
4
+ class BotAlreadyExistError(MultibotError):
5
+ def __init__(self, bot_name: str):
6
+ super().__init__(f"'{bot_name}' bot already exist !")
4
7
 
5
8
  class BotNotFoundError(MultibotError):
6
- """
7
- If the bot is not found with the given name
8
- """
9
- def __init__(self, name: str):
10
- super().__init__(f"{name} is not found")
9
+ def __init__(self, bot_name: str):
10
+ super().__init__(f"'{bot_name}' bot doesn't exist !")
11
+
12
+ class BotNotStartedError(MultibotError):
13
+ def __init__(self, bot_name: str):
14
+ super().__init__(f"'{bot_name}' not started !")
15
+
16
+ class BotNotReadyedError(MultibotError):
17
+ def __init__(self, bot_name: str):
18
+ super().__init__(f"'{bot_name}' not ready !")
19
+
20
+ class BotAlreadyStartedError(MultibotError):
21
+ def __init__(self, bot_name: str):
22
+ super().__init__(f"'{bot_name}' already started !")
23
+
24
+ class SetupCommandFunctionNotFound(MultibotError):
25
+ def __init__(self, setup_command_name: str, file: str):
26
+ super().__init__(f"'{setup_command_name}' function not found in '{file}' file ! Init commands impossible.")
27
+
28
+ class CommandFileNotFoundError(MultibotError):
29
+ def __init__(self, file_name: str):
30
+ super().__init__(f"'{file_name}' file not found ! Init commands impossible.")
@@ -0,0 +1,206 @@
1
+ from multiprocessing import get_context
2
+ from multiprocessing.queues import Queue
3
+ from .process import ManageProcess
4
+ from discord import Intents
5
+ from sys import platform
6
+ from typing import Union
7
+
8
+
9
+ class Multibot:
10
+
11
+ def __init__(self, global_timeout: int = 30):
12
+ """
13
+ Get instance to run few Discord bot
14
+ """
15
+ if platform == 'win32':
16
+ ctx = get_context("spawn")
17
+ else:
18
+ ctx = get_context("forkserver")
19
+ self.__main_queue: Queue = ctx.Queue()
20
+ self.__process_queue: Queue = ctx.Queue()
21
+ # Création du processus gérant les bots
22
+ self.__DiscordProcess = ctx.Process(target=self._start_process)
23
+ self.__DiscordProcess.start()
24
+
25
+ self.global_timeout = global_timeout
26
+
27
+ def __get_data_queue(self) -> Union[list[dict], dict, None]:
28
+ """
29
+ Récupère les données dans la queue processus
30
+ """
31
+ #try:
32
+ result = self.__process_queue.get(timeout=self.global_timeout)
33
+ return result
34
+ #except:
35
+ #return None
36
+
37
+ def _start_process(self):
38
+ """
39
+ Initialise et exécute le gestionnaire de processus.
40
+ """
41
+ manager = ManageProcess(self.__main_queue, self.__process_queue)
42
+ manager.run()
43
+
44
+ def add_bot(self, bot_name: str, token: str, intents: Intents):
45
+ """
46
+ Add a bot in the process
47
+ :param bot_name: Bot name
48
+ :param token: Token bot
49
+ :param intents: Intents bot to Intents discord class
50
+ """
51
+ self.__main_queue.put({"type": "ADD", "bot_name": bot_name, "token": token, 'intents': intents})
52
+ response = self.__get_data_queue()
53
+ return response # Retourne le statut de l'ajout
54
+
55
+ def remove_bot(self, bot_name: str) -> dict[str, str]:
56
+ """
57
+ Shutdown and remove à bot
58
+ :param bot_name: Bot name to remove
59
+ """
60
+ self.__main_queue.put({"type": "REMOVE", "bot_name": bot_name})
61
+ response = self.__get_data_queue()
62
+ return response # Retourne le statut de la suppression
63
+
64
+ def start(self, *bot_names: str) -> list[dict[str, str]]:
65
+ """
66
+ Start bots
67
+ :param bot_names: Bots name to start
68
+ :return: List of data bot status
69
+ """
70
+ results = []
71
+ for bot_name in bot_names:
72
+ self.__main_queue.put({'type': "START", 'bot_name': bot_name})
73
+ results.append(self.__get_data_queue())
74
+ return results
75
+
76
+ def stop(self, *bot_names: str) -> list[dict[str, str]]:
77
+ """
78
+ Stop bots
79
+ :param bot_names: Bots name to start
80
+ :return: Data status dict
81
+ """
82
+ results = []
83
+ for bot_name in bot_names:
84
+ self.__main_queue.put({'type': "STOP", 'bot_name': bot_name})
85
+ results.append(self.__get_data_queue())
86
+ return results
87
+
88
+ def start_all(self) -> list[dict[str, list[str]]]:
89
+ """
90
+ Start all bots in the process.
91
+ """
92
+ self.__main_queue.put({'type': "STARTALL"})
93
+ return self.__get_data_queue()
94
+
95
+ def stop_all(self) -> list[dict[str, list[str]]]:
96
+ """
97
+ Stop all bots in the process.
98
+ This function is slow ! It's shutdown all bots properly.
99
+ """
100
+ self.__main_queue.put({'type': "STOPALL"})
101
+ return self.__get_data_queue()
102
+
103
+ def is_started(self, bot_name: str) -> bool:
104
+ """
105
+ Return the current Websocket connexion status
106
+ :param bot_name: Bot name
107
+ :return: True if the Websocket is online, else False
108
+ """
109
+ self.__main_queue.put({'type': "IS_STARTED", 'bot_name': bot_name})
110
+ return self.__get_data_queue()['message']
111
+
112
+ def is_ready(self, bot_name: str) -> bool:
113
+ """
114
+ Return the current bot connexion status
115
+ :param bot_name: Bot name
116
+ :return: True if the bot if ready, else False
117
+ """
118
+ self.__main_queue.put({'type': "IS_READY", 'bot_name': bot_name})
119
+ return self.__get_data_queue()['message']
120
+
121
+ def is_ws_ratelimited(self, bot_name: str) -> bool:
122
+ """
123
+ Get the current ratelimit status of the bot
124
+ :param bot_name: Bot name
125
+ :return: True if the bot was ratelimited, else False
126
+ """
127
+ self.__main_queue.put({'type': "IS_WS_RATELIMITED", 'bot_name': bot_name})
128
+ return self.__get_data_queue()['message']
129
+
130
+ def reload_commands(self, *bot_names: str) -> list[dict[str, str]]:
131
+ """
132
+ Reload all commands for each bot when bots are ready
133
+ :param bot_names: Bots name to reload commands
134
+ """
135
+ result = []
136
+ for name in bot_names:
137
+ self.__main_queue.put({'type': "RELOAD_COMMANDS", 'name': name})
138
+ result.append(self.__get_data_queue())
139
+ return result
140
+
141
+ def add_pyFile_commands(self, bot_name: str, file: str, setup_function: str = 'setup', reload_command: bool = True) -> dict[str, str]:
142
+ """
143
+ Add and load a command bot file and dependencies.
144
+ Files must have a function called ‘setup’ or an equivalent passed as a parameter.
145
+
146
+ def setup(bot: Bot):
147
+ ...
148
+
149
+ :param bot_name: The bot's name to add commands file
150
+ :param file: Relative or absolute commands file's path
151
+ :param setup_function: Function name called by the process to give the Bot instance.
152
+ :param reload_command: Reload all command in the fil and dependencies. Default : True
153
+ """
154
+ self.__main_queue.put({'type': "ADD_COMMAND_FILE",
155
+ 'bot_name': bot_name,
156
+ 'file': file,
157
+ 'setup_function': setup_function,
158
+ 'reload_command': reload_command})
159
+ return self.__get_data_queue()
160
+
161
+ def modify_pyFile_commands(self, bot_name: str, file: str, setup_function: str = 'setup') -> dict[str, str]:
162
+
163
+ """
164
+ Modifies a file of commands and reloads it.
165
+ Reloads only the file, not the bot commands!
166
+ :param bot_name: The bot's name
167
+ :param file: The file's relative or absolute path
168
+ """
169
+
170
+ self.__main_queue.put({'type': "MODIFY_COMMAND_FILE",
171
+ 'bot_name': bot_name,
172
+ 'file': file,
173
+ 'setup_function': setup_function})
174
+ return self.__get_data_queue()
175
+
176
+ @property
177
+ def bot_count(self) -> int:
178
+ """
179
+ Return the total number of bots
180
+ """
181
+ self.__main_queue.put({'type': "BOT_COUNT"})
182
+ return self.__get_data_queue()['message']
183
+
184
+ @property
185
+ def started_bot_count(self) -> int:
186
+ """
187
+ Return the total number of started bots
188
+ """
189
+ self.__main_queue.put({'type': "STARTED_BOT_COUNT"})
190
+ return self.__get_data_queue()['message']
191
+
192
+ @property
193
+ def shutdown_bot_count(self) -> int:
194
+ """
195
+ Return the total number of shutdown bots
196
+ """
197
+ self.__main_queue.put({'type': "SHUTDOWN_BOT_COUNT"})
198
+ return self.__get_data_queue()['message']
199
+
200
+ @property
201
+ def get_bots_name(self) -> list[str]:
202
+ """
203
+ Return all bots name (not real name of bots)
204
+ """
205
+ self.__main_queue.put({'type': "BOTS_NAME"})
206
+ return self.__get_data_queue()['message']