Skip to content

Commit 80cad4a

Browse files
committed
feat: ensuring dotenv while keeping pep8
1 parent a20666e commit 80cad4a

File tree

6 files changed

+111
-22
lines changed

6 files changed

+111
-22
lines changed

__test__/todos_test.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
from dotenv import load_dotenv
12
import asyncio
23
import pytest
3-
from app.redis import redis
4+
from app.redis import get_client
45
from app.components.todos.store import TodoStatus, TodoStore
56

6-
todos = TodoStore(redis)
7+
load_dotenv()
8+
9+
todos = TodoStore(get_client())
710

811
@pytest.fixture(autouse=True)
912
async def run_around_each():

src/app/components/todos/router.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,20 @@
55
from pydantic import BaseModel
66

77
from app.components.todos.store import Todo, TodoDocument, Todos, TodoStatus, TodoStore
8-
from app.redis import redis
8+
from app.redis import get_client
99

10-
todos = TodoStore(redis)
10+
todos_store: TodoStore | None = None
11+
12+
def get_todos() -> TodoStore:
13+
global todos_store
14+
15+
return todos_store if todos_store is not None else TodoStore(get_client())
1116

1217

1318
@asynccontextmanager
1419
async def lifespan(_: FastAPI) -> AsyncIterator[Never]:
1520
# before
21+
todos = get_todos()
1622
await todos.initialize()
1723
yield # type: ignore
1824
# after
@@ -24,16 +30,19 @@ async def lifespan(_: FastAPI) -> AsyncIterator[Never]:
2430

2531
@router.get("/", tags=["todos"])
2632
async def all() -> Todos:
33+
todos = get_todos()
2734
return await todos.all()
2835

2936

3037
@router.get("/search", tags=["todos"])
3138
async def search(name: str | None = None, status: TodoStatus | None = None) -> Todos:
39+
todos = get_todos()
3240
return await todos.search(name, status)
3341

3442

3543
@router.get("/{id}", tags=["todos"])
3644
async def one(id: str) -> Todo:
45+
todos = get_todos()
3746
return await todos.one(id)
3847

3948

@@ -44,6 +53,7 @@ class CreateTodo(BaseModel):
4453

4554
@router.post("/", tags=["todos"])
4655
async def create(todo: CreateTodo) -> TodoDocument:
56+
todos = get_todos()
4757
return await todos.create(todo.id, todo.name)
4858

4959

@@ -53,9 +63,11 @@ class UpdateTodo(BaseModel):
5363

5464
@router.patch("/{id}", tags=["todos"])
5565
async def update(id: str, todo: UpdateTodo) -> Todo:
66+
todos = get_todos()
5667
return await todos.update(id, todo.status)
5768

5869

5970
@router.delete("/{id}", tags=["todos"])
6071
async def delete(id: str) -> None:
72+
todos = get_todos()
6173
return await todos.delete(id)

src/app/components/todos/store.py

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,20 @@
2121

2222

2323
class TodoStatus(str, Enum):
24+
"""
25+
An enum for todo status
26+
"""
27+
2428
todo = "todo"
2529
in_progress = "in progress"
2630
complete = "complete"
2731

2832

2933
class Todo(BaseModel):
34+
"""
35+
Defines a todo
36+
"""
37+
3038
name: str
3139
status: TodoStatus
3240
created_date: datetime | None = None
@@ -42,28 +50,44 @@ def serialize_updated_date(self, dt: datetime) -> str:
4250

4351

4452
class TodoDocument(BaseModel):
53+
"""
54+
Defines a todo document including id as returned from redis
55+
"""
56+
4557
id: str
4658
value: Todo
4759

4860

4961
class Todos(BaseModel):
62+
"""
63+
Defines a list of todos and a total, mapping to the results of FT.SEARCH
64+
"""
65+
5066
total: int
5167
documents: List[TodoDocument]
5268

5369

5470
class TodoStore:
55-
"""Stores todos"""
71+
"""
72+
Stores and retrieves todos in redis
73+
"""
5674

5775
def __init__(self, redis: Redis):
5876
self.redis = redis
5977
self.INDEX = TODOS_INDEX
6078
self.PREFIX = TODOS_PREFIX
6179

6280
async def initialize(self) -> None:
81+
"""
82+
Sets up redis to be used with todos
83+
"""
6384
await self.create_index_if_not_exists()
6485
return None
6586

6687
async def have_index(self) -> bool:
88+
"""
89+
Checks if the TODOS_INDEX already exists in Redis
90+
"""
6791
try:
6892
await self.redis.ft(self.INDEX).info()
6993
except ResponseError as e:
@@ -75,6 +99,9 @@ async def have_index(self) -> bool:
7599
return True
76100

77101
async def create_index_if_not_exists(self) -> None:
102+
"""
103+
Creates the TODOS_INDEX if it doesn't exist already
104+
"""
78105
if await self.have_index():
79106
return None
80107

@@ -101,6 +128,9 @@ async def create_index_if_not_exists(self) -> None:
101128
return None
102129

103130
async def drop_index(self) -> None:
131+
"""
132+
Drops the TODOS_INDEX if it exists
133+
"""
104134
if not await self.have_index():
105135
return None
106136

@@ -115,36 +145,52 @@ async def drop_index(self) -> None:
115145
return None
116146

