Skip to content

Commit d70c329

Browse files
committed
Merge branch 'develop' into feature/keyboard-shortcuts
2 parents ab47c1e + bb388ce commit d70c329

23 files changed

+7122
-34
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ dmypy.json
138138
# Pyre type checker
139139
.pyre/
140140

141-
142141
# VScode
143142
.vscode/
144143
app/.vscode/

app/config.py.example

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class Settings(BaseSettings):
1313
env_file = ".env"
1414

1515

16-
# general
16+
# GENERAL
1717
DOMAIN = 'Our-Domain'
1818

1919
# DATABASE
@@ -29,11 +29,11 @@ AVATAR_SIZE = (120, 120)
2929
# API-KEYS
3030
WEATHER_API_KEY = os.getenv('WEATHER_API_KEY')
3131

32-
# export
32+
# EXPORT
3333
ICAL_VERSION = '2.0'
3434
PRODUCT_ID = '-//Our product id//'
3535

36-
# email
36+
# EMAIL
3737
email_conf = ConnectionConfig(
3838
MAIL_USERNAME=os.getenv("MAIL_USERNAME") or "user",
3939
MAIL_PASSWORD=os.getenv("MAIL_PASSWORD") or "password",
@@ -47,3 +47,14 @@ email_conf = ConnectionConfig(
4747

4848
# PATHS
4949
STATIC_ABS_PATH = os.path.abspath("static")
50+
51+
# LOGGER
52+
LOG_PATH = "./var/log"
53+
LOG_FILENAME = "calendar.log"
54+
LOG_LEVEL = "error"
55+
LOG_ROTATION_INTERVAL = "20 days"
56+
LOG_RETENTION_INTERVAL = "1 month"
57+
LOG_FORMAT = ("<level>{level: <8}</level>"
58+
" <green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green>"
59+
" - <cyan>{name}</cyan>:<cyan>{function}</cyan>"
60+
" - <level>{message}</level>")

app/database/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,11 @@ def __repr__(self):
108108
f'({self.event.owner}'
109109
f'to {self.recipient})>'
110110
)
111+
112+
113+
class Quote(Base):
114+
__tablename__ = "quotes"
115+
116+
id = Column(Integer, primary_key=True, index=True)
117+
text = Column(String, nullable=False)
118+
author = Column(String)

app/internal/logger_customizer.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import sys
2+
3+
from pathlib import Path
4+
from loguru import logger, _Logger as Logger
5+
6+
7+
class LoggerConfigError(Exception):
8+
pass
9+
10+
11+
class LoggerCustomizer:
12+
13+
@classmethod
14+
def make_logger(cls, log_path: Path,
15+
log_filename: str,
16+
log_level: str,
17+
log_rotation_interval: str,
18+
log_retention_interval: str,
19+
log_format: str) -> Logger:
20+
"""Creates a logger from given configurations
21+
22+
Args:
23+
log_path (Path): Path where the log file is located
24+
log_filename (str):
25+
26+
log_level (str): The level we want to start logging from
27+
log_rotation_interval (str): Every how long the logs
28+
would be rotated
29+
log_retention_interval (str): Amount of time in words defining
30+
how long the log will be kept
31+
log_format (str): The logging format
32+
33+
Raises:
34+
LoggerConfigError: Error raised when the configuration is invalid
35+
36+
Returns:
37+
Logger: Loguru logger instance
38+
"""
39+
try:
40+
logger = cls.customize_logging(
41+
file_path=Path(log_path) / Path(log_filename),
42+
level=log_level,
43+
retention=log_retention_interval,
44+
rotation=log_rotation_interval,
45+
format=log_format
46+
)
47+
except (TypeError, ValueError) as err:
48+
raise LoggerConfigError(
49+
f"You have an issue with the logger configuration: {err!r}, "
50+
"fix it please")
51+
52+
return logger
53+
54+
@classmethod
55+
def customize_logging(cls,
56+
file_path: Path,
57+
level: str,
58+
rotation: str,
59+
retention: str,
60+
format: str
61+
) -> Logger:
62+
"""Used to customize the logger instance
63+
64+
Args:
65+
file_path (Path): Path where the log file is located
66+
level (str): The level wanted to start logging from
67+
rotation (str): Every how long the logs would be
68+
rotated(creation of new file)
69+
retention (str): Amount of time in words defining how
70+
long a log is kept
71+
format (str): The logging format
72+
73+
Returns:
74+
Logger: Instance of a logger mechanism
75+
"""
76+
logger.remove()
77+
logger.add(
78+
sys.stdout,
79+
enqueue=True,
80+
backtrace=True,
81+
level=level.upper(),
82+
format=format
83+
)
84+
logger.add(
85+
str(file_path),
86+
rotation=rotation,
87+
retention=retention,
88+
enqueue=True,
89+
backtrace=True,
90+
level=level.upper(),
91+
format=format
92+
)
93+
94+
return logger

app/internal/quotes/daily_quotes.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from datetime import date
2+
from typing import Optional
3+
4+
from app.database.models import Quote
5+
6+
from sqlalchemy.orm import Session
7+
from sqlalchemy.sql.expression import func
8+
9+
TOTAL_DAYS = 366
10+
11+
12+
def quote_per_day(
13+
session: Session, date: date = date.today()
14+
) -> Optional[Quote]:
15+
"""This function provides a daily quote, relevant to the current
16+
day of the year. The quote is randomally selected from a set
17+
of quotes matching to the given day"""
18+
day_num = date.timetuple().tm_yday
19+
quote = session.query(Quote).filter(
20+
Quote.id % TOTAL_DAYS == day_num).order_by(func.random()).first()
21+
return quote

app/internal/quotes/load_quotes.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import json
2+
from typing import Dict, List, Optional
3+
4+
from app.database.models import Quote
5+
6+
from sqlalchemy.orm import Session
7+
8+
9+
def get_quotes_from_json() -> List[Dict[str, Optional[str]]]:
10+
"""This function reads all of the daily quotes from a specific JSON file.
11+
The JSON file content is copied from the free API:
12+
'https://type.fit/api/quotes'. I saved the content so the API won't be
13+
called every time the app is initialized."""
14+
try:
15+
with open('app/resources/quotes.json', 'r') as f:
16+
quotes_list = json.load(f)
17+
except (IOError, ValueError):
18+
return []
19+
return quotes_list
20+
21+
22+
def add_quotes_to_db(session: Session) -> None:
23+
"""This function reads the quotes and inserts them into the db"""
24+
all_quotes = get_quotes_from_json()
25+
quotes_objects = [
26+
Quote(text=quote['text'], author=quote['author'])
27+
for quote in all_quotes
28+
]
29+
session.add_all(quotes_objects)
30+
session.commit()
31+
32+
33+
def is_quotes_table_empty(session: Session) -> bool:
34+
return session.query(Quote).count() == 0
35+
36+
37+
def load_daily_quotes(session: Session) -> None:
38+
"""This function loads the daily quotes to the db,
39+
if they weren't already loaden"""
40+
if is_quotes_table_empty(session):
41+
add_quotes_to_db(session)

app/main.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
from fastapi import FastAPI, Request
1+
from fastapi import Depends, FastAPI, Request
22
from fastapi.staticfiles import StaticFiles
3+
from sqlalchemy.orm import Session
34

45
from app.config import PSQL_ENVIRONMENT
56
from app.database import models
6-
from app.database.database import engine
7+
from app.database.database import engine, get_db
78
from app.dependencies import (
89
MEDIA_PATH, STATIC_PATH, templates)
10+
from app.internal.quotes import load_quotes, daily_quotes
911
from app.routers import (
10-
agenda, dayview, email, event, invitation, profile, search, telegram, keyboard_shortcuts)
12+
agenda, dayview, email, event, invitation, profile, search, telegram, keyboard_shortcuts, whatsapp)
13+
1114
from app.telegram.bot import telegram_bot
15+
from app.internal.logger_customizer import LoggerCustomizer
16+
from app import config
1217

