diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5bb707e..e0a3c46 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -12,7 +12,7 @@ body: label: Error Page description: The [error page](https://github.com/Dotsian/DexScript/wiki/Errors) explains what common errors mean and how to solve them. options: - - label: Did you read the error page and view the common errors? + - label: Did you read the error page and take a look at the common errors? required: true - type: dropdown id: location @@ -46,15 +46,6 @@ body: placeholder: Version number... validations: required: true - - type: dropdown - id: platform - attributes: - label: What fork are you using? - options: - - Ballsdex - - CarFigures - validations: - required: true - type: textarea id: error attributes: diff --git a/DATAPOLICY.md b/DATAPOLICY.md index 1ff136e..72dbe3f 100644 --- a/DATAPOLICY.md +++ b/DATAPOLICY.md @@ -8,10 +8,7 @@ We do not collect, store, or access any data from your application. DexScript on ## File Modifications -During the DexScript installation process, DexScript will modify the following based on the Ballsdex version: - -- **2.22.0-** - `ballsdex/core/bot.py`. -- **2.22.0+** - `config.yml` +During the DexScript installation process, DexScript will modify your application's `config.yml` file. DexScript will add a single line of code to allow the DexScript extension to load when your application starts. This modification is solely for the purpose of running DexScript. You can view the code added by checking the `DexScript/github/installer.py` file in the official DexScript GitHub repository. diff --git a/DexScript/github/installer.py b/DexScript/github/installer.py index b41915a..7a2a4fe 100644 --- a/DexScript/github/installer.py +++ b/DexScript/github/installer.py @@ -6,6 +6,7 @@ # An explanation of the code will be provided below. # # # # THIS CODE IS RAN VIA THE `EVAL` COMMAND. # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # import os @@ -15,7 +16,6 @@ from dataclasses import dataclass from dataclasses import field as datafield from datetime import datetime -from enum import Enum from io import StringIO from traceback import format_exc @@ -25,11 +25,7 @@ from discord.ext import commands UPDATING = os.path.isdir("ballsdex/packages/dexscript") - - -class MigrationType(Enum): - APPEND = 1 - REPLACE = 2 +ASSET_PATH = "https://raw.githubusercontent.com/Dotsian/DexScript/refs/heads/main/assets" @dataclass @@ -38,25 +34,13 @@ class InstallerConfig: Configuration class for the installer. """ - github = ["Dotsian/DexScript", "main"] - files = ["__init__.py", "cog.py", "commands.py", "parser.py", "utils.py"] + github = ["Dotsian/DexScript", "dev"] + files = ["__init__.py", "cog.py", "commands.py", "parser.py", "utils.py", "config.toml"] appearance = { - "logo": "https://raw.githubusercontent.com/Dotsian/DexScript/refs/heads/dev/assets/DexScriptLogo.png", - "logo_error": "https://raw.githubusercontent.com/Dotsian/DexScript/refs/heads/dev/assets/DexScriptLogoError.png", - "banner": "https://raw.githubusercontent.com/Dotsian/DexScript/refs/heads/dev/assets/DexScriptPromo.png", + "logo": f"{ASSET_PATH}/DexScriptLogo.png", + "logo_error": f"{ASSET_PATH}/DexScriptLogoError.png", + "banner": f"{ASSET_PATH}/DexScriptPromo.png", } - install_migrations = [ - ( - "||await self.add_cog(Core(self))", - '||await self.load_extension("ballsdex.packages.dexscript")\n', - MigrationType.APPEND, - ), - ( - '||await self.load_extension("ballsdex.core.dexscript")\n', - '||await self.load_extension("ballsdex.packages.dexscript")\n', - MigrationType.REPLACE, - ), - ] uninstall_migrations = [ '||await self.load_extension("ballsdex.core.dexscript")\n', '||await self.load_extension("ballsdex.packages.dexscript")\n', @@ -87,15 +71,10 @@ def __init__(self, installer, embed_type="setup"): self.installer = installer - match embed_type: - case "setup": - self.setup() - case "error": - self.error() - case "installed": - self.installed() - case "uninstalled": - self.uninstalled() + if not hasattr(self, embed_type): + return + + getattr(self, embed_type)() def setup(self): self.title = "DexScript Installation" @@ -125,7 +104,12 @@ def error(self): self.timestamp = datetime.now() if logger.output != []: - self.description += f"\n```{logger.output[-1]}```" + output = logger.output[-1] + + if len(output) >= 750: + output = logger.output[-1][:750] + "..." + + self.description += f"\n```{output}```" self.installer.interface.attachments = [logger.file("DexScript.log")] @@ -150,6 +134,15 @@ def uninstalled(self): self.set_thumbnail(url=config.appearance["logo"]) + def config(self): + with open(f"{config.path}/config.toml") as file: + file_contents = file.read() + + self.title = "DexScript Configuration" + self.description = f"```toml\n{file_contents}\n```" + self.color = discord.Color.from_str("#03BAFC") + self.timestamp = datetime.now() + class InstallerView(discord.ui.View): def __init__(self, installer): @@ -188,11 +181,153 @@ async def uninstall_button(self, interaction: discord.Interaction, _: discord.ui await interaction.message.edit(**self.installer.interface.fields) await interaction.response.defer() + @discord.ui.button( + style=discord.ButtonStyle.secondary, + label="Config", + disabled=not os.path.isfile(f"{config.path}/config.toml") + ) + async def config_button(self, interaction: discord.Interaction, _: discord.ui.Button): + self.installer.interface.embed = InstallerEmbed(self.installer, "config") + self.installer.interface.view = ConfigView(self.installer) + + await interaction.message.edit(**self.installer.interface.fields) + await interaction.response.defer() + @discord.ui.button(style=discord.ButtonStyle.red, label="Exit") async def quit_button(self, interaction: discord.Interaction, _: discord.ui.Button): - self.install_button.disabled = True - self.uninstall_button.disabled = True - self.quit_button.disabled = True + for item in self.children: + item.disabled = True + + await interaction.message.edit(**self.installer.interface.fields) + await interaction.response.defer() + + +class ConfigModal(discord.ui.Modal): + def __init__(self, installer, setting: str): + self.installer = installer + self.setting = setting + + super().__init__(title=f"Editing `{setting}`") + + value = discord.ui.TextInput(label="New value", required=True) + + async def on_submit(self, interaction: discord.Interaction): + with open(f"{config.path}/config.toml") as file: + lines = [x.strip() for x in file.readlines()] + new_lines = [] + + for line in lines: + if not line.startswith(self.setting): + new_lines.append(line + "\n") + continue + + full_value = self.value.value + new_value = f'"{full_value}"' + + if full_value.lower() in ["true", "false"]: + new_value = full_value.lower() + elif full_value.startswith("[") and full_value.endswith("]"): + new_value = full_value + + new_lines.append(f"{self.setting} = {new_value}\n") + + with open(f"{config.path}/config.toml", "w") as write_file: + write_file.writelines(new_lines) + + self.installer.interface.embed = InstallerEmbed(self.installer, "config") + + await interaction.message.edit(**self.installer.interface.fields) + + await interaction.response.send_message( + f"Updated `{self.setting}` to `{self.value.value}`!", + ephemeral=True + ) + + +class ConfigSelect(discord.ui.Select): + def __init__(self, installer): + self.installer = installer + + options = [] + + with open(f"{config.path}/config.toml") as file: + description = "" + + for line in file.readlines(): + if line.rstrip() in ["\n", "", "]"] or line.startswith(" "): + continue + + if line.startswith("#"): + description = line[2:] + continue + + name = line.split(" ")[0] + + options.append( + discord.SelectOption(label=name, value=name, description=description) + ) + + description = "" + + super().__init__(placeholder="Edit setting", max_values=1, min_values=1, options=options) + + async def callback(self, interaction: discord.Interaction): + await interaction.response.send_modal(ConfigModal(self.installer, self.values[0])) + + +class ConfigView(discord.ui.View): + def __init__(self, installer): + super().__init__() + self.installer = installer + + back_button = discord.ui.Button(label="Back", style=discord.ButtonStyle.primary) + reset_button = discord.ui.Button(label="Reset", style=discord.ButtonStyle.grey) + quit_button = discord.ui.Button(label="Exit", style=discord.ButtonStyle.red) + + back_button.callback = self.back_button + reset_button.callback = self.reset_button + quit_button.callback = self.quit_button + + self.add_item(back_button) + self.add_item(ConfigSelect(installer)) + self.add_item(reset_button) + self.add_item(quit_button) + + async def back_button(self, interaction: discord.Interaction): + self.installer.interface.embed = InstallerEmbed(self.installer, "setup") + self.installer.interface.view = InstallerView(self.installer) + + await interaction.message.edit(**self.installer.interface.fields) + await interaction.response.defer() + + async def reset_button(self, interaction: discord.Interaction): + request = requests.get( + f"https://api.github.com/repos/{config.github[0]}/" + "contents/DexScript/package/config.toml", + {"ref": config.github[1]} + ) + + if request.status_code != requests.codes.ok: + await interaction.response.send_message( + f"Failed to reset config file `({request.status_code})`", ephemeral=True + ) + return + + request = request.json() + content = b64decode(request["content"]) + + with open(f"{config.path}/config.toml", "w") as opened_file: + opened_file.write(content.decode()) + + self.installer.interface.embed = InstallerEmbed(self.installer, "config") + + await interaction.message.edit(**self.installer.interface.fields) + + await interaction.response.send_message("Successfully reset config file", ephemeral=True) + + async def quit_button(self, interaction: discord.Interaction): + for item in self.children: + item.disabled = True await interaction.message.edit(**self.installer.interface.fields) await interaction.response.defer() @@ -232,6 +367,12 @@ class Installer: def __init__(self): self.interface = InstallerGUI(self) + def has_package_config(self) -> bool: + with open("config.yml", "r") as file: + lines = file.readlines() + + return "packages:\n" in lines + def add_package(self, package: str) -> bool: with open("config.yml", "r") as file: lines = file.readlines() @@ -270,41 +411,24 @@ def uninstall_migrate(self): with open("ballsdex/core/bot.py", "w") as write_file: write_file.writelines(lines) - def install_migrate(self): - with open("ballsdex/core/bot.py", "r") as read_file: - lines = read_file.readlines() - - for index, line in enumerate(lines): - for migration in config.install_migrations: - original = self.format_migration(migration[0]) - new = self.format_migration(migration[1]) - - match migration[2]: - case MigrationType.REPLACE: - if line.rstrip() != original: - continue - - lines[index] = new - case MigrationType.APPEND: - if line.rstrip() != original or new in lines: - continue - - lines.insert(index + 1, new) - - with open("ballsdex/core/bot.py", "w") as write_file: - write_file.writelines(lines) - async def install(self): + if not self.has_package_config(): + raise Exception("Your Ballsdex version is no longer compatible with DexScript") + if os.path.isfile("ballsdex/core/dexscript.py"): os.remove("ballsdex/core/dexscript.py") await bot.remove_cog("DexScript") # type: ignore - link = f"https://api.github.com/repos/{config.github[0]}/contents/" + link = f"https://api.github.com/repos/{config.github[0]}/contents" os.makedirs(config.path, exist_ok=True) for file in config.files: + if file.endswith(".toml") and os.path.isfile(f"{config.path}/{file}"): + logger.log(f"{file} already exists, skipping", "INFO") + continue + logger.log(f"Fetching {file} from '{link}/DexScript/package'", "INFO") request = requests.get(f"{link}/DexScript/package/{file}", {"ref": config.github[1]}) @@ -319,16 +443,13 @@ async def install(self): content = b64decode(request["content"]) with open(f"{config.path}/{file}", "w") as opened_file: - opened_file.write(content.decode("UTF-8")) + opened_file.write(content.decode()) logger.log(f"Installed {file} from '{link}/DexScript/package'", "INFO") logger.log("Applying bot.py migrations", "INFO") - if self.add_package(config.path.replace("/", ".")): - self.uninstall_migrate() - else: - self.install_migrate() + self.add_package(config.path.replace("/", ".")) logger.log("Loading DexScript extension", "INFO") @@ -363,7 +484,7 @@ def latest_version(self): if pyproject_request.status_code != requests.codes.ok: return - toml_content = b64decode(pyproject_request.json()["content"]).decode("UTF-8") + toml_content = b64decode(pyproject_request.json()["content"]).decode() new_version = re.search(r'version\s*=\s*"(.*?)"', toml_content) if not new_version: diff --git a/DexScript/github/migration.py b/DexScript/github/migration.py deleted file mode 100644 index 10ad6a9..0000000 --- a/DexScript/github/migration.py +++ /dev/null @@ -1,31 +0,0 @@ -def repair_bot_file(): - """ - Repairs the `bot.py` file and removes extra newlines caused by an old DexScript installer. - """ - new_lines = [] - - with open("ballsdex/core/bot.py", "r") as file: - if "import asyncio\n\n" not in file.read(): - return - - with open("ballsdex/core/bot.py", "r") as file: - last_was_newline = False - - for line in file.readlines(): - if last_was_newline is True: - last_was_newline = False - continue - - if line.endswith("\n") and line != "\n" or line == "\n": - last_was_newline = True - - new_lines.append(line) - - with open("ballsdex/core/bot.py", "w") as file: - file.writelines(new_lines) - - -# |-----------------------------------------------------------------------------------------|# - - -repair_bot_file() diff --git a/DexScript/package/cog.py b/DexScript/package/cog.py index 044a265..23055f6 100644 --- a/DexScript/package/cog.py +++ b/DexScript/package/cog.py @@ -10,12 +10,19 @@ from .parser import DexScriptParser from .utils import Utils, config -__version__ = "0.5" +__version__ = "1.0.0" +ASSET_PATH = "https://raw.githubusercontent.com/Dotsian/DexScript/refs/heads/main/assets" + +async def check_dexscript_user(ctx): + user = ctx.message.author + + return await ctx.bot.is_owner(user) or user.id in config.dexscript_user_ids + class DexScript(commands.Cog): """ - DexScript commands + DexScript commands. """ def __init__(self, bot): @@ -23,12 +30,12 @@ def __init__(self, bot): @staticmethod def check_version(): - if not config.versioncheck: + if not config.version_warning: return None request = requests.get( "https://api.github.com/repos/Dotsian/DexScript/contents/pyproject.toml", - {"ref": config.reference}, + {"ref": config.branch}, ) if request.status_code != requests.codes.ok: @@ -41,13 +48,13 @@ def check_version(): return ( f"Your DexScript version ({__version__}) is outdated. " f"Please update to version ({new_version}) " - f"by running `{settings.prefix}upgrade`" + f"by running `{settings.prefix}installer`" ) return None @commands.command() - @commands.is_owner() + @commands.check(check_dexscript_user) async def run(self, ctx: commands.Context, *, code: str): """ Executes DexScript code. @@ -55,7 +62,7 @@ async def run(self, ctx: commands.Context, *, code: str): Parameters ---------- code: str - The code you want to execute. + The code you want to execute. """ body = Utils.remove_code_markdown(code) @@ -81,7 +88,7 @@ async def run(self, ctx: commands.Context, *, code: str): await ctx.message.add_reaction("✅") @commands.command() - @commands.is_owner() + @commands.check(check_dexscript_user) async def about(self, ctx: commands.Context): """ Displays information about DexScript. @@ -102,7 +109,7 @@ async def about(self, ctx: commands.Context): ) embed = discord.Embed( - title="DexScript - BETA", + title="DexScript", description=description, color=discord.Color.from_str("#03BAFC"), ) @@ -110,26 +117,35 @@ async def about(self, ctx: commands.Context): version_check = "OUTDATED" if self.check_version() is not None else "LATEST" embed.set_thumbnail( - url="https://raw.githubusercontent.com/Dotsian/DexScript/refs/heads/dev/assets/DexScriptLogo.png" + url=f"{ASSET_PATH}/DexScriptLogo.png" ) embed.set_footer(text=f"DexScript {__version__} ({version_check})") await ctx.send(embed=embed) @commands.command() - @commands.is_owner() - async def installer(self, ctx: commands.Context): + @commands.check(check_dexscript_user) + async def installer(self, ctx: commands.Context, reference: str = "main"): + """ + Displays the DexScript installer. + + Parameters + ---------- + reference: str + The DexScript branch you want to run the installer on. + """ link = ( "https://api.github.com/repos/Dotsian/DexScript/contents/DexScript/github/installer.py" ) - request = requests.get(link, {"ref": config.reference}) + + request = requests.get(link, {"ref": reference}) match request.status_code: case requests.codes.not_found: - await ctx.send(f"Could not find installer for the {config.reference} branch.") + await ctx.send(f"Could not find installer for the {reference} branch.") case requests.codes.ok: - content = requests.get(link, {"ref": config.reference}).json()["content"] + content = requests.get(link, {"ref": reference}).json()["content"] await ctx.invoke( self.bot.get_command("eval"), body=base64.b64decode(content).decode() @@ -137,32 +153,3 @@ async def installer(self, ctx: commands.Context): case _: await ctx.send(f"Request raised error code `{request.status_code}`.") - - @commands.command() - @commands.is_owner() - async def setting(self, ctx: commands.Context, setting: str, value: str | None = None): - """ - Changes a setting based on the value provided. - - Parameters - ---------- - setting: str - The setting you want to toggle. - value: str | None - The value you want to set the setting to. - """ - setting = setting.lower() - - if setting not in vars(config): - await ctx.send(f"`{setting}` is not a valid setting.") - return - - setting_value = vars(config)[setting] - new_value = value - - if isinstance(setting_value, bool): - new_value = bool(value) if value else not setting_value - - setattr(config, setting, new_value) - - await ctx.send(f"`{setting}` has been set to `{new_value}`") diff --git a/DexScript/package/commands.py b/DexScript/package/commands.py index 99a492c..1701bca 100644 --- a/DexScript/package/commands.py +++ b/DexScript/package/commands.py @@ -1,21 +1,13 @@ import asyncio import os +import re import shutil -from dataclasses import dataclass -from dataclasses import field as datafield import discord from .utils import STATIC, Types, Utils - -@dataclass -class Shared: - """ - Values that will be retained throughout the entire code execution. - """ - - attachments: list = datafield(default_factory=list) +EMOJI_RE = re.compile(r"^[a-zA-Z0-9_]{2,32}$") class DexCommand: @@ -23,14 +15,27 @@ class DexCommand: Default class for all dex commands. """ - def __init__(self, bot, shared): + def __init__(self, bot, attachments): self.bot = bot - self.shared = shared + self.attachments = attachments def __loaded__(self): + """ + Calls whenever the command is loaded for the first time. + """ pass def attribute_error(self, model, attribute): + """ + Raises an error if an attribute doesn't exist in a model. + + Parameters + ---------- + model: + The model you want to check in. + attribute: + The attribute you want to check. + """ if model.value is None or hasattr(model.value(), attribute): return @@ -40,41 +45,93 @@ def attribute_error(self, model, attribute): "all attributes for that model" ) + def type_error(self, value, name: str, allowed_types: list[Types]): + """ + Raises an error if the type of a `Value` is not allowed. + + Parameters + ---------- + value: + The value that has the original type. + name: + The name of the value. + allowed_types: + A list of types that are allowed. + """ + if value is None or value.type in allowed_types: + return + + raise Exception(f"'{value.type}' is an invalid type for '{name}'.") + class Global(DexCommand): """ Main methods for DexScript. """ - async def create(self, ctx, model, identifier): + async def create(self, ctx, model, identifier, values=None): """ Creates a model instance. + Parameters + ------------- + model: + The model you want to create an instance for. + identifier: + The identifier (name) of the model instance. + values: + The values you want to provide to the newly created model instance. + Documentation ------------- - CREATE > MODEL > IDENTIFIER + CREATE > MODEL > IDENTIFIER > VALUES(?) """ - await Utils.create_model(model.value, identifier) + await Utils.create_model(model.value, identifier, values) await ctx.send(f"Created `{identifier}` {model.name.lower()}") - async def delete(self, ctx, model, identifier): + async def delete(self, ctx, model, identifiers): """ - Deletes a model instance. + Deletes one or multiple model instances. + + Parameters + ------------- + model: + The model you want to delete the instance from. + identifiers: + The identifier(s) of the instance(s) you want to delete. Documentation ------------- - DELETE > MODEL > IDENTIFIER + DELETE > MODEL > IDENTIFIER(S) """ - fetched_model = await Utils.get_model(model, identifier) - - await fetched_model.delete() + async def delete_model(identifier): + fetched_model = await Utils.get_model(model, identifier) + + await fetched_model.delete() + + await ctx.send(f"Deleted `{identifier}` {model.name.lower()}") + + if identifiers.type != Types.ARRAY: + await delete_model(identifiers) + return - await ctx.send(f"Deleted `{identifier}` {model.name.lower()}") + for identifier in identifiers.value: + await delete_model(identifier) async def update(self, ctx, model, identifier, attribute, value=None): """ - Updates a model instance's attribute. If value is None, it will check - for any attachments. + Updates a model instance's attribute. + + Parameters + ------------- + model: + The model you want to update the instance from. + identifier: + The identifier of the model instance. + attribute: + The attribute you want to update. + value: + The new value of the specified attribute. If blank, it will search for attachments. Documentation ------------- @@ -93,13 +150,22 @@ async def update(self, ctx, model, identifier, attribute, value=None): ), ) - if value is None and self.shared.attachments and attribute_name in image_fields: - image_path = await Utils.save_file(self.shared.attachments.pop(0)) - new_value = f"/static/uploads/{image_path}" if STATIC else f"/{image_path}" + if attribute_name in image_fields: + file = None - if attribute.type == Types.MODEL: + if new_value is not None and new_value.startswith("https://"): + file = Utils.from_link(new_value) + else: + file = self.attachments.pop(0) + + image_path = await Utils.save_file(file) + + new_value = f"/static/uploads/{image_path}" if STATIC else image_path + + if attribute.type == Types.MODEL and value is not None: attribute_name = f"{attribute.name.lower()}_id" attribute_model = await Utils.get_model(attribute, value) + new_value = attribute_model.pk setattr(returned_model, attribute_name, new_value) @@ -111,7 +177,7 @@ async def update(self, ctx, model, identifier, attribute, value=None): async def view(self, ctx, model, identifier, attribute=None): """ - Displays an attribute of a model instance. If `ATTRIBUTE` is left blank, + Displays an attribute of a model instance. If `ATTRIBUTE` is left blank, it will display every attribute of that model instance. Documentation @@ -130,7 +196,9 @@ async def view(self, ctx, model, identifier, attribute=None): fields["content"] += f"{key}: {value}\n" if isinstance(value, str) and Utils.is_image(value): - fields.setdefault("files", []).append(discord.File(Utils.image_path(value))) + fields.setdefault( + "files", []).append(discord.File(Utils.image_path(value)) # type: ignore + ) fields["content"] += "```" await ctx.send(**fields) @@ -147,7 +215,7 @@ async def view(self, ctx, model, identifier, attribute=None): ) return - if attribute.type == Types.MODEL: + if attribute.type == Types.MODEL and not isinstance(new_attribute, str): new_attribute = await new_attribute.values_list(attribute.extra_data[0], flat=True) await ctx.send(f"```{new_attribute}```") @@ -161,7 +229,7 @@ async def attributes(self, ctx, model, filter=None): ATTRIBUTES > MODEL > FILTER(?) """ def filter_function(_, field_type): - if field_type == "BackwardFKRelation": + if field_type.__class__.__name__ == "BackwardFKRelation": return False if filter is None: @@ -188,8 +256,8 @@ class Filter(DexCommand): async def update(self, ctx, model, attribute, old_value, new_value, tortoise_operator=None): """ - Updates all instances of a model to the specified value where the specified attribute - meets the condition defined by the optional `TORTOISE_OPERATOR` argument + Updates all instances of a model to the specified value where the specified attribute + meets the condition defined by the optional `TORTOISE_OPERATOR` argument (e.g., greater than, equal to, etc.). Documentation @@ -217,8 +285,8 @@ async def update(self, ctx, model, attribute, old_value, new_value, tortoise_ope async def delete(self, ctx, model, attribute, value, tortoise_operator=None): """ - Deletes all instances of a model where the specified attribute meets the condition - defined by the optional `TORTOISE_OPERATOR` argument + Deletes all instances of a model where the specified attribute meets the condition + defined by the optional `TORTOISE_OPERATOR` argument (e.g., greater than, equal to, etc.). Documentation @@ -244,8 +312,8 @@ async def delete(self, ctx, model, attribute, value, tortoise_operator=None): async def view(self, ctx, model, attribute, value, tortoise_operator=None): """ - Displays all instances of a model where the specified attribute meets the condition - defined by the optional `TORTOISE_OPERATOR` argument + Displays all instances of a model where the specified attribute meets the condition + defined by the optional `TORTOISE_OPERATOR` argument (e.g., greater than, equal to, etc.). Documentation @@ -288,6 +356,11 @@ async def save(self, ctx, name): """ Saves an eval preset. + Parameters + ---------- + name: + The name of the eval preset you want to save. + Documentation ------------- EVAL > SAVE > NAME @@ -299,9 +372,6 @@ async def save(self, ctx, name): f"`{name}` exceeds the {NAME_LIMIT}-character limit ({len(name)} > {NAME_LIMIT})" ) - if os.path.isfile(f"eval_presets/{name}.py"): - raise Exception(f"`{name}` already exists.") - await ctx.send("Please send the eval command below...") try: @@ -319,10 +389,29 @@ async def save(self, ctx, name): await ctx.send(f"`{name}` eval preset has been saved!") + async def file(self, ctx): + """ + Runs an eval command from a file. + + Documentation + ------------- + EVAL > FILE + """ + content = await self.attachments[0].read() + + self.attachments.pop(0) + + await ctx.invoke(self.bot.get_command("eval"), body=content.decode()) + async def remove(self, ctx, name): """ Removes an eval preset. + Parameters + ---------- + name: + The name of the eval preset you want to remove. + Documentation ------------- EVAL > REMOVE > NAME @@ -335,16 +424,28 @@ async def remove(self, ctx, name): await ctx.send(f"Removed `{name}` preset.") async def list(self, ctx): + """ + Lists all eval presets. + + Documentation + ------------- + EVAL > LIST + """ if os.listdir("eval_presets") == []: await ctx.send("You have no eval presets saved.") return await Utils.message_list(ctx, os.listdir("eval_presets")) - async def run(self, ctx, name): # TODO: Allow args to be passed through `run`. + async def run(self, ctx, name): """ Runs an eval preset. + Parameters + ---------- + name: + The name of the eval preset you want to run. + Documentation ------------- EVAL > RUN > NAME @@ -366,31 +467,50 @@ class File(DexCommand): Commands for managing and modifying the bot's internal filesystem. """ - async def read(self, ctx, file_path): + async def read(self, ctx, file_paths): """ - Sends a file based on the specified file path. + Sends one or more files based on the specified file paths. Documentation ------------- - FILE > READ > FILE_PATH + FILE > READ > FILE_PATH(S) """ - await ctx.send(file=discord.File(file_path.name)) + if file_paths.type != Types.ARRAY: + await ctx.send(file=discord.File(file_paths.name)) + return + + files = [] + + for path in file_paths.value: + files.append(discord.File(path.name)) + + await ctx.send(files=files) async def write(self, ctx, file_path): """ - Writes to a file using the attached file's contents. + Writes to a file. If an attachment is added, it will save the attachment. Documentation ------------- FILE > WRITE > FILE_PATH """ - new_file = ctx.message.attachments[0] + if self.attachments == []: + if os.path.isfile(file_path.name): + raise Exception(f"{file_path} already exists.") + + with open(file_path.name, "w"): + pass - with open(file_path.name, "w") as opened_file: - contents = await new_file.read() - opened_file.write(contents.decode("utf-8")) + await ctx.send(f"Created `{file_path}`") + return + + new_file = self.attachments[0] + + self.attachments.pop(0) - await ctx.send(f"Wrote to `{file_path}`") + await new_file.save(file_path.name) + + await ctx.send(f"Wrote to `{file_path}` from `{new_file.filename}`") async def clear(self, ctx, file_path): """ @@ -420,6 +540,23 @@ async def listdir(self, ctx, file_path=None): await Utils.message_list(ctx, os.listdir(path)) + async def mkdir(self, ctx, file_path): + """ + Creates a new directory. + + Parameters + ------------- + file_path: + The file path of the directory. + + Documentation + ------------- + FILE > MKDIR > FILE_PATH + """ + os.mkdir(file_path.value) + + await ctx.send(f"Created `{file_path}` directory") + async def delete(self, ctx, file_path): """ Deletes a file or directory based on the specified file path. @@ -445,27 +582,121 @@ class Template(DexCommand): Template commands used to assist with DexScript commands. """ - # TODO: Softcode model creation template. - async def create(self, ctx, model, argument="..."): + async def create(self, ctx, model, argument="...", include_args=None): """ Sends the `create` template for a model. Documentation ------------- - TEMPLATE > CREATE > MODEL > ARGUMENT(?) - """ - match model.name.lower(): - case "ball": - template_commands = [ - f"CREATE > BALL > {argument}", - f"UPDATE > BALL > {argument} > REGIME > ...", - f"UPDATE > BALL > {argument} > HEALTH > ...", - f"UPDATE > BALL > {argument} > ATTACK > ...", - f"UPDATE > BALL > {argument} > RARITY > ...", - f"UPDATE > BALL > {argument} > EMOJI_ID > ...", - f"UPDATE > BALL > {argument} > CREDITS > ...", - f"UPDATE > BALL > {argument} > CAPACITY_NAME > ...", - f"UPDATE > BALL > {argument} > CAPACITY_DESCRIPTION > ...", - ] - - await ctx.send(f"```sql\n{'\n'.join(template_commands)}\n```") + TEMPLATE > CREATE > MODEL > ARGUMENT(?) > INCLUDE_ARGS(?) + """ + def filter_function(field, field_type): + if field_type.__class__.__name__ in ["BackwardFKRelation", "JSONField"]: + return False + + if field_type.null or field_type.default or field.endswith("_id"): + return False + + if field in [model.extra_data[0], "id"]: + return False + + return True + + args = [x.upper() for x in Utils.fetch_fields(model.value, filter_function)] + + structure = f"UPDATE > {model.name.upper()} > {argument} > $value > ..." + template_commands = [f"CREATE > {model.name.upper()} > {argument}"] + + if include_args is not None: + args += [x.name.upper() for x in include_args.value] + + for arg in args: + template_commands.append(structure.replace("$value", arg)) + + await ctx.send(f"```sql\n{'\n'.join(template_commands)}\n```") + +class Emoji(DexCommand): + """ + Commands used for modifying application emojis. + """ + + async def new(self, ctx, name, image=None): + """ + Creates an application emoji based on the provided image and name. + + Parameters + ------------- + name: + The name of the emoji you want to create. + image: + The image link you want to use if there are no attachments. + + Documentation + ------------- + EMOJI > NEW > NAME > IMAGE(?) + """ + image_content = None + + if image is None: + image_content = await self.attachments[0].read() + + self.attachments.pop(0) + else: + image_content = Utils.from_link(image.value)[1] + + new_name = name.value.replace(" ", "").replace('"', "") + + if not bool(EMOJI_RE.match(new_name)): + raise Exception(f"Emoji name `{new_name}` is invalid.") + + emoji = await self.bot.create_application_emoji(name=new_name, image=image_content) + + await ctx.send(f"Created {emoji} **{new_name}** `({emoji.id})`") + + async def delete(self, ctx, name): + """ + Deletes an application emoji. + + Parameters + ------------- + name: + The name of the emoji you want to delete. + + Documentation + ------------- + EMOJI > DELETE > NAME + """ + emojis = await self.bot.fetch_application_emojis() + new_name = name.value.replace(" ", "").replace('"', "") + + emoji = discord.utils.get(emojis, name=new_name) + + if not emoji.is_application_owned(): + raise Exception("This emoji is not owned by the application.") + + await ctx.send(f"Deleted {emoji} **{new_name}**") + + await emoji.delete() + + async def info(self, ctx, name): + """ + Displays information about an application emoji. + + Parameters + ------------- + name: + The name of the emoji you want to view. + + Documentation + ------------- + EMOJI > INFO > NAME + """ + emojis = await self.bot.fetch_application_emojis() + new_name = name.value.replace(" ", "").replace('"', "") + + emoji = discord.utils.get(emojis, name=new_name) + + if not emoji.is_application_owned(): + raise Exception("This emoji is not owned by the application.") + + await ctx.send(f"{emoji} **{new_name}**\nID: `{emoji.id}`") diff --git a/DexScript/package/config.toml b/DexScript/package/config.toml new file mode 100644 index 0000000..1306048 --- /dev/null +++ b/DexScript/package/config.toml @@ -0,0 +1,14 @@ +# Whether or not DexScript will warn you for running an outdated version. +version-warning = true + +# The command groups that DexScript will load. +command-groups = ["Global", "Emoji", "Eval", "File", "Filter", "Template"] + +# A list of user IDs that will be able to execute DexScript commands. +dexscript-user-ids = [] + +# Displays additional information for error handling. +debug = false + +# The GitHub branch that will be used for version checking and other misc features. +branch = "main" diff --git a/DexScript/package/parser.py b/DexScript/package/parser.py index b5798dc..c54d06f 100644 --- a/DexScript/package/parser.py +++ b/DexScript/package/parser.py @@ -1,5 +1,4 @@ import inspect -import re import traceback from dataclasses import dataclass from dataclasses import field as datafield @@ -14,7 +13,7 @@ @dataclass class Value: name: str - type: Types = Types.DEFAULT + type: Types = Types.STRING value: Any = None extra_data: list = datafield(default_factory=list) @@ -31,7 +30,6 @@ class DexScriptParser: def __init__(self, ctx, bot): self.ctx = ctx self.bot = bot - # self.attachments = ctx.message.attachments self.command_classes = inspect.getmembers( commands, @@ -40,23 +38,31 @@ def __init__(self, ctx, bot): and issubclass(o, commands.DexCommand) and not issubclass(o, commands.Global) and o.__name__ != "DexCommand" + and o.__name__ in config.command_groups ), ) self.global_methods = [x for x in dir(commands.Global) if not x.startswith("__")] + if "Global" not in config.command_groups: + self.global_methods = [] + def create_value(self, line): value = Value(line) value.value = line lower = line.lower() + pascal = Utils.pascal_case(line) type_dict = { Types.METHOD: lower in self.global_methods, Types.CLASS: lower in [x[0].lower() for x in self.command_classes], - Types.MODEL: lower in Utils.models(True, key=str.lower), + Types.MODEL: pascal in Utils.models(True), Types.DATETIME: Utils.is_date(lower) and lower.count("-") >= 2, Types.BOOLEAN: lower in ["true", "false"], + Types.HEX: lower.startswith("#"), + Types.ARRAY: lower.startswith("[") and lower.endswith("]"), + Types.NONE: lower == "nil" } for key, operation in type_dict.items(): @@ -68,10 +74,10 @@ def create_value(self, line): match value.type: case Types.MODEL: - model = Utils.fetch_model(line) + model = Utils.fetch_model(pascal, False) if model is None: - raise Exception(f"'{line}' is not a valid model") + raise Exception(f"'{pascal}' is not a valid model") string_key = Utils.extract_str_attr(model) @@ -86,21 +92,82 @@ def create_value(self, line): case Types.DATETIME: value.value = parse_date(line) + case Types.HEX: + hex_str = str(int(line[1:], 16)) + + value.name = hex_str + value.value = hex_str + + case Types.ARRAY: + value.value = [self.create_value(x.strip()) for x in line[1:-1].split("|")] + + case Types.NONE: + value.value = None + return value def error(self, message, log): return (message, log)[config.debug] async def execute(self, code: str, run_commands=True): - shared_instance = commands.Shared(self.ctx.message.attachments) - + data = [] + new_data = [] + + entered_indent = False + iteration_level = 0 + original_index = -1 + split_code = [x for x in code.split("\n") if x.strip() != ""] + + for line in split_code: + line_new = line.strip() + + if line_new.startswith("--"): + continue + + if not line_new.startswith("|"): + data.append([line, {}]) + original_index = -1 + + if entered_indent: + iteration_level += 1 + + continue + + if original_index == -1: + entered_indent = True + original_index = iteration_level - parsed_code = [ - [self.create_value(s.strip()) for s in re.findall(r"[^>]+", line)] - for line in split_code - if not line.strip().startswith("--") - ] + split_pipe = line.split("|")[1].strip() + + if ">" not in split_pipe: + data[original_index][1][split_pipe] = split_pipe + continue + + split_line = line.split("|")[1].split(">") + key = split_line[0].strip() + + data[original_index][1][key] = split_line[1].strip() + + for item in data: + if item[1] == {}: + new_data.append(item[0]) + continue + + statement = item[0] + + for key, value in item[1].items(): + new_statement = f"{statement} > {key}" + + if key != value: + new_statement += f" > {value}" + + new_data.append(new_statement) + + parsed_code = [] + + for line in new_data: + parsed_code.append([self.create_value(x.strip()) for x in line.split(">")]) if not run_commands: return parsed_code @@ -125,7 +192,7 @@ async def execute(self, code: str, run_commands=True): line2.pop(0) class_loaded = commands.Global if method[0] == commands.Global else method[0] - class_loaded = class_loaded(self.bot, shared_instance) + class_loaded = class_loaded(self.bot, self.ctx.message.attachments) class_loaded.__loaded__() method_call = getattr(class_loaded, method[1].name.lower()) diff --git a/DexScript/package/utils.py b/DexScript/package/utils.py index b352ef8..300024e 100644 --- a/DexScript/package/utils.py +++ b/DexScript/package/utils.py @@ -3,6 +3,7 @@ import inspect import os import re +import tomllib from dataclasses import dataclass from difflib import get_close_matches from enum import Enum @@ -11,30 +12,45 @@ from typing import Any, Callable import discord -from ballsdex.core.models import Ball, Economy, Regime, Special # noqa: F401, I001 +import requests +from ballsdex.core.models import ( + Ball, # noqa: F401 + BallInstance, # noqa: F401 + Economy, # noqa: F401 + Player, # noqa: F401 + Regime, # noqa: F401 + Special, # noqa: F401 +) from dateutil.parser import parse as parse_date START_CODE_BLOCK_RE = re.compile(r"^((```sql?)(?=\s)|(```))") FILENAME_RE = re.compile(r"^(.+)(\.\S+)$") +PASCAL_RE = re.compile(r"((-|_)[a-z])") +STR_RE = re.compile(r"return\s+self\.(\w+)") STATIC = os.path.isdir("static") MEDIA_PATH = "./static/uploads" if STATIC else "./admin_panel/media" MODELS = [ "Ball", - "Regime", + "BallInstance", "Economy", + "Player", + "Regime", "Special", ] class Types(Enum): - DEFAULT = 0 + STRING = 0 METHOD = 1 CLASS = 2 BOOLEAN = 3 MODEL = 4 DATETIME = 5 + HEX = 6 + ARRAY = 7 + NONE = 8 @dataclass @@ -43,12 +59,31 @@ class Settings: Settings class for DexScript. """ - debug: bool = False - versioncheck: bool = False - reference: str = "main" + def __init__(self, path): + with open(path, "rb") as f: + data = tomllib.load(f) + + if data is None: + return + + self.version_warning = data.get("version-warning", True) + + self.command_groups = data.get("command-groups", [ + "Global", + "Emoji", + "Eval", + "File", + "Filter", + "Template" + ]) + self.dexscript_user_ids = data.get("dexscript-user-ids", []) -config = Settings() + self.debug = data.get("debug", False) + self.branch = data.get("branch", "main") + + +config = Settings(Path(os.path.dirname(os.path.abspath(__file__)), "./config.toml")) @dataclass @@ -58,7 +93,26 @@ class Utils: """ @staticmethod - def image_path(path: str) -> bool: + def from_link(link: str) -> tuple[str, bytes]: + """ + Returns a image link's content and filename. + + Parameters + ---------- + link: str + The image link you want to load the data from. + """ + data = requests.get(link, stream=True) + + if not data.ok: + raise Exception(f"Failed to request '{link}'") + + filename = os.path.basename(link).split("?")[0] + + return (filename, data.content) + + @staticmethod + def image_path(path: str) -> str: """ Formats an image path correctly. @@ -113,8 +167,9 @@ def pascal_case(string: str) -> str: The string you want to convert. """ string = string.lower() - return re.sub( - r"(_[a-z])", lambda m: m.group(1)[1].upper(), string[:1].upper() + string[1:] + + return PASCAL_RE.sub( + lambda m: m.group(1)[1].upper(), string[:1].upper() + string[1:] ) @staticmethod @@ -180,22 +235,29 @@ def check(message): if response.content.lower() == "more": continue - await ctx.send(file=discord.File(StringIO("\n".join(messages)), filename="output.txt")) + discord_file = discord.File( + StringIO("\n".join(messages)), filename="output.txt" # type: ignore + ) + + await ctx.send(file=discord_file) break @staticmethod - async def save_file(attachment: discord.Attachment) -> Path: + async def save_file(attachment: discord.Attachment | tuple[str, bytes]) -> Path: """ - Saves a `discord.Attachment` object into a directory. + Saves a `discord.Attachment` or a `tuple` object into a directory. Parameters ---------- - attachment: discord.Attachment + attachment: discord.Attachment | tuple[str, bytes] The attachment you want to save. """ - path = Path(f"{MEDIA_PATH}/{attachment.filename}") - match = FILENAME_RE.match(attachment.filename) + is_attachment = isinstance(attachment, discord.Attachment) + filename = attachment.filename if is_attachment else attachment[0] + + path = Path(f"{MEDIA_PATH}/{filename}") + match = FILENAME_RE.match(filename) if not match: raise TypeError("The file you uploaded lacks an extension.") @@ -206,12 +268,16 @@ async def save_file(attachment: discord.Attachment) -> Path: path = Path(f"{MEDIA_PATH}/{match.group(1)}-{i}{match.group(2)}") i = i + 1 - await attachment.save(path) + if isinstance(attachment, discord.Attachment): + await attachment.save(path) + else: + with open(path, "wb") as write_file: + write_file.write(attachment[1]) return path.relative_to(MEDIA_PATH) @staticmethod - def fetch_model(model: str): + def fetch_model(model: str, force_pascal: bool = True): """ Fetches a model's class based on the model name provided. @@ -219,8 +285,10 @@ def fetch_model(model: str): ---------- model: str The name of the model you want to fetch. + force_pascal: bool + Whether you want to force pascal casing on the model name. """ - return globals().get(Utils.pascal_case(model)) + return globals().get(Utils.pascal_case(model) if force_pascal else model) @staticmethod def models(names=False, key: Callable | None = None): @@ -247,7 +315,7 @@ def models(names=False, key: Callable | None = None): return model_list @staticmethod - async def create_model(model, identifier, fields_only=False): + async def create_model(model, identifier, values=None, fields_only=False): """ Creates a model instance while providing default values for all. @@ -257,6 +325,8 @@ async def create_model(model, identifier, fields_only=False): The tortoise model you want to use. identifier: str The name of the model instance. + values: dict | None + Default values that will be added to the model. fields_only: bool Whether you want to return the fields created only or not (debugging). """ @@ -281,10 +351,12 @@ async def create_model(model, identifier, fields_only=False): case "ForeignKeyFieldInstance": casing_field = Utils.pascal_case(field) - instance = await Utils.fetch_model(casing_field).first() + instance = Utils.fetch_model(casing_field) if instance is None: raise Exception(f"Could not find default {casing_field}") + + instance = await instance.first() fields[f"{field}_id"] = instance.pk @@ -297,6 +369,10 @@ async def create_model(model, identifier, fields_only=False): case _: fields[field] = 1 + if values is not None: + for key, item in values.items(): + fields[key] = item + if fields_only: return fields @@ -316,6 +392,9 @@ async def get_model(model, identifier: str): """ correction_list = await model.value.all().values_list(model.extra_data[0], flat=True) + if not all([isinstance(x, str) for x in correction_list]): + correction_list = [str(x) for x in correction_list] + try: returned_model = await model.value.filter( **{model.extra_data[0]: Utils.autocorrect(str(identifier), correction_list)} @@ -386,7 +465,7 @@ def autocorrect(string: str, correction_list: list[str], error="does not exist." return autocorrection[0] @staticmethod - def extract_str_attr(object: Any): + def extract_str_attr(object: Any) -> str: """ Extracts the attribute used in the `__str__` method of a class. @@ -395,9 +474,18 @@ def extract_str_attr(object: Any): object: Any The class you want to fetch the `__str__` attribute from. """ - expression = r"return\s+self\.(\w+)" # TODO: Add `return str()` + source = inspect.getsource(object.__str__).replace("str(", "") + extracted = STR_RE.search(source) + + if extracted is None: + return "id" + + extracted = extracted.group(1) + + if extracted == "to_string": + return "id" - return re.search(expression, inspect.getsource(object.__str__)).group(1) + return extracted @staticmethod def remove_code_markdown(content: str) -> str: diff --git a/README.md b/README.md index e0adb9f..b99dca6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# DexScript - BETA +# DexScript ![DexScript Banner](assets/DexScriptPromo.png) @@ -20,18 +20,17 @@ DexScript has a ton more features too! All of them can be found within our exten * **Mass updating and deleting Balls, Regimes, Specials, etc.** * **Saving evals and loading them**. -DexScript is currently in beta. However, the latest version is a release candidate for the full release. - ## DexScript Requirements To install DexScript, you must have the following: * Ballsdex -* Eval access +* Eval access (if using eval) +* [DexI](https://github.com/Dotsian/DexI) (if using DexI) ## DexScript Setup -The DexScript installer is a intuitive menu that can allow you to easily update, install, and uninstall DexScript. To bring up the DexScript installer, all you have to do is run one eval command! +The DexScript installer is a intuitive menu that can allow you to easily update, install, configure, and uninstall DexScript. To bring up the DexScript installer, all you have to do is run one command! ### Versions @@ -39,29 +38,46 @@ DexScript has two versions, the release version and the development version. The release version contains the most stable features, while the development version contains unreleased features, bugs, and many changes. -To install DexScript, run the following eval command: +To install DexScript, run the following command: #### Release Version +**Using DexI** + +```bash +dexi add Dotsian/DexScript +``` + +**Using Eval** + ```py import base64, requests; await ctx.invoke(bot.get_command("eval"), body=base64.b64decode(requests.get("https://api.github.com/repos/Dotsian/DexScript/contents/DexScript/github/installer.py").json()["content"]).decode()) ``` #### Development Version -```py -import base64, requests; await ctx.invoke(bot.get_command("eval"), body=base64.b64decode(requests.get("https://api.github.com/repos/Dotsian/DexScript/contents/DexScript/github/installer.py", {"ref": "dev"}).json()["content"]).decode()) +**Using DexI** + +```bash +dexi add Dotsian/DexScript --branch dev ``` -### DexScript Installer +**Using Eval** + +> ```py +> import base64, requests; await ctx.invoke(bot.get_command("eval"), body=base64.b64decode(requests.get("https://api.github.com/repos/Dotsian/DexScript/contents/DexScript/github/installer.py", {"ref": "dev"}).json()["content"]).decode()) +> ``` + +### DexScript Installer (Eval) > [!NOTE] -> If DexScript is already installed, you can run `b.installer` to show the DexScript installer, replacing `b.` with your application's prefix.. +> If DexScript is already installed, you can run `b.installer` to show the DexScript installer, replacing `b.` with your application's prefix. Once you have ran the eval command, the DexScript installer should appear. There will be three buttons: * Install [or] Update * Uninstall +* Config * Exit > [!IMPORTANT] @@ -75,6 +91,10 @@ If you are installing DexScript for the first time, you will see a button called If you already have DexScript, you will see a button called "Update". When you click that button, DexScript will update to the latest version. This will instantly update DexScript, which means you don't have to restart your bot. +#### Configuration + +If you have a `config.toml` file already installed within DexScript, the "Config" button will appear. This button will allow you to access the configuration menu, which will let you modify DexScript's internal settings. + #### Uninstalling If you already have DexScript, you will see a button called "Uninstall". Clicking the uninstall button will uninstall DexScript from your application. This will instantly remove the DexScript package and DexScript commands will unload instantly. diff --git a/poetry.lock b/poetry.lock index 9ff107d..1cc6225 100644 --- a/poetry.lock +++ b/poetry.lock @@ -570,30 +570,30 @@ files = [ [[package]] name = "ruff" -version = "0.11.6" +version = "0.11.13" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "ruff-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:d84dcbe74cf9356d1bdb4a78cf74fd47c740bf7bdeb7529068f69b08272239a1"}, - {file = "ruff-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9bc583628e1096148011a5d51ff3c836f51899e61112e03e5f2b1573a9b726de"}, - {file = "ruff-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2959049faeb5ba5e3b378709e9d1bf0cab06528b306b9dd6ebd2a312127964a"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c5d4e30d9d0de7fedbfb3e9e20d134b73a30c1e74b596f40f0629d5c28a193"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4b9a4e1439f7d0a091c6763a100cef8fbdc10d68593df6f3cfa5abdd9246e"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5edf270223dd622218256569636dc3e708c2cb989242262fe378609eccf1308"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f55844e818206a9dd31ff27f91385afb538067e2dc0beb05f82c293ab84f7d55"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d8f782286c5ff562e4e00344f954b9320026d8e3fae2ba9e6948443fafd9ffc"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01c63ba219514271cee955cd0adc26a4083df1956d57847978383b0e50ffd7d2"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15adac20ef2ca296dd3d8e2bedc6202ea6de81c091a74661c3666e5c4c223ff6"}, - {file = "ruff-0.11.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4dd6b09e98144ad7aec026f5588e493c65057d1b387dd937d7787baa531d9bc2"}, - {file = "ruff-0.11.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:45b2e1d6c0eed89c248d024ea95074d0e09988d8e7b1dad8d3ab9a67017a5b03"}, - {file = "ruff-0.11.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd40de4115b2ec4850302f1a1d8067f42e70b4990b68838ccb9ccd9f110c5e8b"}, - {file = "ruff-0.11.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:77cda2dfbac1ab73aef5e514c4cbfc4ec1fbef4b84a44c736cc26f61b3814cd9"}, - {file = "ruff-0.11.6-py3-none-win32.whl", hash = "sha256:5151a871554be3036cd6e51d0ec6eef56334d74dfe1702de717a995ee3d5b287"}, - {file = "ruff-0.11.6-py3-none-win_amd64.whl", hash = "sha256:cce85721d09c51f3b782c331b0abd07e9d7d5f775840379c640606d3159cae0e"}, - {file = "ruff-0.11.6-py3-none-win_arm64.whl", hash = "sha256:3567ba0d07fb170b1b48d944715e3294b77f5b7679e8ba258199a250383ccb79"}, - {file = "ruff-0.11.6.tar.gz", hash = "sha256:bec8bcc3ac228a45ccc811e45f7eb61b950dbf4cf31a67fa89352574b01c7d79"}, + {file = "ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46"}, + {file = "ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48"}, + {file = "ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492"}, + {file = "ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250"}, + {file = "ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3"}, + {file = "ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b"}, + {file = "ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514"}, ] [[package]] @@ -718,4 +718,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "acca86dc3528062287dee36e3a3ee37e3d8c303c4943a7c75411c05e5e519366" +content-hash = "aed82117906c27eee3590872dcdf92c74dd8e1df5917aa2802d83dc97bc4c435" diff --git a/pyproject.toml b/pyproject.toml index fed695d..9deb925 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,25 @@ -[tool.poetry] +[project] name = "dexscript" -version = "0.5" +version = "1.0.0" description = "" -authors = ["dotzz "] +requires-python = ">=3.12" license = "MIT" +dependencies = [ + "discord.py>=2.5.0", + "ruff>=0.11.13" +] -[tool.poetry.dependencies] -python = ">=3.12" -"discord.py" = "^2.5.0" -ruff = "^0.11.6" +[tool.dexi] +public = true +ballsdex-version = ">=2.22.0" +include-license = true -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.dexi.package] +source = "DexScript/package" +target = "DexScript" +exclude = [] [tool.ruff] line-length = 99 lint.ignore = ["E203", "F704", "F706", "F821"] -lint.extend-select = ["E501", "I001"] \ No newline at end of file +lint.extend-select = ["E501", "I001"]