From 315cb5266e7736f89bd274a895a9300a49edcdf5 Mon Sep 17 00:00:00 2001 From: penguinboi Date: Fri, 21 Feb 2025 01:46:13 -0500 Subject: [PATCH 1/3] Started implementation of database generator --- grace/cli.py | 4 +--- grace/generator.py | 14 ++++++++++++-- grace/generators/database_generator.py | 19 +++++++++++++++++++ grace/generators/project_generator.py | 9 +++++++-- .../templates/database/cookiecutter.json | 3 +++ .../__init__.py | 0 .../alembic/env.py | 0 .../alembic/script.py.mako | 0 .../alembic/versions/.gitkeep | 0 .../seed.py | 0 10 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 grace/generators/database_generator.py create mode 100644 grace/generators/templates/database/cookiecutter.json rename grace/generators/templates/{project/{{ cookiecutter.__project_slug }}/db => database/{{ cookiecutter.__database_slug }}}/__init__.py (100%) rename grace/generators/templates/{project/{{ cookiecutter.__project_slug }}/db => database/{{ cookiecutter.__database_slug }}}/alembic/env.py (100%) rename grace/generators/templates/{project/{{ cookiecutter.__project_slug }}/db => database/{{ cookiecutter.__database_slug }}}/alembic/script.py.mako (100%) rename grace/generators/templates/{project/{{ cookiecutter.__project_slug }}/db => database/{{ cookiecutter.__database_slug }}}/alembic/versions/.gitkeep (100%) rename grace/generators/templates/{project/{{ cookiecutter.__project_slug }}/db => database/{{ cookiecutter.__database_slug }}}/seed.py (100%) diff --git a/grace/cli.py b/grace/cli.py index 5cf8187..2bc6245 100644 --- a/grace/cli.py +++ b/grace/cli.py @@ -29,9 +29,7 @@ def generate(): @cli.command() @argument("name") -# This database option is currently disabled since the application and config -# does not currently support it. -# @option("--database/--no-database", default=True) +@option("--database/--no-database", default=True) @pass_context def new(ctx, name, database=True): cmd = generate.get_command(ctx, 'project') diff --git a/grace/generator.py b/grace/generator.py index a2dfaab..08512af 100644 --- a/grace/generator.py +++ b/grace/generator.py @@ -108,7 +108,12 @@ def validate(self, *args, **kwargs): """Validates the arguments passed to the command.""" return True - def generate_template(self, template_dir: str, variables: dict[str, any] = {}): + def generate_template( + self, + template_dir: str, + variables: dict[str, any] = {}, + output_dir: str = "" + ): """Generates a template using Cookiecutter. :param template_dir: The name of the template to generate. @@ -118,7 +123,12 @@ def generate_template(self, template_dir: str, variables: dict[str, any] = {}): :type variables: dict[str, any] """ template = str(self.templates_path / template_dir) - cookiecutter(template, extra_context=variables, no_input=True) + cookiecutter( + template, + extra_context=variables, + no_input=True, + output_dir=output_dir + ) def generate_file( self, diff --git a/grace/generators/database_generator.py b/grace/generators/database_generator.py new file mode 100644 index 0000000..8d648f3 --- /dev/null +++ b/grace/generators/database_generator.py @@ -0,0 +1,19 @@ +from grace.generator import Generator +from re import match +from logging import info + + +class DatabaseGenerator(Generator): + NAME = 'database' + OPTIONS = {} + + def generate(self, output_dir: str = ""): + info(f"Creating database") + self.generate_template(self.NAME, output_dir=output_dir) + + def validate(self, *_args, **_kwargs) -> bool: + return True + + +def generator() -> Generator: + return DatabaseGenerator() \ No newline at end of file diff --git a/grace/generators/project_generator.py b/grace/generators/project_generator.py index ed0bc35..ecadff8 100644 --- a/grace/generators/project_generator.py +++ b/grace/generators/project_generator.py @@ -1,4 +1,5 @@ from grace.generator import Generator +from grace.generators.database_generator import generator as db_generator from re import match from logging import info @@ -12,12 +13,16 @@ class ProjectGenerator(Generator): def generate(self, name: str, database: bool = True): info(f"Creating '{name}'") - self.generate_template(self.NAME, values={ + self.generate_template(self.NAME, variables={ "project_name": name, "project_description": "", - "database": "yes" if database else "no" }) + if database: + # Should probably be moved into it's own generator so we can + # generate add the database later on. + database_generator = db_generator().generate(output_dir=name) + def validate(self, name: str, **_kwargs) -> bool: """Validate the project name. diff --git a/grace/generators/templates/database/cookiecutter.json b/grace/generators/templates/database/cookiecutter.json new file mode 100644 index 0000000..c2ed5e5 --- /dev/null +++ b/grace/generators/templates/database/cookiecutter.json @@ -0,0 +1,3 @@ +{ + "__database_slug": "db" +} \ No newline at end of file diff --git a/grace/generators/templates/project/{{ cookiecutter.__project_slug }}/db/__init__.py b/grace/generators/templates/database/{{ cookiecutter.__database_slug }}/__init__.py similarity index 100% rename from grace/generators/templates/project/{{ cookiecutter.__project_slug }}/db/__init__.py rename to grace/generators/templates/database/{{ cookiecutter.__database_slug }}/__init__.py diff --git a/grace/generators/templates/project/{{ cookiecutter.__project_slug }}/db/alembic/env.py b/grace/generators/templates/database/{{ cookiecutter.__database_slug }}/alembic/env.py similarity index 100% rename from grace/generators/templates/project/{{ cookiecutter.__project_slug }}/db/alembic/env.py rename to grace/generators/templates/database/{{ cookiecutter.__database_slug }}/alembic/env.py diff --git a/grace/generators/templates/project/{{ cookiecutter.__project_slug }}/db/alembic/script.py.mako b/grace/generators/templates/database/{{ cookiecutter.__database_slug }}/alembic/script.py.mako similarity index 100% rename from grace/generators/templates/project/{{ cookiecutter.__project_slug }}/db/alembic/script.py.mako rename to grace/generators/templates/database/{{ cookiecutter.__database_slug }}/alembic/script.py.mako diff --git a/grace/generators/templates/project/{{ cookiecutter.__project_slug }}/db/alembic/versions/.gitkeep b/grace/generators/templates/database/{{ cookiecutter.__database_slug }}/alembic/versions/.gitkeep similarity index 100% rename from grace/generators/templates/project/{{ cookiecutter.__project_slug }}/db/alembic/versions/.gitkeep rename to grace/generators/templates/database/{{ cookiecutter.__database_slug }}/alembic/versions/.gitkeep diff --git a/grace/generators/templates/project/{{ cookiecutter.__project_slug }}/db/seed.py b/grace/generators/templates/database/{{ cookiecutter.__database_slug }}/seed.py similarity index 100% rename from grace/generators/templates/project/{{ cookiecutter.__project_slug }}/db/seed.py rename to grace/generators/templates/database/{{ cookiecutter.__database_slug }}/seed.py From 27729b914378129a1c91e1574ffb46f895323636 Mon Sep 17 00:00:00 2001 From: penguinboi Date: Fri, 21 Feb 2025 03:39:39 -0500 Subject: [PATCH 2/3] crude implementation of no database --- grace/application.py | 46 ++++++++++++------- grace/cli.py | 20 ++++---- grace/config.py | 13 +++++- grace/generators/project_generator.py | 3 +- .../templates/project/cookiecutter.json | 2 +- .../config/{settings.cfg => config.cfg} | 4 ++ 6 files changed, 58 insertions(+), 30 deletions(-) rename grace/generators/templates/project/{{ cookiecutter.__project_slug }}/config/{settings.cfg => config.cfg} (82%) diff --git a/grace/application.py b/grace/application.py index 925de1b..cdea148 100644 --- a/grace/application.py +++ b/grace/application.py @@ -39,11 +39,6 @@ class Application: __base: DeclarativeMeta = declarative_base() def __init__(self): - database_config_path: Path = Path("config/database.cfg") - - if not database_config_path.exists(): - raise ConfigError("Unable to find the 'database.cfg' file.") - self.__token: str = self.config.get("discord", "token") self.__engine: Union[Engine, None] = None @@ -78,6 +73,20 @@ def config(self) -> Config: def client(self) -> SectionProxy: return self.config.client + @property + def database(self) -> SectionProxy: + return self.config.database + + @property + def database_infos(self) -> Dict[str, str]: + if not self.database: + return {} + + return { + "dialect": self.session.bind.dialect.name, + "database": self.session.bind.url.database + } + @property def extension_modules(self) -> Generator[str, Any, None]: """Generate the extensions modules""" @@ -90,17 +99,6 @@ def extension_modules(self) -> Generator[str, Any, None]: continue yield module - @property - def database_infos(self) -> Dict[str, str]: - return { - "dialect": self.session.bind.dialect.name, - "database": self.session.bind.url.database - } - - @property - def database_exists(self): - return database_exists(self.config.database_uri) - def get_extension_module(self, extension_name) -> Union[str, None]: """Return the extension from the given extension name""" @@ -150,17 +148,23 @@ def load_logs(self): def load_database(self): """Loads and connects to the database using the loaded config""" + if not self.database: + return None self.__engine = create_engine( self.config.database_uri, echo=self.config.environment.getboolean("sqlalchemy_echo") ) - if self.database_exists: + if database_exists(self.config.database_uri): try: self.__engine.connect() except OperationalError as e: critical(f"Unable to load the 'database': {e}") + else: + self.create_database() + self.create_tables() + def unload_database(self): """Unloads the current database""" @@ -179,24 +183,32 @@ def reload_database(self): def create_database(self): """Creates the database for the current loaded config""" + if not self.database: + return None self.load_database() create_database(self.config.database_uri) def drop_database(self): """Drops the database for the current loaded config""" + if not self.database: + return None self.load_database() drop_database(self.config.database_uri) def create_tables(self): """Creates all the tables for the current loaded database""" + if not self.database: + return None self.load_database() self.base.metadata.create_all(self.__engine) def drop_tables(self): """Drops all the tables for the current loaded database""" + if not self.database: + return None self.load_database() self.base.metadata.drop_all(self.__engine) diff --git a/grace/cli.py b/grace/cli.py index 2bc6245..8b97c6c 100644 --- a/grace/cli.py +++ b/grace/cli.py @@ -12,6 +12,9 @@ | PID: {pid} | Environment: {env} | Syncing command: {command_sync} +""".rstrip() + +DB_INFO = """ | Using database: {database} with {dialect} """.rstrip() @@ -46,7 +49,6 @@ def run(environment=None, sync=None): from bot import app, run _loading_application(app, environment, sync) - _load_database(app) _show_application_info(app) run() @@ -58,19 +60,19 @@ def _loading_application(app, environment, command_sync): app.load(environment, command_sync=command_sync) -def _load_database(app): - if not app.database_exists: - app.create_database() - app.create_tables() - def _show_application_info(app): - info(APP_INFO.format( + info_message = APP_INFO + + if app.database: + info_message += DB_INFO + + info(info_message.format( discord_version=discord.__version__, env=app.config.current_environment, pid=getpid(), command_sync=app.command_sync, - database=app.database_infos["database"], - dialect=app.database_infos["dialect"], + database=app.database_infos.get("database"), + dialect=app.database_infos.get("dialect"), )) diff --git a/grace/config.py b/grace/config.py index 119262c..f17c976 100644 --- a/grace/config.py +++ b/grace/config.py @@ -75,12 +75,19 @@ def __init__(self): interpolation=EnvironmentInterpolation() ) - self.read("config/settings.cfg") - self.read("config/database.cfg") + self.read("config/config.cfg") self.read("config/environment.cfg") + self.__database_config = self.get("database", "config") + + if self.__database_config: + self.read(f"config/{self.__database_config}") + @property def database_uri(self) -> Union[str, URL]: + if not self.database: + return None + if self.database.get("url"): return self.database.get("url") @@ -95,6 +102,8 @@ def database_uri(self) -> Union[str, URL]: @property def database(self) -> SectionProxy: + if not self.__database_config: + return None return self.__config[f"database.{self.__environment}"] @property diff --git a/grace/generators/project_generator.py b/grace/generators/project_generator.py index ecadff8..527d2e5 100644 --- a/grace/generators/project_generator.py +++ b/grace/generators/project_generator.py @@ -16,12 +16,13 @@ def generate(self, name: str, database: bool = True): self.generate_template(self.NAME, variables={ "project_name": name, "project_description": "", + "database": database }) if database: # Should probably be moved into it's own generator so we can # generate add the database later on. - database_generator = db_generator().generate(output_dir=name) + db_generator().generate(output_dir=name) def validate(self, name: str, **_kwargs) -> bool: """Validate the project name. diff --git a/grace/generators/templates/project/cookiecutter.json b/grace/generators/templates/project/cookiecutter.json index 6e8044a..a0e09ae 100644 --- a/grace/generators/templates/project/cookiecutter.json +++ b/grace/generators/templates/project/cookiecutter.json @@ -3,5 +3,5 @@ "__project_slug": "{{ cookiecutter.project_name|lower|replace('-', '_') }}", "__project_class": "{{ cookiecutter.project_name|title|replace('-', '') }}", "project_description": "{{ cookiecutter.project_description }}", - "database": ["yes", "no"] + "database": [true, false] } diff --git a/grace/generators/templates/project/{{ cookiecutter.__project_slug }}/config/settings.cfg b/grace/generators/templates/project/{{ cookiecutter.__project_slug }}/config/config.cfg similarity index 82% rename from grace/generators/templates/project/{{ cookiecutter.__project_slug }}/config/settings.cfg rename to grace/generators/templates/project/{{ cookiecutter.__project_slug }}/config/config.cfg index 9586d10..b40c9ae 100644 --- a/grace/generators/templates/project/{{ cookiecutter.__project_slug }}/config/settings.cfg +++ b/grace/generators/templates/project/{{ cookiecutter.__project_slug }}/config/config.cfg @@ -8,3 +8,7 @@ guild_id = ${GUILD_ID} ; Although it is possible to set directly your discord token here, we recommend, for security reasons, that you set ; your discord token as an environment variable called 'DISCORD_TOKEN'. token = ${DISCORD_TOKEN} +{% if cookiecutter.database %} +[database] +config = database.cfg +{% endif %} \ No newline at end of file From ce8a449c46c336f8d0d5f315d3a0ac5133ebaa98 Mon Sep 17 00:00:00 2001 From: penguinboi Date: Sat, 27 Sep 2025 23:14:12 -0400 Subject: [PATCH 3/3] Fix tests --- grace/generator.py | 7 +++--- grace/generators/database_generator.py | 6 ++++-- grace/generators/project_generator.py | 6 +++--- tests/generators/test_project_generator.py | 25 +++++++++++++++------- tests/test_generator.py | 7 +++++- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/grace/generator.py b/grace/generator.py index 08512af..4335d21 100644 --- a/grace/generator.py +++ b/grace/generator.py @@ -27,14 +27,15 @@ def generator() -> Generator: from grace.importer import import_package_modules from grace.exceptions import GeneratorError, ValidationError, NoTemplateError from cookiecutter.main import cookiecutter -from jinja2 import Environment, PackageLoader, Template +from jinja2 import Environment, PackageLoader def register_generators(command_group: Group): """Registers generator commands to the given Click command group. - This function dynamically imports all modules in the `grace.generators` package - and registers each module's `generator` command to the provided `command_group`. + This function dynamically imports all modules in the `grace.generators` + package and registers each module's `generator` command to the provided + `command_group`. :param command_group: The Click command group to register the generators to. :type command_group: Group diff --git a/grace/generators/database_generator.py b/grace/generators/database_generator.py index 8d648f3..6413d36 100644 --- a/grace/generators/database_generator.py +++ b/grace/generators/database_generator.py @@ -8,8 +8,10 @@ class DatabaseGenerator(Generator): OPTIONS = {} def generate(self, output_dir: str = ""): - info(f"Creating database") - self.generate_template(self.NAME, output_dir=output_dir) + info(f"Creating database at {output_dir}") + self.generate_template(self.NAME, variables={ + "output_dir": output_dir + }) def validate(self, *_args, **_kwargs) -> bool: return True diff --git a/grace/generators/project_generator.py b/grace/generators/project_generator.py index 527d2e5..14ae1db 100644 --- a/grace/generators/project_generator.py +++ b/grace/generators/project_generator.py @@ -15,12 +15,12 @@ def generate(self, name: str, database: bool = True): self.generate_template(self.NAME, variables={ "project_name": name, - "project_description": "", + "project_description": "", "database": database }) if database: - # Should probably be moved into it's own generator so we can + # Should probably be moved into its own generator so we can # generate add the database later on. db_generator().generate(output_dir=name) @@ -45,4 +45,4 @@ def validate(self, name: str, **_kwargs) -> bool: def generator() -> Generator: - return ProjectGenerator() \ No newline at end of file + return ProjectGenerator() diff --git a/tests/generators/test_project_generator.py b/tests/generators/test_project_generator.py index d9a9605..cc9477d 100644 --- a/tests/generators/test_project_generator.py +++ b/tests/generators/test_project_generator.py @@ -1,4 +1,5 @@ import pytest +from unittest.mock import call from grace.generator import Generator from grace.generators.project_generator import ProjectGenerator @@ -12,14 +13,22 @@ def test_generate_project_with_database(mocker, generator): """Test if the generate method creates the correct template with a database.""" mock_generate_template = mocker.patch.object(Generator, 'generate_template') name = "example-project" - + generator.generate(name, database=True) - mock_generate_template.assert_called_once_with('project', values={ - 'project_name': name, - 'project_description': '', - 'database': 'yes' - }) + expected_calls = [ + call('project', variables={ + 'project_name': name, + 'project_description': '', + 'database': True + }), + call('database', variables={ + 'output_dir': name + }) + ] + + mock_generate_template.assert_has_calls(expected_calls) + assert mock_generate_template.call_count == 2 def test_generate_project_without_database(mocker, generator): @@ -29,10 +38,10 @@ def test_generate_project_without_database(mocker, generator): generator.generate(name, database=False) - mock_generate_template.assert_called_once_with('project', values={ + mock_generate_template.assert_called_once_with('project', variables={ 'project_name': name, 'project_description': '', - 'database': 'no' + 'database': False }) diff --git a/tests/test_generator.py b/tests/test_generator.py index 1d5fe7b..24fbde7 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -30,7 +30,12 @@ def test_generate_template(generator): with patch('grace.generator.cookiecutter') as cookiecutter: generator.generate_template('project', variables={}) template_path = str(generator.templates_path / 'project') - cookiecutter.assert_called_once_with(template_path, extra_context={}, no_input=True) + cookiecutter.assert_called_once_with( + template_path, + extra_context={}, + no_input=True, + output_dir='' + ) def test_generate(generator):