117147
def format_id(self, id: str) -> str:
148+
"""
149+
Allow for id with or without TODOS_PREFIX
150+
"""
118151
if re.match(f"^{self.PREFIX}", id):
119152
return id
120153

121154
return f"{self.PREFIX}{id}"
122155

123-
def parse_todo_document(self, todo: Document) -> TodoDocument:
156+
def deserialize_todo_document(self, todo: Document) -> TodoDocument:
157+
"""
158+
Deserializes a TodoDocument from JSON
159+
"""
124160
return TodoDocument(
125161
id=todo.id,
126162
value=Todo(**from_json(todo.json, allow_partial=True)), # type: ignore
127163
)
128164

129-
def parse_todo_documents(self, todos: list[Document]) -> list[TodoDocument]:
165+
def deserialize_todo_documents(self, todos: list[Document]) -> list[TodoDocument]:
166+
"""
167+
Deserializes a list[TodoDocument] from list[JSON]
168+
"""
130169
todo_docs = []
131170

132171
for doc in todos:
133-
todo_docs.append(self.parse_todo_document(doc))
172+
todo_docs.append(self.deserialize_todo_document(doc))
134173

135174
return todo_docs
136175

137176
async def all(self) -> Todos:
177+
"""
178+
Gets all todos
179+
"""
138180
try:
139181
result = await self.redis.ft(self.INDEX).search("*")
140182
return Todos(
141-
total=result.total, documents=self.parse_todo_documents(result.docs)
183+
total=result.total,
184+
documents=self.deserialize_todo_documents(result.docs),
142185
)
143186
except Exception as e:
144187
logger.error(f"Error getting all todos: {e}")
145188
raise
146189

147190
async def one(self, id: str) -> Todo:
191+
"""
192+
Gets a todo by id
193+
"""
148194
id = self.format_id(id)
149195

150196
try:
@@ -156,6 +202,9 @@ async def one(self, id: str) -> Todo:
156202
return Todo(**json)
157203

158204
async def search(self, name: str | None, status: TodoStatus | None) -> Todos:
205+
"""
206+
Searches for todos by name and/or status
207+
"""
159208
searches = []
160209

161210
if name is not None and len(name) > 0:
@@ -167,13 +216,17 @@ async def search(self, name: str | None, status: TodoStatus | None) -> Todos:
167216
try:
168217
result = await self.redis.ft(self.INDEX).search(Query(" ".join(searches)))
169218
return Todos(
170-
total=result.total, documents=self.parse_todo_documents(result.docs)
219+
total=result.total,
220+
documents=self.deserialize_todo_documents(result.docs),
171221
)
172222
except Exception as e:
173223
logger.error(f"Error getting todo {id}: {e}")
174224
raise
175225

176226
async def create(self, id: Optional[str], name: Optional[str]) -> TodoDocument:
227+
"""
228+
Creates a todo
229+
"""
177230
dt = datetime.now(UTC)
178231

179232
if name is None:
@@ -201,6 +254,9 @@ async def create(self, id: Optional[str], name: Optional[str]) -> TodoDocument:
201254
return todo
202255

203256
async def update(self, id: str, status: TodoStatus) -> Todo:
257+
"""
258+
Updates a todo
259+
"""
204260
dt = datetime.now(UTC)
205261

206262
todo = await self.one(id)
@@ -222,6 +278,9 @@ async def update(self, id: str, status: TodoStatus) -> Todo:
222278
return todo
223279

224280
async def delete(self, id: str) -> None:
281+
"""
282+
Deletes a todo
283+
"""
225284
try:
226285
await self.redis.json().delete(self.format_id(id))
227286
except Exception as e:
@@ -231,6 +290,9 @@ async def delete(self, id: str) -> None:
231290
return None
232291

233292
async def delete_all(self) -> None:
293+
"""
294+
Delete all todos
295+
"""
234296
todos = await self.all()
235297
coros = []
236298

src/app/env.py

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/app/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from dotenv import load_dotenv
12
from fastapi import FastAPI
23

34
from app.components.todos.router import router as todos_router
45

6+
load_dotenv()
57
app = FastAPI()
68
app.include_router(router=todos_router, prefix="/api/todos")

src/app/redis.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,25 @@
55
from redis.exceptions import ConnectionError, TimeoutError
66
from redis.retry import Retry
77

8-
redis_url = os.environ.get("REDIS_URL", "redis://localhost:6379")
9-
redis = Redis.from_url(
10-
redis_url,
11-
decode_responses=True,
12-
retry=Retry(ExponentialBackoff(cap=10, base=1), 25),
13-
retry_on_error=[ConnectionError, TimeoutError, ConnectionResetError],
14-
health_check_interval=1,
15-
)
8+
clients: dict[str, Redis] = {}
9+
10+
11+
def get_client(url: str | None = None) -> Redis:
12+
redis_url = (
13+
url
14+
if url is not None
15+
else os.environ.get("REDIS_URL", "redis://localhost:6379")
16+
)
17+
18+
if redis_url in clients:
19+
return clients[redis_url]
20+
21+
clients[redis_url] = Redis.from_url(
22+
redis_url,
23+
decode_responses=True,
24+
retry=Retry(ExponentialBackoff(cap=10, base=1), 25),
25+
retry_on_error=[ConnectionError, TimeoutError, ConnectionResetError],
26+
health_check_interval=1,
27+
)
28+
29+
return clients[redis_url]

0 commit comments

Comments
 (0)