Skip to content

Commit e0dbe19

Browse files
authored
feat: add database seed commands (#11)
* feat: add faker dependency * feat: seed admin user * feat: seed database quotes
1 parent 16c08db commit e0dbe19

File tree

7 files changed

+318
-113
lines changed

7 files changed

+318
-113
lines changed

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ bpython = "==0.22.1"
1818
ipdb = "==0.13.9"
1919
ptvsd = "==4.3.2"
2020
debugpy = "==1.6.0"
21+
faker = "==13.14.0"
2122

2223
[packages]
2324
flask = "==2.1.2"

Pipfile.lock

Lines changed: 110 additions & 110 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def register_cli_commands(app):
1414
if file.startswith("flask_") and file.endswith(".py"):
1515
# Get the file name
1616
file_name = file[:-3]
17-
command_name = file[4:-3]
17+
command_name = file[6:-3]
1818

1919
# Import the cli command module
2020
module = importlib.import_module(f"cli.{file_name}")

cli/flask_database.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
from random import randrange
2+
from passlib.context import CryptContext
3+
4+
import click
5+
from faker import Faker
6+
from flask import current_app
7+
from flask.cli import with_appcontext
8+
9+
from quotes_api.extensions import odm as database_ext
10+
from quotes_api.api.models import Quote
11+
from quotes_api.auth.models import User
12+
13+
fake = Faker()
14+
15+
16+
@click.group()
17+
def database():
18+
"""Run database related tasks."""
19+
20+
21+
@database.command()
22+
@with_appcontext
23+
def init():
24+
"""
25+
Initialize the database
26+
27+
:return: None
28+
"""
29+
app_config = current_app.config
30+
31+
# Check if databases exist.
32+
33+
# Drop existing databases if they exist. Create them if they don't.
34+
35+
# Create the database collections for Quote, User and TokenBlacklist
36+
37+
38+
@database.command()
39+
@with_appcontext
40+
def seed():
41+
"""
42+
Seed the database with initial values.
43+
:return: None
44+
"""
45+
app_config = current_app.config
46+
pwd_hasher = CryptContext(schemes=["sha256_crypt"])
47+
48+
seed_admin(app_config, User, pwd_hasher)
49+
seed_quotes(Quote, 100)
50+
51+
52+
def seed_admin(config, model, pwd_hasher):
53+
"""
54+
Seed initial admin user.
55+
56+
:param config: Flask application config
57+
:param model: Mongoengine document model
58+
:param pwd_hasher: Crytographic hasher
59+
:return: None
60+
"""
61+
62+
admin = None
63+
admin_username = config["SEED_ADMIN_USERNAME"]
64+
admin_email = config["SEED_ADMIN_EMAIL"]
65+
admin_password = pwd_hasher.hash(config["SEED_ADMIN_PASSWORD"])
66+
67+
click.secho("Seeding admin user...", bg="magenta", fg="white", bold=True)
68+
try:
69+
admin = model.objects.get(username=admin_username)
70+
71+
if admin:
72+
click.secho("Admin user already exists", bg="green", fg="white", bold=True)
73+
click.secho(f"\nAdmin User:\n {admin}", bg="black", fg="white", bold=True)
74+
75+
return None
76+
77+
except Exception:
78+
click.secho("Admin user does not exist.", bg="blue", fg="white", bold=True)
79+
80+
try:
81+
if admin is None:
82+
admin_user_data = {
83+
"username": admin_username,
84+
"email": admin_email,
85+
"password": admin_password,
86+
"active": True,
87+
"roles": ["admin"],
88+
}
89+
click.secho("Creating admin user...", bg="blue", fg="white", bold=True)
90+
91+
admin = model(**admin_user_data)
92+
admin.save()
93+
94+
click.secho("Admin user created.", bg="green", fg="white", bold=True)
95+
click.secho(f"\nAdmin User: {admin}", bg="black", fg="white", bold=True)
96+
97+
except Exception:
98+
click.secho(
99+
"Could not create admin user.", err=True, bg="red", fg="white", bold=True
100+
)
101+
102+
103+
def seed_quotes(model, quotes_number):
104+
"""
105+
Seed fake quotes.
106+
107+
:param config: Flask application config
108+
:param model: Mongoengine document model
109+
:return: None
110+
"""
111+
# Create all the quote models
112+
quote_instances = []
113+
114+
for _ in range(0, quotes_number):
115+
quote = fake.text()
116+
author = fake.name()
117+
image = fake.image_url()
118+
tags = []
119+
for _ in range(randrange(1, 10)):
120+
tag = fake.word().lower()
121+
tags.append(tag)
122+
123+
quote_data = {
124+
"quote_text": quote,
125+
"author_name": author,
126+
"author_image": image,
127+
"tags": tags,
128+
}
129+
quote = model(**quote_data)
130+
quote_instances.append(quote)
131+
132+
click.secho("\nSeeding quotes...", bg="magenta", fg="white", bold=True)
133+
_bulk_insert(model, quote_instances, "Quote documents")
134+
135+
136+
def _bulk_insert(model, data, label):
137+
"""
138+
Bulk insert data to a specific model and log it. This is more
139+
efficient than adding 1 row at a time in a loop.
140+
141+
:param model: Model being affected
142+
:type model: Mongoengine Document
143+
:param data: Document fields to be saved
144+
:type data: list
145+
:param label: Label for the output
146+
:type label: str
147+
:return: None
148+
"""
149+
# Delete the current data
150+
try:
151+
click.secho(
152+
f"Deleting existing {label} documents...",
153+
bg="blue",
154+
fg="white",
155+
bold=True,
156+
)
157+
model.objects().delete()
158+
click.secho("Documents deleted", bg="green", fg="white", bold=True)
159+
160+
except Exception:
161+
click.secho(
162+
f"Could not delete previous documents for {label} collection.",
163+
bg="red",
164+
fg="white",
165+
bold=True,
166+
err=True,
167+
)
168+
return None
169+
170+
# Bulk insert documents
171+
try:
172+
click.secho(
173+
f"Creating {len(data)} documents for {label} collection...",
174+
bg="blue",
175+
fg="white",
176+
bold=True,
177+
)
178+
model.objects.insert(data, load_bulk=False)
179+
click.secho(f"Created {len(data)} {label}", bg="green", fg="white", bold=True)
180+
181+
except Exception:
182+
click.secho(
183+
f"Could not bulk insert documents for {label} collection.",
184+
bg="red",
185+
fg="white",
186+
bold=True,
187+
err=True,
188+
)
189+
return None
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
# Flask
12
FLASK_ENV=development
23
FLASK_APP=wsgi
34
APP_CONFIGURATION=development
45
SERVER=YOUR_LOCAL_SERVER
56
SECRET_KEY=YOUR_SECRET_KEY
6-
JWT_SECRET_KEY=YOUR_JWT_SECRET_KEY
7+
8+
# Flask JWT Extended
9+
JWT_SECRET_KEY=YOUR_JWT_SECRET_KEY
10+
11+
# Database Seed
12+
SEED_ADMIN_USERNAME=YOUR_ADMIN_USERNAME
13+
SEED_ADMIN_EMAIL=YOUR_ADMIN_EMAIL
14+
SEED_ADMIN_PASSWORD=YOUR_ADMIN_PASSWORD
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
# Flask
12
FLASK_ENV=production
23
FLASK_APP=wsgi
34
APP_CONFIGURATION=production
45
SERVER=YOUR_SERVER_URL
56
SECRET_KEY=YOUR_SECRET_KEY
6-
JWT_SECRET_KEY=YOUR_JWT_SECRET_KEY
7+
8+
# Flask JWT Extended
9+
JWT_SECRET_KEY=YOUR_JWT_SECRET_KEY

quotes_api/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ class DevelopmentConfig(Config):
4343
MONGODB_DB = os.getenv("MONGODB_DB")
4444
MONGODB_HOST = os.getenv("MONGODB_HOST")
4545

46+
SEED_ADMIN_USERNAME = os.getenv("SEED_ADMIN_USERNAME")
47+
SEED_ADMIN_EMAIL = os.getenv("SEED_ADMIN_EMAIL")
48+
SEED_ADMIN_PASSWORD = os.getenv("SEED_ADMIN_PASSWORD")
49+
4650

4751
class TestingConfig(Config):
4852
"""Testing environment configuration class."""

0 commit comments

Comments
 (0)