Skip to content

Commit 28adc74

Browse files
committed
Merge branch 'develop' of https://github.com/PythonFreeCourse/calendar into horoscope
2 parents eda6a3d + b5f0545 commit 28adc74

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1725
-131
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ __pycache__/
1313
# Distribution / packaging
1414
.Python
1515
build/
16+
Scripts/
17+
include/
1618
develop-eggs/
1719
dist/
1820
downloads/
@@ -30,6 +32,8 @@ share/python-wheels/
3032
.installed.cfg
3133
*.egg
3234
MANIFEST
35+
pyvenv.cfg
36+
3337

3438
# PyInstaller
3539
# Usually these files are written by a python script from a template
@@ -116,6 +120,7 @@ venv/
116120
ENV/
117121
env.bak/
118122
venv.bak/
123+
.vscode/
119124
Scripts/*
120125
pyvenv.cfg
121126

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ uvicorn app.main:app --reload
3535
python -m venv venv
3636
source venv/bin/activate
3737
pip install -r requirements.txt
38-
cp app/config.py.example app/configuration.py
38+
cp app/config.py.example app/config.py
3939
# Edit the variables' values.
4040
uvicorn app.main:app --reload
4141
```

app/database/database.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from sqlalchemy import create_engine
44
from sqlalchemy.ext.declarative import declarative_base
5-
from sqlalchemy.orm import sessionmaker
5+
from sqlalchemy.orm import Session, sessionmaker
66

77
from app import config
88

@@ -25,7 +25,7 @@ def create_env_engine(psql_environment, sqlalchemy_database_url):
2525
Base = declarative_base()
2626

2727

28-
def get_db():
28+
def get_db() -> Session:
2929
db = SessionLocal()
3030
try:
3131
yield db

app/database/models.py

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
from __future__ import annotations
2+
13
from datetime import datetime
4+
from typing import Dict, Any
25

36
from sqlalchemy import (DDL, Boolean, Column, DateTime, ForeignKey, Index,
4-
Integer, String, event)
7+
Integer, String, event, UniqueConstraint)
58
from sqlalchemy.dialects.postgresql import TSVECTOR
6-
from sqlalchemy.orm import relationship
9+
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
10+
from sqlalchemy.orm import relationship, Session
711

812
from app.config import PSQL_ENVIRONMENT
913
from app.database.database import Base
14+
from app.dependencies import logger
1015

1116

1217
class UserEvent(Base):
@@ -52,11 +57,12 @@ class Event(Base):
5257
end = Column(DateTime, nullable=False)
5358
content = Column(String)
5459
location = Column(String)
60+
color = Column(String, nullable=True)
5561

56-
owner = relationship("User")
5762
owner_id = Column(Integer, ForeignKey("users.id"))
58-
color = Column(String, nullable=True)
63+
category_id = Column(Integer, ForeignKey("categories.id"))
5964

65+
owner = relationship("User")
6066
participants = relationship("UserEvent", back_populates="events")
6167

6268
# PostgreSQL
@@ -66,12 +72,45 @@ class Event(Base):
6672
'events_tsv_idx',
6773
'events_tsv',
6874
postgresql_using='gin'),
69-
)
75+
)
7076

7177
def __repr__(self):
7278
return f'<Event {self.id}>'
7379

7480

81+
class Category(Base):
82+
__tablename__ = "categories"
83+
84+
__table_args__ = (
85+
UniqueConstraint('user_id', 'name', 'color'),
86+
)
87+
id = Column(Integer, primary_key=True, index=True)
88+
name = Column(String, nullable=False)
89+
color = Column(String, nullable=False)
90+
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
91+
92+
@staticmethod
93+
def create(db_session: Session, name: str, color: str,
94+
user_id: int) -> Category:
95+
try:
96+
category = Category(name=name, color=color, user_id=user_id)
97+
db_session.add(category)
98+
db_session.flush()
99+
db_session.commit()
100+
db_session.refresh(category)
101+
except (SQLAlchemyError, IntegrityError) as e:
102+
logger.error(f"Failed to create category: {e}")
103+
raise e
104+
else:
105+
return category
106+
107+
def to_dict(self) -> Dict[str, Any]:
108+
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
109+
110+
def __repr__(self) -> str:
111+
return f'<Category {self.id} {self.name} {self.color}>'
112+
113+
75114
class PSQLEnvironmentError(Exception):
76115
pass
77116

@@ -89,7 +128,7 @@ class PSQLEnvironmentError(Exception):
89128
Event.__table__,
90129
'after_create',
91130
trigger_snippet.execute_if(dialect='postgresql')
92-
)
131+
)
93132

94133

95134
class Invitation(Base):
@@ -118,3 +157,22 @@ class Quote(Base):
118157
id = Column(Integer, primary_key=True, index=True)
119158
text = Column(String, nullable=False)
120159
author = Column(String)
160+
161+
162+
class Zodiac(Base):
163+
__tablename__ = "zodiac-signs"
164+
165+
id = Column(Integer, primary_key=True, index=True)
166+
name = Column(String, nullable=False)
167+
start_month = Column(Integer, nullable=False)
168+
start_day_in_month = Column(Integer, nullable=False)
169+
end_month = Column(Integer, nullable=False)
170+
end_day_in_month = Column(Integer, nullable=False)
171+
172+
def __repr__(self):
173+
return (
174+
f'<Zodiac '
175+
f'{self.name} '
176+
f'{self.start_day_in_month}/{self.start_month}-'
177+
f'{self.end_day_in_month}/{self.end_month}>'
178+
)

app/internal/quotes/daily_quotes.py renamed to app/internal/daily_quotes.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import date
2-
from typing import Optional
2+
from typing import Dict, Optional
33

44
from app.database.models import Quote
55

@@ -9,6 +9,15 @@
99
TOTAL_DAYS = 366
1010

1111

12+
def create_quote_object(quotes_fields: Dict[str, Optional[str]]) -> Quote:
13+
"""This function create a quote object from given fields dictionary.
14+
It is used for adding the data from the json into the db"""
15+
return Quote(
16+
text=quotes_fields['text'],
17+
author=quotes_fields['author']
18+
)
19+
20+
1221
def quote_per_day(
1322
session: Session, date: date = date.today()
1423
) -> Optional[Quote]:

app/internal/json_data_loader.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import json
2+
import os
3+
from typing import Dict, List, Callable, Union
4+
5+
from loguru import logger
6+
from sqlalchemy.orm import Session
7+
8+
from app.database.database import Base
9+
from app.database.models import Quote, Zodiac
10+
from app.internal import daily_quotes, zodiac
11+
12+
13+
def get_data_from_json(path: str) -> List[Dict[str, Union[str, int, None]]]:
14+
"""This function reads all of the data from a specific JSON file.
15+
The json file consists of list of dictionaries"""
16+
try:
17+
with open(path, 'r') as f:
18+
json_content_list = json.load(f)
19+
except (IOError, ValueError):
20+
file_name = os.path.basename(path)
21+
logger.exception(
22+
f"An error occurred during reading of json file: {file_name}")
23+
return []
24+
return json_content_list
25+
26+
27+
def is_table_empty(session: Session, table: Base) -> bool:
28+
return session.query(table).count() == 0
29+
30+
31+
def load_data(
32+
session: Session, path: str,
33+
table: Base, object_creator_function: Callable) -> None:
34+
"""This function loads the specific data to the db,
35+
if it wasn't already loaded"""
36+
if not is_table_empty(session, table):
37+
return None
38+
json_objects_list = get_data_from_json(path)
39+
objects = [
40+
object_creator_function(json_object)
41+
for json_object in json_objects_list]
42+
session.add_all(objects)
43+
session.commit()
44+
45+
46+
def load_to_db(session) -> None:
47+
"""This function loads all the data for features
48+
based on pre-defind json data"""
49+
load_data(
50+
session, 'app/resources/zodiac.json',
51+
Zodiac, zodiac.create_zodiac_object)
52+
load_data(
53+
session, 'app/resources/quotes.json',
54+
Quote, daily_quotes.create_quote_object)
55+
"""The quotes JSON file content is copied from the free API:
56+
'https://type.fit/api/quotes'. I saved the content so the API won't be
57+
called every time the app is initialized."""

app/internal/quotes/load_quotes.py

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

app/internal/zodiac.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from datetime import date
2+
from typing import Dict, Union
3+
4+
from sqlalchemy.orm import Session
5+
from sqlalchemy import and_, or_
6+
7+
from app.database.models import Zodiac
8+
9+
10+
def create_zodiac_object(zodiac_fields: Dict[str, Union[str, int]]) -> Zodiac:
11+
"""This function create a zodiac object from given fields dictionary.
12+
It is used for adding the data from the json into the db"""
13+
return Zodiac(
14+
name=zodiac_fields['name'],
15+
start_month=zodiac_fields['start_month'],
16+
start_day_in_month=zodiac_fields['start_day_in_month'],
17+
end_month=zodiac_fields['end_month'],
18+
end_day_in_month=zodiac_fields['end_day_in_month']
19+
)
20+
21+
22+
def get_zodiac_of_day(session: Session, date: date) -> Zodiac:
23+
"""This function return a zodiac object
24+
according to the current day."""
25+
first_month_of_sign_filter = and_(
26+
Zodiac.start_month == date.month,
27+
Zodiac.start_day_in_month <= date.day)
28+
second_month_of_sign_filter = and_(
29+
Zodiac.end_month == date.month,
30+
Zodiac.end_day_in_month >= date.day)
31+
zodiac_obj = session.query(Zodiac).filter(
32+
or_(first_month_of_sign_filter, second_month_of_sign_filter)).first()
33+
return zodiac_obj
34+
35+
36+
# TODO: Call this function from the month view
37+
def get_zodiac_of_month(session: Session, date: date) -> Zodiac:
38+
"""This function return a zodiac object
39+
according to the current month."""
40+
zodiac_obj = session.query(Zodiac).filter(
41+
Zodiac.end_month == date.month).first()
42+
return zodiac_obj

app/main.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
from app.database import models
77
from app.database.database import engine, get_db
88
from app.dependencies import (logger, MEDIA_PATH, STATIC_PATH, templates)
9-
from app.internal.quotes import daily_quotes, load_quotes
9+
from app.internal import daily_quotes, json_data_loader
1010
from app.routers import (
11-
agenda, dayview, email, event, invitation, profile, search, telegram,
12-
whatsapp
11+
agenda, calendar, categories, dayview, email,
12+
event, invitation, profile, search, telegram, whatsapp
1313
)
1414
from app.telegram.bot import telegram_bot
1515

@@ -30,25 +30,32 @@ def create_tables(engine, psql_environment):
3030
app.mount("/static", StaticFiles(directory=STATIC_PATH), name="static")
3131
app.mount("/media", StaticFiles(directory=MEDIA_PATH), name="media")
3232

33-
load_quotes.load_daily_quotes(next(get_db()))
33+
json_data_loader.load_to_db(next(get_db()))
3434

3535
app.logger = logger
3636

37-
app.include_router(profile.router)
38-
app.include_router(event.router)
39-
app.include_router(agenda.router)
40-
app.include_router(telegram.router)
41-
app.include_router(dayview.router)
42-
app.include_router(email.router)
43-
app.include_router(invitation.router)
44-
app.include_router(whatsapp.router)
45-
app.include_router(search.router)
37+
routers_to_include = [
38+
agenda.router,
39+
calendar.router,
40+
categories.router,
41+
dayview.router,
42+
email.router,
43+
event.router,
44+
invitation.router,
45+
profile.router,
46+
search.router,
47+
telegram.router,
48+
whatsapp.router,
49+
]
50+
51+
for router in routers_to_include:
52+
app.include_router(router)
4653

4754
telegram_bot.set_webhook()
4855

4956

5057
# TODO: I add the quote day to the home page
51-
# until the relavent calendar view will be developed.
58+
# until the relevant calendar view will be developed.
5259
@app.get("/")
5360
@logger.catch()
5461
async def home(request: Request, db: Session = Depends(get_db)):

0 commit comments

Comments
 (0)