Skip to content

Commit 757529c

Browse files
committed
Update SQLAlchemy, refactor project.
1 parent dae723a commit 757529c

File tree

23 files changed

+566
-352
lines changed

23 files changed

+566
-352
lines changed

Makefile

+7-3
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ export HELP
1818
.PHONY: run restart deploy update format lint clean help
1919

2020
requirements: .requirements.txt
21-
env: .venv/bin/activate
22-
21+
env: ./.venv/bin/activate
2322

2423
.requirements.txt: requirements.txt
2524
$(shell . .venv/bin/activate && pip install -r requirements.txt)
@@ -42,6 +41,7 @@ deploy:
4241

4342
.PHONY: update
4443
update: env
44+
.venv/bin/python3 -m pip install --upgrade pip setuptools wheel
4545
poetry update
4646
poetry export -f requirements.txt --output requirements.txt --without-hashes
4747

@@ -54,7 +54,11 @@ format: env
5454

5555
.PHONY: lint
5656
lint:
57-
flake8 ./app --count --select=E9,F63,F7,F82 --show-source --statistics
57+
flake8 . --count \
58+
--select=E9,F63,F7,F82 \
59+
--exclude .git,.github,__pycache__,.pytest_cache,.venv,logs,creds,.venv,docs,logs \
60+
--show-source \
61+
--statistics
5862

5963

6064
.PHONY: clean

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# SQLAlchemy Tutorial
22

