Skip to content

Commit f8e3022

Browse files
committed
Merge branch '2025-context'
2 parents 268c7f6 + e9d3d10 commit f8e3022

File tree

7 files changed

+284
-0
lines changed

7 files changed

+284
-0
lines changed

2025/context/.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

2025/context/before.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import logging
2+
from db import db_session, init_db, Article, Session
3+
4+
# -----------------------------------
5+
# Application Logic
6+
# -----------------------------------
7+
8+
def render_article(article_id: int, db: Session, logger: logging.Logger, api_key: str) -> str:
9+
article = db.query(Article).filter(Article.id == article_id).first()
10+
if not article:
11+
logger.error(f"Article {article_id} not found.")
12+
return "<p>Article not found.</p>"
13+
14+
logger.info(f"Rendering article {article_id} using API key {api_key[:4]}...")
15+
html = f"<h1>{article.title}</h1><p>{article.body}</p>"
16+
return html
17+
18+
def main() -> None:
19+
logging.basicConfig(level=logging.INFO)
20+
logger = logging.getLogger("app")
21+
22+
init_db()
23+
24+
with db_session() as session:
25+
26+
api_key = "abcdef123456"
27+
28+
html = render_article(1, session, logger, api_key)
29+
print(html)
30+
31+
html = render_article(999, session, logger, api_key) # not found
32+
print(html)
33+
34+
35+
if __name__ == "__main__":
36+
main()
37+
38+

2025/context/context_v1.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import logging
2+
from dataclasses import dataclass
3+
from typing import Any
4+
from db import db_session, init_db, Article , Session
5+
6+
# -----------------------------------
7+
# Context Object
8+
# -----------------------------------
9+
10+
@dataclass
11+
class AppContext:
12+
user_id: int
13+
db: Session
14+
logger: logging.Logger
15+
config: dict[str, Any]
16+
17+
# -----------------------------------
18+
# Application Logic
19+
# -----------------------------------
20+
21+
def render_article(article_id: int, context: AppContext) -> str:
22+
article = context.db.query(Article).filter(Article.id == article_id).first()
23+
if not article:
24+
context.logger.error(f"Article {article_id} not found.")
25+
return "<p>Article not found.</p>"
26+
27+
context.logger.info(f"Rendering article {article_id}")
28+
html = f"<h1>{article.title}</h1><p>{article.body}</p>"
29+
return html
30+
# -----------------------------------
31+
# Entry Point
32+
# -----------------------------------
33+
34+
def main() -> None:
35+
logging.basicConfig(level=logging.INFO)
36+
logger = logging.getLogger("app")
37+
38+
init_db()
39+
40+
with db_session() as session:
41+
context = AppContext(
42+
user_id=42,
43+
db=session,
44+
logger=logger,
45+
config={"api_key": "abcdef123456"},
46+
)
47+
48+
html = render_article(1, context)
49+
print(html)
50+
51+
html = render_article(999, context) # not found
52+
print(html)
53+
54+
if __name__ == "__main__":
55+
main()
56+
57+

2025/context/context_v2.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import logging
2+
from dataclasses import dataclass
3+
from typing import Protocol, Any
4+
from db import db_session, init_db, Article
5+
6+
# -----------------------------------
7+
# Protocols for loose coupling
8+
# -----------------------------------
9+
10+
class LoggerProtocol(Protocol):
11+
def info(self, msg: str, *args: Any, **kwargs: Any) -> None: ...
12+
def error(self, msg: str, *args: Any, **kwargs: Any) -> None: ...
13+
14+
class DBProtocol(Protocol):
15+
def query(self, *args: Any, **kwargs: Any) -> Any: ...
16+
17+
# -----------------------------------
18+
# Context Object
19+
# -----------------------------------
20+
21+
@dataclass
22+
class AppContext:
23+
user_id: int
24+
db: DBProtocol
25+
logger: LoggerProtocol
26+
config: dict[str, Any]
27+
28+
# -----------------------------------
29+
# Application Logic
30+
# -----------------------------------
31+
32+
def render_article(article_id: int, db: DBProtocol, logger: LoggerProtocol) -> str:
33+
article = db.query(Article).filter(Article.id == article_id).first()
34+
if not article:
35+
logger.error(f"Article {article_id} not found.")
36+
return "<p>Article not found.</p>"
37+
38+
logger.info(f"Rendering article {article_id}")
39+
html = f"<h1>{article.title}</h1><p>{article.body}</p>"
40+
return html
41+
42+
def send_to_external_service(html: str, api_key: str) -> None:
43+
print(f"Sending to API with key {api_key[:4]}... Content: {html[:30]}...")
44+
45+
def publish_article(article_id: int, context: AppContext) -> None:
46+
html = render_article(
47+
article_id,
48+
db=context.db,
49+
logger=context.logger,
50+
)
51+
send_to_external_service(html, context.config['api_key'])
52+
53+
# -----------------------------------
54+
# Entry Point
55+
# -----------------------------------
56+
57+
def main() -> None:
58+
logging.basicConfig(level=logging.INFO)
59+
logger = logging.getLogger("app")
60+
61+
init_db()
62+
63+
with db_session() as session:
64+
context = AppContext(
65+
user_id=42,
66+
db=session,
67+
logger=logger,
68+
config={"api_key": "abcdef123456"},
69+
)
70+
71+
publish_article(1, context)
72+
publish_article(2, context)
73+
publish_article(999, context) # Not found example
74+
75+
if __name__ == "__main__":
76+
main()
77+
78+

2025/context/db.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from contextlib import contextmanager
2+
from sqlalchemy import Column, Integer, String, create_engine
3+
from sqlalchemy.orm import Session, declarative_base, sessionmaker
4+
from typing import Generator
5+
6+
Base = declarative_base()
7+
8+
class Article(Base):
9+
__tablename__ = "articles"
10+
11+
id = Column(Integer, primary_key=True)
12+
title = Column(String)
13+
body = Column(String)
14+
15+
# SQLite in-memory DB for demo purposes
16+
engine = create_engine("sqlite:///:memory:", echo=False, future=True)
17+
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
18+
19+
def init_db() -> None:
20+
Base.metadata.create_all(bind=engine)
21+
with db_session() as session:
22+
session.add_all([
23+
Article(id=1, title="Hello", body="World!"),
24+
Article(id=2, title="Python", body="Is Awesome"),
25+
])
26+
session.commit()
27+
28+
@contextmanager
29+
def db_session() -> Generator[Session, None, None]:
30+
session = SessionLocal()
31+
try:
32+
yield session
33+
finally:
34+
session.close()

2025/context/pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[project]
2+
name = "context-object-example"
3+
version = "0.1.0"
4+
description = "An example of the Context Object Pattern using SQLite and SQLAlchemy"
5+
requires-python = ">=3.13"
6+
dependencies = [
7+
"sqlalchemy>=2.0",
8+
]

2025/context/uv.lock

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

0 commit comments

Comments
 (0)