1318

1419
def create_tables(engine, psql_environment):
@@ -27,22 +32,39 @@ def create_tables(engine, psql_environment):
2732
app.mount("/static", StaticFiles(directory=STATIC_PATH), name="static")
2833
app.mount("/media", StaticFiles(directory=MEDIA_PATH), name="media")
2934

35+
load_quotes.load_daily_quotes(next(get_db()))
36+
37+
# Configure logger
38+
logger = LoggerCustomizer.make_logger(config.LOG_PATH,
39+
config.LOG_FILENAME,
40+
config.LOG_LEVEL,
41+
config.LOG_ROTATION_INTERVAL,
42+
config.LOG_RETENTION_INTERVAL,
43+
config.LOG_FORMAT)
44+
app.logger = logger
45+
3046
app.include_router(profile.router)
3147
app.include_router(event.router)
3248
app.include_router(agenda.router)
3349
app.include_router(telegram.router)
3450
app.include_router(dayview.router)
3551
app.include_router(email.router)
3652
app.include_router(invitation.router)
53+
app.include_router(whatsapp.router)
3754
app.include_router(search.router)
3855
app.include_router(keyboard_shortcuts.router)
3956

4057
telegram_bot.set_webhook()
4158

4259

60+
# TODO: I add the quote day to the home page
61+
# until the relavent calendar view will be developed.
4362
@app.get("/")
44-
async def home(request: Request):
63+
@app.logger.catch()
64+
async def home(request: Request, db: Session = Depends(get_db)):
65+
quote = daily_quotes.quote_per_day(db)
4566
return templates.TemplateResponse("home.html", {
4667
"request": request,
4768
"message": "Hello, World!",
69+
"quote": quote
4870
})

0 commit comments

Comments
 (0)