33
![Python](https://img.shields.io/badge/Python-v^3.8-blue.svg?logo=python&longCache=true&logoColor=white&colorB=5e81ac&style=flat-square&colorA=4c566a)
4-
![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-v^1.3.0-blue.svg?longCache=true&logo=python&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a)
4+
![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-v^1.4.0-blue.svg?longCache=true&logo=python&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a)
55
![PyMySQL](https://img.shields.io/badge/PyMySQL-v^1.0.0-red.svg?longCache=true&style=flat-square&logo=scala&logoColor=white&colorA=4c566a&colorB=bf616a)
66
![GitHub Last Commit](https://img.shields.io/github/last-commit/google/skia.svg?style=flat-square&colorA=4c566a&colorB=a3be8c&logo=GitHub)
77
[![GitHub Issues](https://img.shields.io/github/issues/hackersandslackers/sqlalchemy-tutorial.svg?style=flat-square&colorA=4c566a&logo=GitHub&colorB=ebcb8b)](https://github.com/hackersandslackers/sqlalchemy-tutorial/issues)

config.py

+3
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@
1010
# Database connection variables
1111
SQLALCHEMY_DATABASE_URI = environ.get("SQLALCHEMY_DATABASE_URI")
1212
SQLALCHEMY_DATABASE_PEM = environ.get("SQLALCHEMY_DATABASE_PEM")
13+
14+
# Reset data after each run
15+
CLEANUP_DATA = True

database/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .connect import db, session
1+
from .connect import engine, session

database/connect.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
from config import SQLALCHEMY_DATABASE_PEM, SQLALCHEMY_DATABASE_URI
66

77
# Create database engine
8-
db = create_engine(
8+
engine = create_engine(
99
SQLALCHEMY_DATABASE_URI, connect_args={"ssl": {"key": SQLALCHEMY_DATABASE_PEM}}
1010
)
1111

1212
# Create database session
13-
Session = sessionmaker(bind=db)
13+
Session = sessionmaker(bind=engine)
1414
session = Session()

logger.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Custom logger."""
2+
from sys import stdout
3+
4+
from loguru import logger as custom_logger
5+
6+
7+
def formatter(log: dict) -> str:
8+
"""
9+
Format log colors based on level.
10+
11+
:param log: Logged event stored as map containing contextual metadata.
12+
:type log: dict
13+
:returns: str
14+
"""
15+
if log["level"].name == "WARNING":
16+
return (
17+
"<white>{time:MM-DD-YYYY HH:mm:ss}</white> | "
18+
"<light-yellow>{level}</light-yellow>: "
19+
"<light-white>{message}</light-white> \n"
20+
)
21+
elif log["level"].name == "ERROR":
22+
return (
23+
"<white>{time:MM-DD-YYYY HH:mm:ss}</white> | "
24+
"<light-red>{level}</light-red>: "
25+
"<light-white>{message}</light-white> \n"
26+
)
27+
elif log["level"].name == "SUCCESS":
28+
return (
29+
"<white>{time:MM-DD-YYYY HH:mm:ss}</white> | "
30+
"<light-green>{level}</light-green>: "
31+
"<light-white>{message}</light-white> \n"
32+
)
33+
else:
34+
return (
35+
"<white>{time:MM-DD-YYYY HH:mm:ss}</white> | "
36+
"<fg #67c9c4>{level}</fg #67c9c4>: "
37+
"<light-white>{message}</light-white> \n"
38+
)
39+
40+
41+
def create_logger() -> custom_logger:
42+
"""Create custom logger."""
43+
custom_logger.remove()
44+
custom_logger.add(stdout, colorize=True, format=formatter)
45+
return custom_logger
46+
47+
48+
LOGGER = create_logger()

poetry.lock

+198-127
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

requirements.txt

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
colorama==0.4.4; python_version >= "3.5" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.5" and python_full_version >= "3.5.0"
2+
greenlet==1.0.0; python_version >= "3" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3"
23
loguru==0.5.3; python_version >= "3.5"
34
pymysql==1.0.2; python_version >= "3.6"
4-
python-dotenv==0.15.0
5-
sqlalchemy==1.3.22; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
5+
python-dotenv==0.16.0
6+
sqlalchemy==1.4.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
67
win32-setctime==1.0.3; sys_platform == "win32" and python_version >= "3.5"

sqlalchemy_tutorial/__init__.py

+18-11
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
11
"""Run source code for each tutorial found on HackersAndSlackers."""
2-
from database import db, session
2+
from config import CLEANUP_DATA
3+
from logger import LOGGER
34

4-
from .logger import LOGGER
5-
from .part1_connections import fetch_job_listings, update_job_listing
6-
from .part2_orm_models import orm_create_user, orm_delete_user
7-
from .part3_relationships import get_posts, orm_create_data
5+
from .cleanup import cleanup_data
6+
from .part1_connections import execute_queries
7+
from .part2_orm import create_and_delete_users
8+
from .part3_relationships import create_relationships
89

910

1011
def init_script():
1112
"""Run all scripts."""
1213

1314
# Part 1: Executing SELECT and UPDATE queries with an SQLAlchemy engine
15+
LOGGER.info("----------------------------------------------------")
1416
LOGGER.info("Part 1: Executing queries against an SQLAlchemy engine.")
15-
fetch_job_listings(db)
16-
update_job_listing(db)
17+
execute_queries()
1718

1819
# Part 2: Implementing an ORM
20+
LOGGER.info("----------------------------------------------------")
1921
LOGGER.info("Part 2: Create and delete records from a data model.")
20-
user = orm_create_user(session)
21-
orm_delete_user(session, user)
22+
create_and_delete_users()
2223

2324
# Part 3: ORM relationships
25+
LOGGER.info("----------------------------------------------------")
2426
LOGGER.info("Part 3: Utilize relationships to execute JOINs.")
25-
orm_create_data(session)
26-
get_posts(session)
27+
create_relationships()
28+
29+
# OPTIONAL: Reset table data after each run
30+
if CLEANUP_DATA:
31+
LOGGER.info("----------------------------------------------------")
32+
LOGGER.info("Purging all created data...")
33+
cleanup_data()

sqlalchemy_tutorial/cleanup.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Purge all data from database."""
2+
from sqlalchemy import text
3+
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
4+
5+
from database import session
6+
from logger import LOGGER
7+
8+
9+
def cleanup_data():
10+
try:
11+
session.execute(text("SET FOREIGN_KEY_CHECKS=0;"))
12+
session.commit()
13+
session.execute(text("TRUNCATE TABLE comment;"))
14+
session.commit()
15+
session.execute(text("TRUNCATE TABLE post;"))
16+
session.commit()
17+
session.execute(text("TRUNCATE TABLE user;"))
18+
session.commit()
19+
session.execute(text("SET FOREIGN_KEY_CHECKS=1;"))
20+
session.commit()
21+
LOGGER.success("Successfully reset all data.")
22+
except IntegrityError as e:
23+
LOGGER.error(e.orig)
24+
raise e.orig
25+
except SQLAlchemyError as e:
26+
LOGGER.error(f"Unexpected error when resetting data: {e}")
27+
raise e

sqlalchemy_tutorial/logger.py

-21
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1 +1,13 @@
1-
from .engines import fetch_job_listings, update_job_listing
1+
from database import engine
2+
3+
from .queries import fetch_job_listings, update_job_listing
4+
5+
6+
def execute_queries():
7+
"""
8+
Fetch and update records using raw SQL queries.
9+
10+
:return: None
11+
"""
12+
fetch_job_listings(engine)
13+
update_job_listing(engine)

sqlalchemy_tutorial/part1_connections/engines.py

-51
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Execute raw SQL queries against an SQLAlchemy engine."""
2+
from typing import List, Optional
3+
4+
from sqlalchemy import text
5+
from sqlalchemy.engine.base import Engine
6+
7+
from logger import LOGGER
8+
9+
10+
def fetch_job_listings(engine: Engine) -> Optional[List[dict]]:
11+
"""
12+
Select rows from database and parse as list of dicts.
13+
14+
:param engine: Database engine to handle raw SQL queries.
15+
:type engine: engine
16+
17+
:return: Optional[List[dict]]
18+
"""
19+
result = engine.execute(
20+
text(
21+
"SELECT job_id, agency, business_title, \
22+
salary_range_from, salary_range_to \
23+
FROM nyc_jobs ORDER BY RAND();"
24+
)
25+
)
26+
rows = [dict(row) for row in result.fetchall()]
27+
LOGGER.info(f"Selected {result.rowcount} rows: {rows}")
28+
return rows
29+
30+
31+
def update_job_listing(engine: Engine) -> Optional[List[dict]]:
32+
"""
33+
Update row in database with problematic characters escaped.
34+
35+
:param engine: Engine object representing a SQL database.
36+
:type engine: engine
37+
38+
:return: Optional[List[dict]]
39+
"""
40+
result = engine.execute(
41+
text(
42+
"UPDATE nyc_jobs SET business_title = 'Senior QA Scapegoat 🏆', \
43+
job_category = 'Information? <>!#%%Technology!%%#^&%* & Telecom' \
44+
WHERE job_id = 229837;"
45+
)
46+
)
47+
LOGGER.info(
48+
f"Selected {result.rowcount} row: \
49+
{result}"
50+
)
51+
return result.rowcount
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from database import session
2+
from sqlalchemy_tutorial.part2_orm.models import User
3+
4+
from .orm import orm_create_user, orm_delete_user
5+
6+
7+
def create_and_delete_users():
8+
"""
9+
Create a user record via SQLAlchemy's ORM, and subsequently delete it.
10+
11+
:return: None
12+
"""
13+
user = User(
14+
username="admin",
15+
password="Password123lol",
16+
17+
first_name="Todd",
18+
last_name="Birchard",
19+
bio="I write tutorials on the internet.",
20+
avatar_url="https://storage.googleapis.com/hackersandslackers-cdn/authors/[email protected]",
21+
)
22+
user = orm_create_user(session, user)
23+
orm_delete_user(session, user)

sqlalchemy_tutorial/part2_orm_models/models.py renamed to sqlalchemy_tutorial/part2_orm/models.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from sqlalchemy.ext.declarative import declarative_base
44
from sqlalchemy.sql import func
55

6-
from database import db
6+
from database import engine
77

88
Base = declarative_base()
99

@@ -30,4 +30,4 @@ def __repr__(self):
3030
return "<User %r>" % self.username
3131

3232

33-
Base.metadata.create_all(db)
33+
Base.metadata.create_all(engine)

0 commit comments

Comments
 (0)