Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ $ grace new my-awesome-bot
$ cd my-awesome-bot
```

By default, a database is scaffolded alongside your bot. Pass `--no-database` to skip
it, and add one later with `grace generate database`.

#### 3- Set your bot token
Edit the `.env` in the project directory and set `DISCORD_TOKEN`.

Expand Down
37 changes: 28 additions & 9 deletions grace/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from logging import basicConfig, critical
from logging.handlers import RotatingFileHandler
from os import environ
from pathlib import Path
from types import ModuleType
from typing import Any, Dict, Generator, Optional, Union, no_type_check

Expand Down Expand Up @@ -31,11 +30,6 @@ class Application:
__session: Union[Session, None] = None

def __init__(self) -> None:
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 = str(self.config.get("discord", "token"))
self.__engine: Union[Engine, None] = None

Expand Down Expand Up @@ -84,15 +78,25 @@ def extension_modules(self) -> Generator[str, Any, None]:
continue
yield module

@property
def has_database(self) -> bool:
return bool(self.config.database_uri)

@property
def database_infos(self) -> Dict[str, str]:
if not self.has_database:
return {}

return {
"dialect": self.session.bind.dialect.name,
"database": self.session.bind.url.database,
}

@property
def database_exists(self) -> bool:
if not self.has_database:
return False

return database_exists(self.config.database_uri)

def get_extension_module(self, extension_name) -> Union[str, None]:
Expand Down Expand Up @@ -146,11 +150,13 @@ def load_logs(self) -> None:
def load_database(self) -> None:
"""Loads and connects to the database using the loaded config"""

if not self.config.database_uri:
raise ValueError("No database uri.")
database_uri = self.config.database_uri

if not database_uri:
return

self.__engine = create_engine(
self.config.database_uri,
database_uri,
echo=self.config.environment.getboolean("sqlalchemy_echo"),
)

Expand Down Expand Up @@ -180,18 +186,22 @@ def reload_database(self):
def create_database(self):
"""Creates the database for the current loaded config"""

self._require_database()
self.load_database()
create_database(self.config.database_uri)

def drop_database(self):
"""Drops the database for the current loaded config"""

self._require_database()
self.load_database()
drop_database(self.config.database_uri)

def create_tables(self):
"""Creates all the tables for the current loaded database"""

self._require_database()

if not self.__engine:
raise RuntimeError("Database engine is not initialized.")

Expand All @@ -201,8 +211,17 @@ def create_tables(self):
def drop_tables(self):
"""Drops all the tables for the current loaded database"""

self._require_database()

if not self.__engine:
raise RuntimeError("Database engine is not initialized.")

self.load_database()
self.metadata.drop_all(self.__engine)

def _require_database(self) -> None:
if not self.has_database:
raise ConfigError(
"This project has no database configured. "
"Run 'grace generate database' to add one."
)
46 changes: 39 additions & 7 deletions grace/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
| Environment: {env}
| Syncing command: {command_sync}
| Watcher enabled: {watch}
| Using database: {database} with {dialect}
""".rstrip()

DB_INFO = "| Using database: {database} with {dialect}"


@group()
def cli():
Expand All @@ -26,9 +27,7 @@ def cli():

@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")
Expand Down Expand Up @@ -84,6 +83,9 @@ def run(ctx, sync, watch):
def create(ctx):
app = ctx.obj["app"]

if not _require_database(app):
return

if app.database_exists:
return warning("Database already exists")

Expand All @@ -96,6 +98,9 @@ def create(ctx):
def drop(ctx):
app = ctx.obj["app"]

if not _require_database(app):
return

if not app.database_exists:
return warning("Database does not exist")

Expand All @@ -108,6 +113,9 @@ def drop(ctx):
def seed(ctx):
app = ctx.obj["app"]

if not _require_database(app):
return

if not app.database_exists:
return warning("Database does not exist")

Expand All @@ -122,6 +130,9 @@ def seed(ctx):
def up(ctx, revision):
app = ctx.obj["app"]

if not _require_database(app):
return

if not app.database_exists:
return warning("Database does not exist")

Expand All @@ -134,28 +145,49 @@ def up(ctx, revision):
def down(ctx, revision):
app = ctx.obj["app"]

if not _require_database(app):
return

if not app.database_exists:
return warning("Database does not exist")

down_migration(app, revision)


def _load_database(app):
if not app.has_database:
return

if not app.database_exists:
app.create_database()
# app.create_tables()


def _require_database(app) -> bool:
if not app.has_database:
warning(
"This project has no database configured. "
"Run 'grace generate database' to add one."
)
return False
return True


def _show_application_info(app):
message = APP_INFO

if app.has_database:
message = f"{message}\n{DB_INFO}"

info(
APP_INFO.format(
message.format(
discord_version=discord.__version__,
env=app.environment,
pid=getpid(),
command_sync=app.command_sync,
watch=app.watch,
database=app.database_infos["database"],
dialect=app.database_infos["dialect"],
database=app.database_infos.get("database"),
dialect=app.database_infos.get("dialect"),
)
)

Expand Down
11 changes: 9 additions & 2 deletions grace/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,12 @@ def __init__(self) -> None:
self.read("config/environment.cfg")

@property
def database(self) -> SectionProxy:
return self.__config[f"database.{self.__environment}"]
def database(self) -> Union[SectionProxy, None]:
section = f"database.{self.__environment}"

if not self.__config.has_section(section):
return None
return self.__config[section]

@property
def client(self) -> SectionProxy:
Expand All @@ -102,6 +106,9 @@ def current_environment(self) -> Optional[str]:

@property
def database_uri(self) -> Union[str, URL, None]:
if not self.database:
return None

if self.database.get("url"):
return self.database.get("url")

Expand Down
26 changes: 19 additions & 7 deletions grace/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,29 @@ 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] = {}):
"""Generates a template using Cookiecutter.
def generate_template(
self, template_dir: str, variables: dict[str, Any] = {}, output_dir: str = ""
):
"""Generate a template using Cookiecutter.

:param template_dir: The name of the template to generate.
:type template_dir: str
Renders `template_dir` (a subdirectory of `templates_path`) with the given
variables, writing the result into `output_dir` (defaults to the current
working directory). Returns the path to the generated project directory.

:param variables: The variables to pass to the template. (default: {})
:type variables: dict[str, Any]
## Example

```python
self.generate_template(
"project",
variables={"project_name": "my-bot"},
output_dir="my-bot",
)
```
"""
template = str(self.templates_path / template_dir)
cookiecutter(template, extra_context=variables, no_input=True)
return cookiecutter(
template, extra_context=variables, no_input=True, output_dir=output_dir
)

def generate_file(
self, template_dir: str, variables: dict[str, Any] = {}, output_dir: str = ""
Expand Down
31 changes: 31 additions & 0 deletions grace/generators/database_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from logging import info
from pathlib import Path
from shutil import copy

from grace.generator import Generator


class DatabaseGenerator(Generator):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add optional instructions when added later on

Add the following to your `config.py`:
    
    [database]
    config = database.cfg

NAME = "database"
OPTIONS = {}

def generate(self, output_dir: str = ""):
info(f"Creating database in '{output_dir or '.'}'")

self.generate_template(self.NAME, output_dir=output_dir)
self._copy_config_files(output_dir)

def _copy_config_files(self, output_dir: str):
config_dir = Path(output_dir) / "config"
config_dir.mkdir(parents=True, exist_ok=True)

source = self.templates_path / self.NAME
copy(source / "alembic.ini", Path(output_dir) / "alembic.ini")
copy(source / "database.cfg", config_dir / "database.cfg")

def validate(self, *_args, **_kwargs) -> bool:
return True


def generator() -> Generator:
return DatabaseGenerator()
6 changes: 5 additions & 1 deletion grace/generators/project_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from re import match

from grace.generator import Generator
from grace.generators.database_generator import generator as db_generator


class ProjectGenerator(Generator):
Expand All @@ -11,7 +12,7 @@ class ProjectGenerator(Generator):
def generate(self, name: str, database: bool = True):
info(f"Creating '{name}'")

self.generate_template(
project_dir = self.generate_template(
self.NAME,
variables={
"project_name": name,
Expand All @@ -20,6 +21,9 @@ def generate(self, name: str, database: bool = True):
},
)

if database:
db_generator().generate(output_dir=project_dir)

def validate(self, name: str, **_kwargs) -> bool:
"""Validate the project name.

Expand Down
3 changes: 3 additions & 0 deletions grace/generators/templates/database/cookiecutter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"__database_slug": "db"
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ url = ${DATABASE_URL}

[database.development]
adapter = sqlite
database = {{ cookiecutter.__project_slug }}_development.db
database = development.db

[database.test]
adapter = sqlite
database = {{ cookiecutter.__project_slug }}_test.db
database = test.db
11 changes: 0 additions & 11 deletions grace/generators/templates/project/hooks/post_gen_project.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@ of the installation should complete itself and start the bot.
## Script Usage
- **Bot Command(s)**:
- `grace start` : Starts the bot (`ctrl+c` to stop the bot)
{% if cookiecutter.database == "yes" -%}
- **Database Command(s)**:
- `grace db create` : Creates the database and the tables
- `grace db drop` : Deletes the tables and the database
- `grace db seed` : Seeds the tables (Initialize the default values)
- `grace db reset` : Drop, recreate and seeds the database.

{% else -%}
- **Database**: This project was generated without a database. Run
`grace generate database` to add one.
{% endif %}
All commands can take the optional `-e` argument with a string to define the environment.<br>
Available environment: (production, development [default], test)
Loading
Loading