Skip to content

Commit da1b5fe

Browse files
committedAug 31, 2023
Refactor tutorial.
1 parent f036408 commit da1b5fe

File tree

15 files changed

+131
-164
lines changed

15 files changed

+131
-164
lines changed
 

‎.env.example

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
1-
SQLALCHEMY_DATABASE_URI=mysql+pymysql://myuser:mypassword@host.example.com:1234/mydatabase
2-
SQLALCHEMY_DATABASE_PEM="-----BEGIN CERTIFICATE-----\nghdfigfjvgkjdfvfjkhcvdfjhvfghjbfdvfjshdvjghvfgjvcfjdcvckdjh\n-----END CERTIFICATE-----\n"
1+
DATABASE_USERNAME="yourusername"
2+
DATABASE_PASSWORD="yourpassword"
3+
DATABASE_HOST="db.host.com"
4+
DATABASE_PORT=12345
5+
DATABASE_TABLE="table"
6+
DATABASE_CERT_FILE="ca-certificate.crt"
File renamed without changes.

‎.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ venv/
8989
ENV/
9090
env.bak/
9191
venv.bak/
92+
creds/
93+
**/*.crt
9294

9395
# Spyder project settings
9496
.spyderproject
@@ -115,6 +117,7 @@ ca-certificate.crt
115117

116118
# Idea
117119
.idea/
120+
.vscode/
118121

119122
# logs
120123
logs/*

‎README.md

+16-13
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# SQLAlchemy Tutorial
22

3-
![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.4.0-blue.svg?longCache=true&logo=python&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a)
5-
![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)
3+
![Python](https://img.shields.io/badge/Python-v^3.10-blue.svg?logo=python&longCache=true&logoColor=white&colorB=5e81ac&style=flat-square&colorA=4c566a)
4+
![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-v^2.0.20-blue.svg?longCache=true&logo=python&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a)
5+
![PyMySQL](https://img.shields.io/badge/PyMySQL-v^1.1.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)
88
[![GitHub Stars](https://img.shields.io/github/stars/hackersandslackers/sqlalchemy-tutorial.svg?style=flat-square&colorA=4c566a&logo=GitHub&colorB=ebcb8b)](https://github.com/hackersandslackers/sqlalchemy-tutorial/stargazers)
99
[![GitHub Forks](https://img.shields.io/github/forks/hackersandslackers/sqlalchemy-tutorial.svg?style=flat-square&colorA=4c566a&logo=GitHub&colorB=ebcb8b)](https://github.com/hackersandslackers/sqlalchemy-tutorial/network)
1010

11-
![SQLAlchemy Tutorial](https://github.com/hackersandslackers/sqlalchemy-tutorial/blob/master/.github/sqlalchemy@2x.jpg?raw=true)
11+
![SQLAlchemy Tutorial](https://github.com/hackersandslackers/sqlalchemy-tutorial/blob/master/.github/img/sqlalchemy@2x.jpg?raw=true)
1212

1313
This repository contains the source code for a four-part tutorial series on SQLAlchemy:
1414

@@ -17,29 +17,32 @@ This repository contains the source code for a four-part tutorial series on SQLA
1717
3. [Relationships in SQLAlchemy Data Models](https://hackersandslackers.com/sqlalchemy-data-models)
1818
4. [Constructing Database Queries with SQLAlchemy](https://hackersandslackers.com/database-queries-sqlalchemy-orm)
1919

20-
# Getting Started
20+
## Getting Started
2121

2222
Get set up locally in two steps:
2323

2424
### Environment Variables
2525

2626
Replace the values in **.env.example** with your values and rename this file to **.env**:
2727

28-
29-
* `SQLALCHEMY_DATABASE_URI`: Connection URI of a SQL database.
30-
* `SQLALCHEMY_DATABASE_PEM` _(Optional)_: PEM key for databases requiring an SSL connection.
28+
* `DATABASE_USERNAME`: Username for a SQL database.
29+
* `DATABASE_PASSWORD`: Corresponding password for the above SQL database user.
30+
* `DATABASE_HOST`: Host of the SQL database.
31+
* `DATABASE_PORT`: Numerical port of the SQL database.
32+
* `DATABASE_TABLE`: Name of the SQL database table.
33+
* `DATABASE_CERT_FILE` _(optional)_: Path to SSL certificate file for database.
3134

3235
*Remember never to commit secrets saved in .env files to Github.*
3336

3437
### Installation
3538

36-
Get up and running with `make deploy`:
39+
Get up and running with `make run`:
3740

3841
```shell
39-
$ git clone https://github.com/hackersandslackers/sqlalchemy-tutorial.git
40-
$ cd sqlalchemy-tutorial
41-
$ make deploy
42-
```
42+
git clone https://github.com/hackersandslackers/sqlalchemy-tutorial.git
43+
cd sqlalchemy-tutorial
44+
make run
45+
```
4346

4447
-----
4548

‎config.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Database config."""
2-
from os import environ, path
2+
from os import getenv, path
33

44
from dotenv import load_dotenv
55

@@ -8,8 +8,14 @@
88
load_dotenv(path.join(basedir, ".env"))
99

1010
# Database connection variables
11-
SQLALCHEMY_DATABASE_URI = environ.get("SQLALCHEMY_DATABASE_URI")
12-
SQLALCHEMY_DATABASE_PEM = environ.get("SQLALCHEMY_DATABASE_PEM")
11+
DATABASE_USERNAME = getenv("DATABASE_USERNAME")
12+
DATABASE_PASSWORD = getenv("DATABASE_PASSWORD")
13+
DATABASE_HOST = getenv("DATABASE_HOST")
14+
DATABASE_PORT = getenv("DATABASE_PORT")
15+
DATABASE_TABLE = getenv("DATABASE_TABLE")
16+
DATABASE_CERT_FILE = getenv("DATABASE_CERT_FILE")
17+
18+
SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{DATABASE_USERNAME}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_TABLE}?ssl_ca={DATABASE_CERT_FILE}"
1319

1420
# Reset data after each run
1521
CLEANUP_DATA = False

‎database/__init__.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,12 @@
1-
from .db import engine, session
1+
"""Create SQLAlchemy engine and session objects."""
2+
from sqlalchemy import create_engine
3+
from sqlalchemy.orm import sessionmaker
4+
5+
from config import SQLALCHEMY_DATABASE_URI
6+
7+
# Create database engine
8+
engine = create_engine(SQLALCHEMY_DATABASE_URI, echo=False)
9+
10+
# Create database session
11+
Session = sessionmaker(bind=engine)
12+
session = Session()

‎database/db.py

-14
This file was deleted.

‎logger.py

+10-25
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,19 @@ def formatter(log: dict) -> str:
88
"""
99
Format log colors based on level.
1010
11-
:param log: Logged event stored as map containing contextual metadata.
12-
:type log: dict
11+
:param dict log: Logged event stored as map containing contextual metadata.
12+
1313
:returns: str
1414
"""
15+
if log["level"].name == "INFO":
16+
return "<fg #5278a3>{time:MM-DD-YYYY HH:mm:ss}</fg #5278a3> | <fg #b3cfe7>{level}</fg #b3cfe7>: <light-white>{message}</light-white>\n"
1517
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-
)
18+
return "<fg #5278a3>{time:MM-DD-YYYY HH:mm:ss}</fg #5278a3> | <fg #b09057>{level}</fg #b09057>: <light-white>{message}</light-white>\n"
19+
if log["level"].name == "SUCCESS":
20+
return "<fg #5278a3>{time:MM-DD-YYYY HH:mm:ss}</fg #5278a3> | <fg #6dac77>{level}</fg #6dac77>: <light-white>{message}</light-white>\n"
21+
if log["level"].name == "ERROR":
22+
return "<fg #5278a3>{time:MM-DD-YYYY HH:mm:ss}</fg #5278a3> | <fg #a35252>{level}</fg #a35252>: <light-white>{message}</light-white>\n"
23+
return "<fg #5278a3>{time:MM-DD-YYYY HH:mm:ss}</fg #5278a3> | <fg #b3cfe7>{level}</fg #b3cfe7>: <light-white>{message}</light-white>\n"
3924

4025

4126
def create_logger() -> custom_logger:

‎poetry.lock

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

‎pyproject.toml

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "sqlalchemy-tutorial"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
description = "Use SQLAlchemy to connect, query, and interact with relational databases."
55
authors = ["Todd Birchard <todd@hackersandslackers.com>"]
66
maintainers = ["Todd Birchard <todd@hackersandslackers.com>"]
@@ -9,13 +9,7 @@ readme = "README.md"
99
homepage = "https://github.com/hackersandslackers/sqlalchemy-tutorial/"
1010
repository = "https://github.com/hackersandslackers/sqlalchemy-tutorial/"
1111
documentation = "https://github.com/hackersandslackers/sqlalchemy-tutorial/"
12-
keywords = [
13-
"SQL",
14-
"SQLAlchemy",
15-
"ORM",
16-
"Relational Databases",
17-
"RDBMS"
18-
]
12+
keywords = ["SQL", "SQLAlchemy", "ORM", "Relational Databases", "RDBMS"]
1913

2014
[tool.poetry.dependencies]
2115
python = "^3.10, <4.0"
@@ -37,5 +31,11 @@ run = "main:init_script"
3731
issues = "https://github.com/hackersandslackers/sqlalchemy-tutorial/issues"
3832

3933
[build-system]
40-
requires = ["poetry>=0.12"]
34+
requires = ["poetry>=1.6.1"]
4135
build-backend = "poetry.masonry.api"
36+
37+
[tool.black]
38+
line-length = 120
39+
40+
[tool.pylint.'MESSAGES CONTROL']
41+
disable = "C0103,C0301,W0703,W0621"

‎sqlalchemy_tutorial/cleanup.py

-1
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,3 @@ def cleanup_data():
2626
LOGGER.error(f"SQLAlchemyError error when resetting data: {e}")
2727
except Exception as e:
2828
LOGGER.error(f"Unexpected error when resetting data: {e}")
29-

‎sqlalchemy_tutorial/part1_connections/queries.py

+35-26
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from sqlalchemy import text
55
from sqlalchemy.engine.base import Engine
6+
from sqlalchemy.exc import SQLAlchemyError
67

78
from logger import LOGGER
89

@@ -11,41 +12,49 @@ def fetch_job_listings(engine: Engine) -> Optional[List[dict]]:
1112
"""
1213
Select rows from database and parse as list of dicts.
1314
14-
:param engine: Database engine to handle raw SQL queries.
15-
:type engine: engine
15+
:param Engine engine: Database engine to handle raw SQL queries.
1616
1717
:return: Optional[List[dict]]
1818
"""
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() LIMIT 10;"
24-
)
25-
)
26-
rows = [dict(row) for row in result.fetchall()]
27-
LOGGER.info(f"Selected {result.rowcount} rows: {rows}")
28-
return rows
19+
try:
20+
with engine.begin() as conn:
21+
result = conn.execute(
22+
text(
23+
"SELECT job_id, agency, business_title, \
24+
salary_range_from, salary_range_to \
25+
FROM nyc_jobs ORDER BY RAND() LIMIT 10;"
26+
),
27+
)
28+
results = result.fetchall()
29+
results_dict = [row._asdict() for row in results]
30+
LOGGER.info(f"Selected {result.rowcount} rows.")
31+
return results_dict
32+
except SQLAlchemyError as e:
33+
LOGGER.error(f"SQLAlchemyError while fetching records: {e}")
34+
except Exception as e:
35+
LOGGER.error(f"Unexpected error while fetching records: {e}")
2936

3037

3138
def update_job_listing(engine: Engine) -> Optional[List[dict]]:
3239
"""
3340
Update row in database with problematic characters escaped.
3441
35-
:param engine: Engine object representing a SQL database.
36-
:type engine: engine
42+
:param Engine engine: Database engine to handle raw SQL queries.
3743
3844
:return: Optional[List[dict]]
3945
"""
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
46+
try:
47+
with engine.begin() as conn:
48+
result = conn.execute(
49+
text(
50+
"UPDATE nyc_jobs SET business_title = 'Senior QA Scapegoat 🏆', \
51+
job_category = 'Information? <>!#%%Technology!%%#^&%* & Telecom' \
52+
WHERE job_id = 229837;"
53+
)
54+
)
55+
LOGGER.info(f"Updated {result.rowcount} row: {result}")
56+
return result
57+
except SQLAlchemyError as e:
58+
LOGGER.error(f"SQLAlchemyError while updating records: {e}")
59+
except Exception as e:
60+
LOGGER.error(f"Unexpected error while updating records: {e}")

‎sqlalchemy_tutorial/part3_relationships/joins.py

+7-37
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,15 @@ def get_all_posts(session: Session, admin_user: User):
99
"""
1010
Fetch all posts belonging to an author user.
1111
12-
:param session: SQLAlchemy database session.
13-
:type session: Session
14-
:param admin_user: Author of blog posts.
15-
:type admin_user: User
12+
:param Session session: SQLAlchemy database session.
13+
:param User admin_user: Author of blog posts.
1614
1715
:return: None
1816
"""
19-
posts = (
20-
session.query(Post)
21-
.join(User, Post.author_id == User.id)
22-
.filter_by(username=admin_user.username)
23-
.all()
24-
)
17+
LOGGER.info("Fetching posts with child comments...")
18+
posts = session.query(Post).join(User, Post.author_id == User.id).filter_by(username=admin_user.username).all()
2519
for post in posts:
26-
post_record = {
27-
"post_id": post.id,
28-
"title": post.title,
29-
"summary": post.summary,
30-
"status": post.status,
31-
"feature_image": post.feature_image,
32-
"author": {
33-
"id": post.author_id,
34-
"username": post.author.username,
35-
"first_name": post.author.first_name,
36-
"last_name": post.author.last_name,
37-
"role": post.author.role,
38-
},
39-
}
40-
LOGGER.info(post_record)
20+
LOGGER.success(f"Fetched posts by user: {post}")
4121

4222

4323
def get_all_comments(session: Session):
@@ -49,17 +29,7 @@ def get_all_comments(session: Session):
4929
5030
:return: None
5131
"""
32+
LOGGER.info("Joining comments with parent posts...")
5233
comments = session.query(Comment).join(Post, Post.id == Comment.post_id).all()
5334
for comment in comments:
54-
comment_record = {
55-
"comment_id": comment.id,
56-
"body_summary": f"{comment.body[:50]}...",
57-
"upvotes": comment.upvotes,
58-
"comment_author_id": comment.user_id,
59-
"post": {
60-
"slug": comment.post.slug,
61-
"title": comment.post.title,
62-
"post_author": comment.post.author.username,
63-
},
64-
}
65-
LOGGER.info(comment_record)
35+
LOGGER.success(f"Joined comments with parent posts: {comment}")

‎sqlalchemy_tutorial/part3_relationships/models.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class User(Base):
2828
updated_at = Column(DateTime, onupdate=func.now())
2929

3030
def __repr__(self):
31-
return "<User %r>" % self.username
31+
return f"<User id={self.id}, username={self.username}, email={self.email}>"
3232

3333

3434
class Comment(Base):
@@ -48,7 +48,7 @@ class Comment(Base):
4848
user = relationship("User", backref="comment")
4949

5050
def __repr__(self):
51-
return "<Comment %r>" % self.id
51+
return f"<Comment id={self.id}, post_id={self.post_id}, user_id={self.user_id}, upvotes={self.upvotes}, created_at={self.created_at}>"
5252

5353

5454
class Post(Base):
@@ -72,7 +72,7 @@ class Post(Base):
7272
comments = relationship("Comment", backref="post")
7373

7474
def __repr__(self):
75-
return "<Post %r>" % self.slug
75+
return f"<Post id={self.id}, slug={self.slug}, title={self.title}, body={self.body}>"
7676

7777

7878
Base.metadata.create_all(engine)
+17-30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
"""Create records related to one another via SQLAlchemy's ORM."""
2-
from typing import Tuple
3-
42
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
53
from sqlalchemy.orm import Session
64

@@ -12,17 +10,13 @@ def create_user(session: Session, user: User) -> User:
1210
"""
1311
Create a new user if username isn't already taken.
1412
15-
:param session: SQLAlchemy database session.
16-
:type session: Session
17-
:param user: New user record to create.
18-
:type user: User
13+
:param Session session: SQLAlchemy database session.
14+
:param User user: New user record to create.
1915
2016
:return: Optional[User]
2117
"""
2218
try:
23-
existing_user = (
24-
session.query(User).filter(User.username == user.username).first()
25-
)
19+
existing_user = session.query(User).filter(User.username == user.username).first()
2620
if existing_user is None:
2721
session.add(user) # Add the user
2822
session.commit() # Commit the change
@@ -42,10 +36,8 @@ def create_post(session: Session, post: Post) -> Post:
4236
"""
4337
Create a post.
4438
45-
:param session: SQLAlchemy database session.
46-
:type session: Session
47-
:param post: Blog post to be created.
48-
:type post: Post
39+
:param Session session: SQLAlchemy database session.
40+
:param Post post: Blog post to be created.
4941
5042
:return: Post
5143
"""
@@ -54,29 +46,24 @@ def create_post(session: Session, post: Post) -> Post:
5446
if existing_post is None:
5547
session.add(post) # Add the post
5648
session.commit() # Commit the change
57-
LOGGER.success(
58-
f"Created post {post} published by user {post.author.username}"
59-
)
49+
LOGGER.success(f"Created post {post} published by user {post.author.username}")
6050
return session.query(Post).filter(Post.slug == post.slug).first()
61-
else:
62-
LOGGER.warning(f"Post already exists in database: {post}")
63-
return existing_post
51+
LOGGER.warning(f"Post already exists in database: {post}")
52+
return existing_post
6453
except IntegrityError as e:
65-
LOGGER.error(e.orig)
66-
raise e.orig
54+
LOGGER.error(f"IntegrityError error when creating user: {e}")
6755
except SQLAlchemyError as e:
56+
LOGGER.error(f"SQLAlchemyError error when creating user: {e}")
57+
except Exception as e:
6858
LOGGER.error(f"Unexpected error when creating user: {e}")
69-
raise e
7059

7160

7261
def create_comment(session: Session, comment: Comment) -> Comment:
7362
"""
7463
Create a comment posted by `regular_user` on `admin_user`'s post.
7564
76-
:param session: SQLAlchemy database session.
77-
:type session: Session
78-
:param comment: User comment left on published post.
79-
:type comment: Comment
65+
:param Session session: SQLAlchemy database session.
66+
:param Comment comment: User comment left on published post.
8067
8168
:return: Comment
8269
"""
@@ -86,8 +73,8 @@ def create_comment(session: Session, comment: Comment) -> Comment:
8673
LOGGER.success(f"Created comment {comment} from user {comment.user.username}.")
8774
return comment
8875
except IntegrityError as e:
89-
LOGGER.error(e.orig)
90-
raise e.orig
76+
LOGGER.error(f"IntegrityError error when creating user: {e}")
9177
except SQLAlchemyError as e:
92-
LOGGER.error(f"Unexpected error when creating comment: {e}")
93-
raise e
78+
LOGGER.error(f"SQLAlchemyError error when creating user: {e}")
79+
except Exception as e:
80+
LOGGER.error(f"Unexpected error when creating user: {e}")

0 commit comments

Comments
 (0)
Please sign in to comment.